Compare commits

...

1 Commits

Author SHA1 Message Date
Peter Steinberger
478b599e9b refactor: extract secrets core package 2026-05-30 19:14:25 -04:00
33 changed files with 929 additions and 558 deletions

View File

@@ -177,6 +177,18 @@
"@openclaw/media-generation-core/*": [
"../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts"
],
"@openclaw/secrets-core": [
"../dist/plugin-sdk/packages/secrets-core/src/index.d.ts"
],
"@openclaw/secrets-core/secret-input": [
"../dist/plugin-sdk/packages/secrets-core/src/secret-input.d.ts"
],
"@openclaw/secrets-core/secret-ref": [
"../dist/plugin-sdk/packages/secrets-core/src/secret-ref.d.ts"
],
"@openclaw/secrets-core/*": [
"../dist/plugin-sdk/packages/secrets-core/src/*.d.ts"
],
"@openclaw/terminal-core": [
"../dist/plugin-sdk/packages/terminal-core/src/index.d.ts"
],

View File

@@ -186,6 +186,18 @@
"@openclaw/media-generation-core/*": [
"../../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts"
],
"@openclaw/secrets-core": [
"../../dist/plugin-sdk/packages/secrets-core/src/index.d.ts"
],
"@openclaw/secrets-core/secret-input": [
"../../dist/plugin-sdk/packages/secrets-core/src/secret-input.d.ts"
],
"@openclaw/secrets-core/secret-ref": [
"../../dist/plugin-sdk/packages/secrets-core/src/secret-ref.d.ts"
],
"@openclaw/secrets-core/*": [
"../../dist/plugin-sdk/packages/secrets-core/src/*.d.ts"
],
"@openclaw/terminal-core": [
"../../dist/plugin-sdk/packages/terminal-core/src/index.d.ts"
],

View File

@@ -16,6 +16,7 @@
"../../packages/markdown-core/src/**/*.ts",
"../../packages/media-generation-core/src/**/*.ts",
"../../packages/model-catalog-core/src/**/*.ts",
"../../packages/secrets-core/src/**/*.ts",
"../../packages/terminal-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",

View File

@@ -0,0 +1,3 @@
import { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, SecretDefaults, SecretInput, SecretRef, SecretRefSource, coerceSecretRef, isSecretRef, isValidEnvSecretRefId, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef } from "./secret-ref.mjs";
import { SecretInputStringResolution, SecretInputStringResolutionMode, assertSecretInputResolved, hasConfiguredSecretInput, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, resolveSecretInputString } from "./secret-input.mjs";
export { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, SecretDefaults, SecretInput, SecretInputStringResolution, SecretInputStringResolutionMode, SecretRef, SecretRefSource, assertSecretInputResolved, coerceSecretRef, hasConfiguredSecretInput, isSecretRef, isValidEnvSecretRefId, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef, resolveSecretInputString };

3
packages/secrets-core/dist/index.mjs vendored Normal file
View File

@@ -0,0 +1,3 @@
import { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, coerceSecretRef, isSecretRef, isValidEnvSecretRefId, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef } from "./secret-ref.mjs";
import { assertSecretInputResolved, hasConfiguredSecretInput, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, resolveSecretInputString } from "./secret-input.mjs";
export { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, assertSecretInputResolved, coerceSecretRef, hasConfiguredSecretInput, isSecretRef, isValidEnvSecretRefId, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef, resolveSecretInputString };

View File

@@ -0,0 +1,49 @@
import { SecretDefaults, SecretRef } from "./secret-ref.mjs";
//#region packages/secrets-core/src/secret-input.d.ts
type SecretInputStringResolutionMode = "strict" | "inspect";
type SecretInputStringResolution = {
status: "available";
value: string;
ref: null;
} | {
status: "configured_unavailable";
value: undefined;
ref: SecretRef;
} | {
status: "missing";
value: undefined;
ref: null;
};
/**
* Normalize copy/pasted credentials for HTTP/auth use.
*
* Line breaks embedded in API keys are common paste artifacts, and non-Latin1
* rich-text characters can crash header construction before auth fails.
* Preserve ordinary internal spaces for values such as `Bearer <token>`.
*/
declare function normalizeSecretInput(value: unknown): string;
declare function normalizeOptionalSecretInput(value: unknown): string | undefined;
declare function normalizeSecretInputString(value: unknown): string | undefined;
declare function hasConfiguredSecretInput(value: unknown, defaults?: SecretDefaults): boolean;
declare function assertSecretInputResolved(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): void;
declare function resolveSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
mode?: SecretInputStringResolutionMode;
}): SecretInputStringResolution;
declare function normalizeResolvedSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): string | undefined;
//#endregion
export { SecretInputStringResolution, SecretInputStringResolutionMode, assertSecretInputResolved, hasConfiguredSecretInput, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, resolveSecretInputString };

View File

@@ -0,0 +1,86 @@
import { coerceSecretRef, resolveSecretInputRef } from "./secret-ref.mjs";
//#region packages/secrets-core/src/secret-input.ts
/**
* Normalize copy/pasted credentials for HTTP/auth use.
*
* Line breaks embedded in API keys are common paste artifacts, and non-Latin1
* rich-text characters can crash header construction before auth fails.
* Preserve ordinary internal spaces for values such as `Bearer <token>`.
*/
function normalizeSecretInput(value) {
if (typeof value !== "string") return "";
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 255) latin1Only += char;
}
return latin1Only.trim();
}
function normalizeOptionalSecretInput(value) {
const normalized = normalizeSecretInput(value);
return normalized ? normalized : void 0;
}
function normalizeSecretInputString(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function hasConfiguredSecretInput(value, defaults) {
if (normalizeSecretInputString(value)) return true;
return coerceSecretRef(value, defaults) !== null;
}
function formatSecretRefLabel(ref) {
return `${ref.source}:${ref.provider}:${ref.id}`;
}
function createUnresolvedSecretInputError(params) {
return /* @__PURE__ */ new Error(`${params.path}: unresolved SecretRef "${formatSecretRefLabel(params.ref)}". Resolve this command against an active gateway runtime snapshot before reading it.`);
}
function assertSecretInputResolved(params) {
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults
});
if (!ref) return;
throw createUnresolvedSecretInputError({
path: params.path,
ref
});
}
function resolveSecretInputString(params) {
const normalized = normalizeSecretInputString(params.value);
if (normalized) return {
status: "available",
value: normalized,
ref: null
};
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults
});
if (!ref) return {
status: "missing",
value: void 0,
ref: null
};
if ((params.mode ?? "strict") === "strict") throw createUnresolvedSecretInputError({
path: params.path,
ref
});
return {
status: "configured_unavailable",
value: void 0,
ref
};
}
function normalizeResolvedSecretInputString(params) {
const resolved = resolveSecretInputString({
...params,
mode: "strict"
});
if (resolved.status === "available") return resolved.value;
}
//#endregion
export { assertSecretInputResolved, hasConfiguredSecretInput, normalizeOptionalSecretInput, normalizeResolvedSecretInputString, normalizeSecretInput, normalizeSecretInputString, resolveSecretInputString };

View File

@@ -0,0 +1,40 @@
//#region packages/secrets-core/src/secret-ref.d.ts
type SecretRefSource = "env" | "file" | "exec";
/**
* Stable identifier for a secret in a configured source.
* Examples:
* - env source: provider "default", id "OPENAI_API_KEY"
* - file source: provider "mounted-json", id "/providers/openai/apiKey"
* - exec source: provider "vault", id "openai/api-key"
*/
type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
type SecretInput = string | SecretRef;
type SecretDefaults = {
env?: string;
file?: string;
exec?: string;
};
declare const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
declare const ENV_SECRET_REF_ID_RE: RegExp;
declare const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
declare const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
declare function isValidEnvSecretRefId(value: string): boolean;
declare function isSecretRef(value: unknown): value is SecretRef;
declare function parseEnvTemplateSecretRef(value: unknown, provider?: string): SecretRef | null;
declare function parseLegacySecretRefEnvMarker(value: unknown, provider?: string): SecretRef | null;
declare function coerceSecretRef(value: unknown, defaults?: SecretDefaults): SecretRef | null;
declare function resolveSecretInputRef(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
}): {
explicitRef: SecretRef | null;
inlineRef: SecretRef | null;
ref: SecretRef | null;
};
//#endregion
export { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, SecretDefaults, SecretInput, SecretRef, SecretRefSource, coerceSecretRef, isSecretRef, isValidEnvSecretRefId, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef };

View File

@@ -0,0 +1,84 @@
//#region packages/secrets-core/src/secret-ref.ts
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
const SECRET_REF_SOURCES = new Set([
"env",
"file",
"exec"
]);
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasSecretRefSource(value) {
return typeof value === "string" && SECRET_REF_SOURCES.has(value);
}
function hasNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
function isValidEnvSecretRefId(value) {
return ENV_SECRET_REF_ID_RE.test(value);
}
function isSecretRef(value) {
if (!isRecord(value)) return false;
if (Object.keys(value).length !== 3) return false;
return hasSecretRefSource(value.source) && hasNonEmptyString(value.provider) && hasNonEmptyString(value.id);
}
function isLegacySecretRefWithoutProvider(value) {
if (!isRecord(value)) return false;
return hasSecretRefSource(value.source) && hasNonEmptyString(value.id) && value.provider === void 0;
}
function parseEnvTemplateSecretRef(value, provider = DEFAULT_SECRET_PROVIDER_ALIAS) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
if (!match) return null;
return {
source: "env",
provider: provider.trim() || "default",
id: match[1]
};
}
function parseLegacySecretRefEnvMarker(value, provider = DEFAULT_SECRET_PROVIDER_ALIAS) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
const prefix = trimmed.startsWith("secretref-env:") ? LEGACY_SECRETREF_ENV_MARKER_PREFIX : trimmed.startsWith("__env__:") ? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX : void 0;
if (!prefix) return null;
const id = trimmed.slice(prefix.length);
if (!ENV_SECRET_REF_ID_RE.test(id)) return null;
return {
source: "env",
provider: provider.trim() || "default",
id
};
}
function coerceSecretRef(value, defaults) {
if (isSecretRef(value)) return value;
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
if (legacyEnvMarker) return legacyEnvMarker;
if (isLegacySecretRefWithoutProvider(value)) {
const provider = value.source === "env" ? defaults?.env ?? "default" : value.source === "file" ? defaults?.file ?? "default" : defaults?.exec ?? "default";
return {
source: value.source,
provider,
id: value.id
};
}
const envTemplate = parseEnvTemplateSecretRef(value, defaults?.env);
if (envTemplate) return envTemplate;
return null;
}
function resolveSecretInputRef(params) {
const explicitRef = coerceSecretRef(params.refValue, params.defaults);
const inlineRef = explicitRef ? null : coerceSecretRef(params.value, params.defaults);
return {
explicitRef,
inlineRef,
ref: explicitRef ?? inlineRef
};
}
//#endregion
export { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX, LEGACY_SECRETREF_ENV_MARKER_PREFIX, coerceSecretRef, isSecretRef, isValidEnvSecretRefId, parseEnvTemplateSecretRef, parseLegacySecretRefEnvMarker, resolveSecretInputRef };

View File

@@ -0,0 +1,31 @@
{
"name": "@openclaw/secrets-core",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./secret-input": {
"types": "./dist/secret-input.d.mts",
"import": "./dist/secret-input.mjs",
"default": "./dist/secret-input.mjs"
},
"./secret-ref": {
"types": "./dist/secret-ref.d.mts",
"import": "./dist/secret-ref.mjs",
"default": "./dist/secret-ref.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/secret-input.ts src/secret-ref.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View File

@@ -0,0 +1,2 @@
export * from "./secret-input.js";
export * from "./secret-ref.js";

View File

@@ -1,9 +1,43 @@
import { describe, expect, it } from "vitest";
import {
normalizeOptionalSecretInput,
normalizeResolvedSecretInputString,
normalizeSecretInput,
parseLegacySecretRefEnvMarker,
resolveSecretInputString,
} from "./types.secrets.js";
} from "./index.js";
describe("normalizeSecretInput", () => {
it("returns empty string for non-string values", () => {
expect(normalizeSecretInput(undefined)).toBe("");
expect(normalizeSecretInput(null)).toBe("");
expect(normalizeSecretInput(123)).toBe("");
expect(normalizeSecretInput({})).toBe("");
});
it("strips embedded line breaks and surrounding whitespace", () => {
expect(normalizeSecretInput(" sk-\r\nabc\n123 ")).toBe("sk-abc123");
});
it("drops non-Latin1 code points that can break HTTP ByteString headers", () => {
expect(normalizeSecretInput("key-\u0417\u2502-token")).toBe("key--token");
});
it("preserves Latin-1 characters and internal spaces", () => {
expect(normalizeSecretInput(" café token ")).toBe("café token");
});
});
describe("normalizeOptionalSecretInput", () => {
it("returns undefined when normalized value is empty", () => {
expect(normalizeOptionalSecretInput(" \r\n ")).toBeUndefined();
expect(normalizeOptionalSecretInput("\u0417\u2502")).toBeUndefined();
});
it("returns normalized value when non-empty", () => {
expect(normalizeOptionalSecretInput(" key-\u0417 ")).toBe("key-");
});
});
describe("resolveSecretInputString", () => {
it("returns available for non-empty string values", () => {

View File

@@ -0,0 +1,134 @@
import {
coerceSecretRef,
resolveSecretInputRef,
type SecretDefaults,
type SecretRef,
} from "./secret-ref.js";
export type SecretInputStringResolutionMode = "strict" | "inspect";
export type SecretInputStringResolution =
| { status: "available"; value: string; ref: null }
| { status: "configured_unavailable"; value: undefined; ref: SecretRef }
| { status: "missing"; value: undefined; ref: null };
/**
* Normalize copy/pasted credentials for HTTP/auth use.
*
* Line breaks embedded in API keys are common paste artifacts, and non-Latin1
* rich-text characters can crash header construction before auth fails.
* Preserve ordinary internal spaces for values such as `Bearer <token>`.
*/
export function normalizeSecretInput(value: unknown): string {
if (typeof value !== "string") {
return "";
}
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 0xff) {
latin1Only += char;
}
}
return latin1Only.trim();
}
export function normalizeOptionalSecretInput(value: unknown): string | undefined {
const normalized = normalizeSecretInput(value);
return normalized ? normalized : undefined;
}
export function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
export function hasConfiguredSecretInput(value: unknown, defaults?: SecretDefaults): boolean {
if (normalizeSecretInputString(value)) {
return true;
}
return coerceSecretRef(value, defaults) !== null;
}
function formatSecretRefLabel(ref: SecretRef): string {
return `${ref.source}:${ref.provider}:${ref.id}`;
}
function createUnresolvedSecretInputError(params: { path: string; ref: SecretRef }): Error {
return new Error(
`${params.path}: unresolved SecretRef "${formatSecretRefLabel(params.ref)}". Resolve this command against an active gateway runtime snapshot before reading it.`,
);
}
export function assertSecretInputResolved(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): void {
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (!ref) {
return;
}
throw createUnresolvedSecretInputError({ path: params.path, ref });
}
export function resolveSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
mode?: SecretInputStringResolutionMode;
}): SecretInputStringResolution {
const normalized = normalizeSecretInputString(params.value);
if (normalized) {
return {
status: "available",
value: normalized,
ref: null,
};
}
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (!ref) {
return {
status: "missing",
value: undefined,
ref: null,
};
}
if ((params.mode ?? "strict") === "strict") {
throw createUnresolvedSecretInputError({ path: params.path, ref });
}
return {
status: "configured_unavailable",
value: undefined,
ref,
};
}
export function normalizeResolvedSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): string | undefined {
const resolved = resolveSecretInputString({
...params,
mode: "strict",
});
if (resolved.status === "available") {
return resolved.value;
}
return undefined;
}

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parseEnvTemplateSecretRef } from "./types.secrets.js";
import { parseEnvTemplateSecretRef } from "./secret-ref.js";
describe("parseEnvTemplateSecretRef", () => {
it("parses ${VAR} template syntax", () => {

View File

@@ -0,0 +1,163 @@
export type SecretRefSource = "env" | "file" | "exec"; // pragma: allowlist secret
/**
* Stable identifier for a secret in a configured source.
* Examples:
* - env source: provider "default", id "OPENAI_API_KEY"
* - file source: provider "mounted-json", id "/providers/openai/apiKey"
* - exec source: provider "vault", id "openai/api-key"
*/
export type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
export type SecretInput = string | SecretRef;
export type SecretDefaults = {
env?: string;
file?: string;
exec?: string;
};
export const DEFAULT_SECRET_PROVIDER_ALIAS = "default"; // pragma: allowlist secret
export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
export const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:"; // pragma: allowlist secret
export const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:"; // pragma: allowlist secret
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
const SECRET_REF_SOURCES = new Set<SecretRefSource>(["env", "file", "exec"]);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasSecretRefSource(value: unknown): value is SecretRefSource {
return typeof value === "string" && SECRET_REF_SOURCES.has(value as SecretRefSource);
}
function hasNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function isValidEnvSecretRefId(value: string): boolean {
return ENV_SECRET_REF_ID_RE.test(value);
}
export function isSecretRef(value: unknown): value is SecretRef {
if (!isRecord(value)) {
return false;
}
if (Object.keys(value).length !== 3) {
return false;
}
return (
hasSecretRefSource(value.source) &&
hasNonEmptyString(value.provider) &&
hasNonEmptyString(value.id)
);
}
function isLegacySecretRefWithoutProvider(
value: unknown,
): value is { source: SecretRefSource; id: string } {
if (!isRecord(value)) {
return false;
}
return (
hasSecretRefSource(value.source) && hasNonEmptyString(value.id) && value.provider === undefined
);
}
export function parseEnvTemplateSecretRef(
value: unknown,
provider = DEFAULT_SECRET_PROVIDER_ALIAS,
): SecretRef | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
if (!match) {
return null;
}
return {
source: "env",
provider: provider.trim() || DEFAULT_SECRET_PROVIDER_ALIAS,
id: match[1],
};
}
export function parseLegacySecretRefEnvMarker(
value: unknown,
provider = DEFAULT_SECRET_PROVIDER_ALIAS,
): SecretRef | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const prefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
: undefined;
if (!prefix) {
return null;
}
const id = trimmed.slice(prefix.length);
if (!ENV_SECRET_REF_ID_RE.test(id)) {
return null;
}
return {
source: "env",
provider: provider.trim() || DEFAULT_SECRET_PROVIDER_ALIAS,
id,
};
}
export function coerceSecretRef(value: unknown, defaults?: SecretDefaults): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
if (legacyEnvMarker) {
return legacyEnvMarker;
}
if (isLegacySecretRefWithoutProvider(value)) {
const provider =
value.source === "env"
? (defaults?.env ?? DEFAULT_SECRET_PROVIDER_ALIAS)
: value.source === "file"
? (defaults?.file ?? DEFAULT_SECRET_PROVIDER_ALIAS)
: (defaults?.exec ?? DEFAULT_SECRET_PROVIDER_ALIAS);
return {
source: value.source,
provider,
id: value.id,
};
}
const envTemplate = parseEnvTemplateSecretRef(value, defaults?.env);
if (envTemplate) {
return envTemplate;
}
return null;
}
export function resolveSecretInputRef(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
}): {
explicitRef: SecretRef | null;
inlineRef: SecretRef | null;
ref: SecretRef | null;
} {
const explicitRef = coerceSecretRef(params.refValue, params.defaults);
const inlineRef = explicitRef ? null : coerceSecretRef(params.value, params.defaults);
return {
explicitRef,
inlineRef,
ref: explicitRef ?? inlineRef,
};
}

View File

@@ -1,2 +1,2 @@
import { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition } from "./provider-runtime-shared.mjs";
import { a as resolveWebProviderDefinition, i as resolveWebProviderConfig, n as providerRequiresCredential, r as readWebProviderEnvValue, t as hasWebProviderEntryCredential } from "./provider-runtime-shared-D4ll_JBj.mjs";
export { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -0,0 +1,175 @@
//#region packages/secrets-core/src/secret-ref.ts
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
const SECRET_REF_SOURCES = new Set([
"env",
"file",
"exec"
]);
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasSecretRefSource(value) {
return typeof value === "string" && SECRET_REF_SOURCES.has(value);
}
function hasNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
function isSecretRef(value) {
if (!isRecord(value)) return false;
if (Object.keys(value).length !== 3) return false;
return hasSecretRefSource(value.source) && hasNonEmptyString(value.provider) && hasNonEmptyString(value.id);
}
function isLegacySecretRefWithoutProvider(value) {
if (!isRecord(value)) return false;
return hasSecretRefSource(value.source) && hasNonEmptyString(value.id) && value.provider === void 0;
}
function parseEnvTemplateSecretRef(value, provider = DEFAULT_SECRET_PROVIDER_ALIAS) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
if (!match) return null;
return {
source: "env",
provider: provider.trim() || "default",
id: match[1]
};
}
function parseLegacySecretRefEnvMarker(value, provider = DEFAULT_SECRET_PROVIDER_ALIAS) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
const prefix = trimmed.startsWith("secretref-env:") ? LEGACY_SECRETREF_ENV_MARKER_PREFIX : trimmed.startsWith("__env__:") ? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX : void 0;
if (!prefix) return null;
const id = trimmed.slice(prefix.length);
if (!ENV_SECRET_REF_ID_RE.test(id)) return null;
return {
source: "env",
provider: provider.trim() || "default",
id
};
}
function coerceSecretRef(value, defaults) {
if (isSecretRef(value)) return value;
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
if (legacyEnvMarker) return legacyEnvMarker;
if (isLegacySecretRefWithoutProvider(value)) {
const provider = value.source === "env" ? defaults?.env ?? "default" : value.source === "file" ? defaults?.file ?? "default" : defaults?.exec ?? "default";
return {
source: value.source,
provider,
id: value.id
};
}
const envTemplate = parseEnvTemplateSecretRef(value, defaults?.env);
if (envTemplate) return envTemplate;
return null;
}
//#endregion
//#region packages/secrets-core/src/secret-input.ts
/**
* Normalize copy/pasted credentials for HTTP/auth use.
*
* Line breaks embedded in API keys are common paste artifacts, and non-Latin1
* rich-text characters can crash header construction before auth fails.
* Preserve ordinary internal spaces for values such as `Bearer <token>`.
*/
function normalizeSecretInput(value) {
if (typeof value !== "string") return "";
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 255) latin1Only += char;
}
return latin1Only.trim();
}
function normalizeSecretInputString(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
//#endregion
//#region packages/web-content-core/src/provider-runtime-shared.ts
function resolveWebProviderConfig(cfg, kind) {
const webConfig = cfg?.tools?.web;
if (!webConfig || typeof webConfig !== "object") return;
const toolConfig = webConfig[kind];
if (!toolConfig || typeof toolConfig !== "object") return;
return toolConfig;
}
function readWebProviderEnvValue(envVars, processEnv = process.env) {
for (const envVar of envVars) {
const value = normalizeSecretInput(processEnv[envVar]);
if (value) return value;
}
}
function providerRequiresCredential(provider) {
return provider.requiresCredential !== false;
}
function hasWebProviderEntryCredential(params) {
if (!providerRequiresCredential(params.provider)) return true;
const rawValue = params.resolveRawValue({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const configuredRef = coerceSecretRef(rawValue);
if (configuredRef && configuredRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(rawValue))) return true;
if (params.provider.authProviderId && params.resolveProviderAuthValue?.(params.provider.authProviderId)) return true;
if (params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : void 0
})) return true;
const fallbackRawValue = params.resolveFallbackRawValue?.({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const fallbackRef = coerceSecretRef(fallbackRawValue);
if (fallbackRef && fallbackRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(fallbackRawValue))) return true;
return Boolean(fallbackRef?.source === "env" ? params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: fallbackRef.id
}) : void 0);
}
function resolveWebProviderDefinition(params) {
if (!params.resolveEnabled({
toolConfig: params.toolConfig,
sandboxed: params.sandboxed
})) return null;
const providers = params.providers.filter(Boolean);
if (providers.length === 0) return null;
const autoProviderId = params.resolveAutoProviderId({
config: params.config,
toolConfig: params.toolConfig,
providers
});
const providerId = params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
if (!providerId) return null;
const provider = providers.find((entry) => entry.id === providerId) ?? providers.find((entry) => entry.id === params.resolveFallbackProviderId?.({
config: params.config,
toolConfig: params.toolConfig,
providers,
providerId
}));
if (!provider) return null;
const definition = params.createTool({
provider,
config: params.config,
toolConfig: params.toolConfig,
runtimeMetadata: params.runtimeMetadata
});
if (!definition) return null;
return {
provider,
definition
};
}
//#endregion
export { resolveWebProviderDefinition as a, resolveWebProviderConfig as i, providerRequiresCredential as n, readWebProviderEnvValue as r, hasWebProviderEntryCredential as t };

View File

@@ -1,136 +1,2 @@
//#region packages/web-content-core/src/provider-runtime-shared.ts
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeSecretInputString(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function normalizeSecretInput(value) {
if (typeof value !== "string") return "";
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 255) latin1Only += char;
}
return latin1Only.trim();
}
function isSecretRef(value) {
if (!isRecord(value)) return false;
if (Object.keys(value).length !== 3) return false;
return (value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.provider === "string" && value.provider.trim().length > 0 && typeof value.id === "string" && value.id.trim().length > 0;
}
function coerceSecretRef(value) {
if (isSecretRef(value)) return value;
if (typeof value === "string") {
const trimmed = value.trim();
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX) ? LEGACY_SECRETREF_ENV_MARKER_PREFIX : trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX) ? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX : void 0;
if (legacyPrefix) {
const id = trimmed.slice(legacyPrefix.length);
return ENV_SECRET_REF_ID_RE.test(id) ? {
source: "env",
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id
} : null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
return match ? {
source: "env",
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: match[1]
} : null;
}
if (isRecord(value) && (value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.id === "string" && value.id.trim().length > 0 && value.provider === void 0) return {
source: value.source,
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: value.id
};
return null;
}
function resolveWebProviderConfig(cfg, kind) {
const webConfig = cfg?.tools?.web;
if (!webConfig || typeof webConfig !== "object") return;
const toolConfig = webConfig[kind];
if (!toolConfig || typeof toolConfig !== "object") return;
return toolConfig;
}
function readWebProviderEnvValue(envVars, processEnv = process.env) {
for (const envVar of envVars) {
const value = normalizeSecretInput(processEnv[envVar]);
if (value) return value;
}
}
function providerRequiresCredential(provider) {
return provider.requiresCredential !== false;
}
function hasWebProviderEntryCredential(params) {
if (!providerRequiresCredential(params.provider)) return true;
const rawValue = params.resolveRawValue({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const configuredRef = coerceSecretRef(rawValue);
if (configuredRef && configuredRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(rawValue))) return true;
if (params.provider.authProviderId && params.resolveProviderAuthValue?.(params.provider.authProviderId)) return true;
if (params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : void 0
})) return true;
const fallbackRawValue = params.resolveFallbackRawValue?.({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const fallbackRef = coerceSecretRef(fallbackRawValue);
if (fallbackRef && fallbackRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(fallbackRawValue))) return true;
return Boolean(fallbackRef?.source === "env" ? params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: fallbackRef.id
}) : void 0);
}
function resolveWebProviderDefinition(params) {
if (!params.resolveEnabled({
toolConfig: params.toolConfig,
sandboxed: params.sandboxed
})) return null;
const providers = params.providers.filter(Boolean);
if (providers.length === 0) return null;
const autoProviderId = params.resolveAutoProviderId({
config: params.config,
toolConfig: params.toolConfig,
providers
});
const providerId = params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
if (!providerId) return null;
const provider = providers.find((entry) => entry.id === providerId) ?? providers.find((entry) => entry.id === params.resolveFallbackProviderId?.({
config: params.config,
toolConfig: params.toolConfig,
providers,
providerId
}));
if (!provider) return null;
const definition = params.createTool({
provider,
config: params.config,
toolConfig: params.toolConfig,
runtimeMetadata: params.runtimeMetadata
});
if (!definition) return null;
return {
provider,
definition
};
}
//#endregion
import { a as resolveWebProviderDefinition, i as resolveWebProviderConfig, n as providerRequiresCredential, r as readWebProviderEnvValue, t as hasWebProviderEntryCredential } from "./provider-runtime-shared-D4ll_JBj.mjs";
export { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -22,5 +22,8 @@
},
"scripts": {
"build": "tsdown src/index.ts src/provider-runtime-shared.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
},
"dependencies": {
"@openclaw/secrets-core": "workspace:*"
}
}

View File

@@ -1,3 +1,9 @@
import {
coerceSecretRef,
normalizeSecretInput,
normalizeSecretInputString,
} from "@openclaw/secrets-core";
export type WebProviderConfigSource = {
tools?: {
web?: {
@@ -7,21 +13,6 @@ export type WebProviderConfigSource = {
};
};
type SecretRefSource = "env" | "file" | "exec";
type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
type RuntimeWebProviderMetadata = {
providerConfigured?: string;
selectedProvider?: string;
@@ -35,85 +26,6 @@ type ProviderWithCredential = {
type WebContentProcessEnv = Record<string, string | undefined>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeSecretInput(value: unknown): string {
if (typeof value !== "string") {
return "";
}
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 0xff) {
latin1Only += char;
}
}
return latin1Only.trim();
}
function isSecretRef(value: unknown): value is SecretRef {
if (!isRecord(value)) {
return false;
}
if (Object.keys(value).length !== 3) {
return false;
}
return (
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.provider === "string" &&
value.provider.trim().length > 0 &&
typeof value.id === "string" &&
value.id.trim().length > 0
);
}
function coerceSecretRef(value: unknown): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
if (typeof value === "string") {
const trimmed = value.trim();
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
: undefined;
if (legacyPrefix) {
const id = trimmed.slice(legacyPrefix.length);
return ENV_SECRET_REF_ID_RE.test(id)
? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id }
: null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
return match ? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: match[1] } : null;
}
if (
isRecord(value) &&
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.id === "string" &&
value.id.trim().length > 0 &&
value.provider === undefined
) {
return {
source: value.source,
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: value.id,
};
}
return null;
}
export function resolveWebProviderConfig(
cfg: WebProviderConfigSource | undefined,
kind: "search" | "fetch",

10
pnpm-lock.yaml generated
View File

@@ -1838,6 +1838,8 @@ importers:
specifier: workspace:*
version: link:../gateway-client
packages/secrets-core: {}
packages/speech-core:
dependencies:
openclaw:
@@ -1853,10 +1855,14 @@ importers:
specifier: 5.6.2
version: 5.6.2
packages/web-content-core: {}
packages/tool-call-repair: {}
packages/web-content-core:
dependencies:
'@openclaw/secrets-core':
specifier: workspace:*
version: link:../secrets-core
ui:
dependencies:
'@create-markdown/preview':

View File

@@ -51,6 +51,7 @@ export const BUILD_ALL_STEPS = [
"packages/media-understanding-common/package.json",
"packages/terminal-core/package.json",
"packages/model-catalog-core/package.json",
"packages/secrets-core/package.json",
"packages/web-content-core/package.json",
"packages/memory-host-sdk/package.json",
"tsconfig.json",
@@ -59,6 +60,7 @@ export const BUILD_ALL_STEPS = [
"packages/llm-core/src",
"packages/markdown-core/src",
"packages/model-catalog-core/src",
"packages/secrets-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"packages/media-understanding-common/src",

View File

@@ -116,6 +116,14 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"@openclaw/media-generation-core/*": [
"../dist/plugin-sdk/packages/media-generation-core/src/*.d.ts",
],
"@openclaw/secrets-core": ["../dist/plugin-sdk/packages/secrets-core/src/index.d.ts"],
"@openclaw/secrets-core/secret-input": [
"../dist/plugin-sdk/packages/secrets-core/src/secret-input.d.ts",
],
"@openclaw/secrets-core/secret-ref": [
"../dist/plugin-sdk/packages/secrets-core/src/secret-ref.d.ts",
],
"@openclaw/secrets-core/*": ["../dist/plugin-sdk/packages/secrets-core/src/*.d.ts"],
"@openclaw/terminal-core": ["../dist/plugin-sdk/packages/terminal-core/src/index.d.ts"],
"@openclaw/terminal-core/ansi": ["../dist/plugin-sdk/packages/terminal-core/src/ansi.d.ts"],
"@openclaw/terminal-core/decorative-emoji": [

View File

@@ -9,6 +9,7 @@ const TSDOWN_PACKAGE_NAMES = [
"media-understanding-common",
"model-catalog-core",
"net-policy",
"secrets-core",
"speech-core",
"terminal-core",
];

View File

@@ -18,6 +18,7 @@ const PLUGIN_SDK_TYPE_INPUTS = [
"packages/llm-core/src",
"packages/markdown-core/src",
"packages/model-catalog-core/src",
"packages/secrets-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
@@ -76,6 +77,9 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
"dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
"dist/plugin-sdk/packages/secrets-core/src/index.d.ts",
"dist/plugin-sdk/packages/secrets-core/src/secret-input.d.ts",
"dist/plugin-sdk/packages/secrets-core/src/secret-ref.d.ts",
"dist/plugin-sdk/error-runtime.d.ts",
"dist/plugin-sdk/plugin-entry.d.ts",
"dist/plugin-sdk/provider-auth.d.ts",
@@ -105,6 +109,9 @@ const PACKAGE_DTS_REQUIRED_OUTPUTS = [
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-id.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
"packages/plugin-sdk/dist/packages/secrets-core/src/index.d.ts",
"packages/plugin-sdk/dist/packages/secrets-core/src/secret-input.d.ts",
"packages/plugin-sdk/dist/packages/secrets-core/src/secret-ref.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/ansi.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/decorative-emoji.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/health-style.d.ts",

View File

@@ -1,258 +1,28 @@
import { isRecord } from "../utils.js";
export type SecretRefSource = "env" | "file" | "exec"; // pragma: allowlist secret
/**
* Stable identifier for a secret in a configured source.
* Examples:
* - env source: provider "default", id "OPENAI_API_KEY"
* - file source: provider "mounted-json", id "/providers/openai/apiKey"
* - exec source: provider "vault", id "openai/api-key"
*/
export type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
export type SecretInput = string | SecretRef;
export const DEFAULT_SECRET_PROVIDER_ALIAS = "default"; // pragma: allowlist secret
export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
export const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:"; // pragma: allowlist secret
export const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:"; // pragma: allowlist secret
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
export type SecretInputStringResolutionMode = "strict" | "inspect";
export type SecretInputStringResolution =
| { status: "available"; value: string; ref: null }
| { status: "configured_unavailable"; value: undefined; ref: SecretRef }
| { status: "missing"; value: undefined; ref: null };
type SecretDefaults = {
env?: string;
file?: string;
exec?: string;
};
export function isValidEnvSecretRefId(value: string): boolean {
return ENV_SECRET_REF_ID_RE.test(value);
}
export function isSecretRef(value: unknown): value is SecretRef {
if (!isRecord(value)) {
return false;
}
if (Object.keys(value).length !== 3) {
return false;
}
return (
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.provider === "string" &&
value.provider.trim().length > 0 &&
typeof value.id === "string" &&
value.id.trim().length > 0
);
}
function isLegacySecretRefWithoutProvider(
value: unknown,
): value is { source: SecretRefSource; id: string } {
if (!isRecord(value)) {
return false;
}
return (
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.id === "string" &&
value.id.trim().length > 0 &&
value.provider === undefined
);
}
export function parseEnvTemplateSecretRef(
value: unknown,
provider = DEFAULT_SECRET_PROVIDER_ALIAS,
): SecretRef | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
if (!match) {
return null;
}
return {
source: "env",
provider: provider.trim() || DEFAULT_SECRET_PROVIDER_ALIAS,
id: match[1],
};
}
export function parseLegacySecretRefEnvMarker(
value: unknown,
provider = DEFAULT_SECRET_PROVIDER_ALIAS,
): SecretRef | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
const prefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
: undefined;
if (!prefix) {
return null;
}
const id = trimmed.slice(prefix.length);
if (!ENV_SECRET_REF_ID_RE.test(id)) {
return null;
}
return {
source: "env",
provider: provider.trim() || DEFAULT_SECRET_PROVIDER_ALIAS,
id,
};
}
export function coerceSecretRef(value: unknown, defaults?: SecretDefaults): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
if (legacyEnvMarker) {
return legacyEnvMarker;
}
if (isLegacySecretRefWithoutProvider(value)) {
const provider =
value.source === "env"
? (defaults?.env ?? DEFAULT_SECRET_PROVIDER_ALIAS)
: value.source === "file"
? (defaults?.file ?? DEFAULT_SECRET_PROVIDER_ALIAS)
: (defaults?.exec ?? DEFAULT_SECRET_PROVIDER_ALIAS);
return {
source: value.source,
provider,
id: value.id,
};
}
const envTemplate = parseEnvTemplateSecretRef(value, defaults?.env);
if (envTemplate) {
return envTemplate;
}
return null;
}
export function hasConfiguredSecretInput(value: unknown, defaults?: SecretDefaults): boolean {
if (normalizeSecretInputString(value)) {
return true;
}
return coerceSecretRef(value, defaults) !== null;
}
export function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function formatSecretRefLabel(ref: SecretRef): string {
return `${ref.source}:${ref.provider}:${ref.id}`;
}
function createUnresolvedSecretInputError(params: { path: string; ref: SecretRef }): Error {
return new Error(
`${params.path}: unresolved SecretRef "${formatSecretRefLabel(params.ref)}". Resolve this command against an active gateway runtime snapshot before reading it.`,
);
}
export function assertSecretInputResolved(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): void {
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (!ref) {
return;
}
throw createUnresolvedSecretInputError({ path: params.path, ref });
}
export function resolveSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
mode?: SecretInputStringResolutionMode;
}): SecretInputStringResolution {
const normalized = normalizeSecretInputString(params.value);
if (normalized) {
return {
status: "available",
value: normalized,
ref: null,
};
}
const { ref } = resolveSecretInputRef({
value: params.value,
refValue: params.refValue,
defaults: params.defaults,
});
if (!ref) {
return {
status: "missing",
value: undefined,
ref: null,
};
}
if ((params.mode ?? "strict") === "strict") {
throw createUnresolvedSecretInputError({ path: params.path, ref });
}
return {
status: "configured_unavailable",
value: undefined,
ref,
};
}
export function normalizeResolvedSecretInputString(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
path: string;
}): string | undefined {
const resolved = resolveSecretInputString({
...params,
mode: "strict",
});
if (resolved.status === "available") {
return resolved.value;
}
return undefined;
}
export function resolveSecretInputRef(params: {
value: unknown;
refValue?: unknown;
defaults?: SecretDefaults;
}): {
explicitRef: SecretRef | null;
inlineRef: SecretRef | null;
ref: SecretRef | null;
} {
const explicitRef = coerceSecretRef(params.refValue, params.defaults);
const inlineRef = explicitRef ? null : coerceSecretRef(params.value, params.defaults);
return {
explicitRef,
inlineRef,
ref: explicitRef ?? inlineRef,
};
}
export {
DEFAULT_SECRET_PROVIDER_ALIAS,
ENV_SECRET_REF_ID_RE,
LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX,
LEGACY_SECRETREF_ENV_MARKER_PREFIX,
assertSecretInputResolved,
coerceSecretRef,
hasConfiguredSecretInput,
isSecretRef,
isValidEnvSecretRefId,
normalizeResolvedSecretInputString,
normalizeSecretInput,
normalizeSecretInputString,
parseEnvTemplateSecretRef,
parseLegacySecretRefEnvMarker,
resolveSecretInputRef,
resolveSecretInputString,
} from "../../packages/secrets-core/src/index.js";
export type {
SecretInput,
SecretInputStringResolution,
SecretInputStringResolutionMode,
SecretRef,
SecretRefSource,
} from "../../packages/secrets-core/src/index.js";
export type EnvSecretProviderConfig = {
source: "env";

View File

@@ -197,6 +197,7 @@ describe("opt-in extension package boundaries", () => {
"../../packages/markdown-core/src/**/*.ts",
"../../packages/media-generation-core/src/**/*.ts",
"../../packages/model-catalog-core/src/**/*.ts",
"../../packages/secrets-core/src/**/*.ts",
"../../packages/terminal-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",

View File

@@ -1,35 +0,0 @@
import { describe, expect, it } from "vitest";
import { normalizeOptionalSecretInput, normalizeSecretInput } from "./normalize-secret-input.js";
describe("normalizeSecretInput", () => {
it("returns empty string for non-string values", () => {
expect(normalizeSecretInput(undefined)).toBe("");
expect(normalizeSecretInput(null)).toBe("");
expect(normalizeSecretInput(123)).toBe("");
expect(normalizeSecretInput({})).toBe("");
});
it("strips embedded line breaks and surrounding whitespace", () => {
expect(normalizeSecretInput(" sk-\r\nabc\n123 ")).toBe("sk-abc123");
});
it("drops non-Latin1 code points that can break HTTP ByteString headers", () => {
// U+0417 (Cyrillic З) and U+2502 (box drawing │) are > 255.
expect(normalizeSecretInput("key-\u0417\u2502-token")).toBe("key--token");
});
it("preserves Latin-1 characters and internal spaces", () => {
expect(normalizeSecretInput(" café token ")).toBe("café token");
});
});
describe("normalizeOptionalSecretInput", () => {
it("returns undefined when normalized value is empty", () => {
expect(normalizeOptionalSecretInput(" \r\n ")).toBeUndefined();
expect(normalizeOptionalSecretInput("\u0417\u2502")).toBeUndefined();
});
it("returns normalized value when non-empty", () => {
expect(normalizeOptionalSecretInput(" key-\u0417 ")).toBe("key-");
});
});

View File

@@ -1,34 +1,4 @@
/**
* Secret normalization for copy/pasted credentials.
*
* Common footgun: line breaks (especially `\r`) embedded in API keys/tokens.
* We strip line breaks anywhere, then trim whitespace at the ends.
*
* Another frequent source of runtime failures is rich-text/Unicode artifacts
* (smart punctuation, box-drawing chars, etc.) pasted into API keys. These can
* break HTTP header construction (`ByteString` violations). Drop non-Latin1
* code points so malformed keys fail as auth errors instead of crashing request
* setup.
*
* Intentionally does NOT remove ordinary spaces inside the string to avoid
* silently altering "Bearer <token>" style values.
*/
export function normalizeSecretInput(value: unknown): string {
if (typeof value !== "string") {
return "";
}
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 0xff) {
latin1Only += char;
}
}
return latin1Only.trim();
}
export function normalizeOptionalSecretInput(value: unknown): string | undefined {
const normalized = normalizeSecretInput(value);
return normalized ? normalized : undefined;
}
export {
normalizeOptionalSecretInput,
normalizeSecretInput,
} from "../../packages/secrets-core/src/secret-input.js";

View File

@@ -331,6 +331,18 @@ export const sharedVitestConfig = {
find: "@openclaw/net-policy",
replacement: path.join(repoRoot, "packages", "net-policy", "src", "index.ts"),
},
{
find: "@openclaw/secrets-core/secret-input",
replacement: path.join(repoRoot, "packages", "secrets-core", "src", "secret-input.ts"),
},
{
find: "@openclaw/secrets-core/secret-ref",
replacement: path.join(repoRoot, "packages", "secrets-core", "src", "secret-ref.ts"),
},
{
find: "@openclaw/secrets-core",
replacement: path.join(repoRoot, "packages", "secrets-core", "src", "index.ts"),
},
...sourcePluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),

View File

@@ -149,6 +149,10 @@
],
"@openclaw/net-policy/url-userinfo": ["./packages/net-policy/src/url-userinfo.ts"],
"@openclaw/net-policy/*": ["./packages/net-policy/src/*"],
"@openclaw/secrets-core": ["./packages/secrets-core/src/index.ts"],
"@openclaw/secrets-core/secret-input": ["./packages/secrets-core/src/secret-input.ts"],
"@openclaw/secrets-core/secret-ref": ["./packages/secrets-core/src/secret-ref.ts"],
"@openclaw/secrets-core/*": ["./packages/secrets-core/src/*"],
"@openclaw/web-content-core": ["./packages/web-content-core/src/index.ts"],
"@openclaw/web-content-core/provider-runtime-shared": [
"./packages/web-content-core/src/provider-runtime-shared.ts"

View File

@@ -18,6 +18,7 @@
"packages/media-generation-core/src/**/*.ts",
"packages/model-catalog-core/src/**/*.ts",
"packages/memory-host-sdk/src/**/*.ts",
"packages/secrets-core/src/**/*.ts",
"packages/terminal-core/src/**/*.ts",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",

View File

@@ -455,6 +455,14 @@ function buildWebContentCoreDistEntries(): Record<string, string> {
};
}
function buildSecretsCoreDistEntries(): Record<string, string> {
return {
index: "packages/secrets-core/src/index.ts",
"secret-input": "packages/secrets-core/src/secret-input.ts",
"secret-ref": "packages/secrets-core/src/secret-ref.ts",
};
}
function buildSpeechCoreDistEntries(): Record<string, string> {
return {
api: "packages/speech-core/api.ts",
@@ -654,6 +662,12 @@ export default defineConfig([
neverBundle: shouldExternalizeTerminalCoreDependency,
},
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
entry: buildSecretsCoreDistEntries(),
outDir: tsdownPackageOutputRoot("secrets-core"),
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,