@agent-native/core 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,7 @@ export declare function resolveKeyReferences(text: string, scope: SecretScope, s
53
53
  * 2. active org scope (Dispatch vault sync writes here for org workspaces)
54
54
  * 3. active org workspace scope (legacy shared rows)
55
55
  * 4. solo workspace scope when no org is active
56
+ * 5. legacy app credential store user/org scopes
56
57
  */
57
58
  export declare function resolveKeyReferencesWithRequestScopes(text: string, userScopeId: string): Promise<ResolveKeyReferencesResult>;
58
59
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"substitution.d.ts","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAoBjD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC,CA4CrC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qCAAqC,CACzD,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,0BAA0B,CAAC,CAmCrC;AAiCD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,GACzB,OAAO,CAeT;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAU1B;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAO1B"}
1
+ {"version":3,"file":"substitution.d.ts","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAqBjD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC,CA4CrC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qCAAqC,CACzD,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,0BAA0B,CAAC,CAmCrC;AAkED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,GACzB,OAAO,CAeT;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAU1B;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAO1B"}
@@ -25,6 +25,7 @@
25
25
  * org-admin write-gate is verified to be active.
26
26
  */
27
27
  import { readAppSecret, readAppSecretMeta } from "./storage.js";
28
+ import { resolveCredentialForScope } from "../credentials/index.js";
28
29
  import { getRequestOrgId, getRequestUserEmail, } from "../server/request-context.js";
29
30
  const KEY_REFERENCE_REGEX = /\$\{keys\.([A-Za-z0-9_-]+)\}/g;
30
31
  function isWorkspaceFallbackEnabled() {
@@ -94,6 +95,7 @@ export async function resolveKeyReferences(text, scope, scopeId) {
94
95
  * 2. active org scope (Dispatch vault sync writes here for org workspaces)
95
96
  * 3. active org workspace scope (legacy shared rows)
96
97
  * 4. solo workspace scope when no org is active
98
+ * 5. legacy app credential store user/org scopes
97
99
  */
98
100
  export async function resolveKeyReferencesWithRequestScopes(text, userScopeId) {
99
101
  const usedKeys = [];
@@ -134,8 +136,38 @@ async function readRequestScopedSecret(name, userScopeId) {
134
136
  if (result)
135
137
  return { value: result.value, ref: { name, ...ref } };
136
138
  }
139
+ const legacyCredential = await readLegacyCredential(name, userScopeId);
140
+ if (legacyCredential)
141
+ return legacyCredential;
137
142
  return null;
138
143
  }
144
+ async function readLegacyCredential(name, userScopeId) {
145
+ const orgId = getRequestOrgId();
146
+ const userValue = await resolveCredentialForScope(name, {
147
+ userEmail: userScopeId,
148
+ orgId,
149
+ scope: "user",
150
+ }).catch(() => undefined);
151
+ if (userValue) {
152
+ return {
153
+ value: userValue,
154
+ ref: { name, scope: "user", scopeId: userScopeId },
155
+ };
156
+ }
157
+ if (!orgId)
158
+ return null;
159
+ const orgValue = await resolveCredentialForScope(name, {
160
+ userEmail: userScopeId,
161
+ orgId,
162
+ scope: "org",
163
+ }).catch(() => undefined);
164
+ if (!orgValue)
165
+ return null;
166
+ return {
167
+ value: orgValue,
168
+ ref: { name, scope: "org", scopeId: orgId },
169
+ };
170
+ }
139
171
  function requestSecretCandidates(userScopeId) {
140
172
  const orgId = getRequestOrgId();
141
173
  if (orgId) {
@@ -1 +1 @@
1
- {"version":3,"file":"substitution.js","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side key substitution for automation tools.\n *\n * Resolves `${keys.NAME}` references in user-supplied strings (URLs, headers,\n * bodies, etc.) by looking up the named secret at tool-dispatch time. The\n * raw secret value NEVER enters the model's context — substitution happens\n * after the agent emits its tool call and before the request is dispatched.\n *\n * SECURITY — workspace-scope fallback (audit 05 H2):\n *\n * The user→workspace fallback is OPT-IN via the\n * `AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK=1` env flag. Default OFF.\n *\n * When a user (any org member) writes a workspace-scoped `OPENAI_API_KEY`,\n * a default-on fallback would let every other org member's tools that\n * reference `${keys.OPENAI_API_KEY}` start using the malicious key\n * (key-skimming, mirror requests, billing hijack). The previous\n * fix-wave gated workspace-scope WRITES behind an org-admin check; this\n * file is the read-side defense-in-depth.\n *\n * When the env flag is unset, `resolveKeyReferences(\"user\", scopeId)`\n * queries ONLY user-scope rows. Tools/automations that need shared\n * defaults must explicitly look up via `scope: \"workspace\"`. Most\n * installs benefit from the stricter default — opt in only after the\n * org-admin write-gate is verified to be active.\n */\n\nimport { readAppSecret, readAppSecretMeta } from \"./storage.js\";\nimport type { SecretScope } from \"./register.js\";\nimport {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\n\nconst KEY_REFERENCE_REGEX = /\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g;\n\nfunction isWorkspaceFallbackEnabled(): boolean {\n const v = process.env.AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK;\n if (!v) return false;\n const normalized = v.trim().toLowerCase();\n return (\n normalized === \"1\" ||\n normalized === \"true\" ||\n normalized === \"yes\" ||\n normalized === \"on\"\n );\n}\n\nexport interface ResolveKeyReferencesResult {\n resolved: string;\n usedKeys: string[];\n secretValues: string[];\n resolvedKeys?: ResolvedKeyReference[];\n}\n\nexport interface ResolvedKeyReference {\n name: string;\n scope: SecretScope;\n scopeId: string;\n}\n\n/**\n * Resolve `${keys.NAME}` references in `text`. For each reference, looks up\n * the named secret at the given scope, falling back to workspace-scope when\n * the user-scope row doesn't exist. Throws when a referenced key is missing\n * so the agent receives a clear error rather than dispatching with the\n * literal placeholder.\n */\nexport async function resolveKeyReferences(\n text: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [] };\n }\n\n const resolutions = new Map<string, string>();\n const secretValues: string[] = [];\n const workspaceFallbackEnabled = isWorkspaceFallbackEnabled();\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n let result = await readAppSecret({ key: name, scope, scopeId });\n // SECURITY (audit 05 H2): user→workspace fallback is opt-in. Default\n // off prevents one malicious org member from poisoning every other\n // member's `${keys.NAME}` resolution with a workspace-scoped value.\n if (!result && scope === \"user\" && workspaceFallbackEnabled) {\n result = await readAppSecret({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for scope \"${scope}\". Create it in Settings or via the secrets API before using this automation.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues };\n}\n\n/**\n * Resolve `${keys.NAME}` for browser extension fetches and other request-bound\n * code paths that should honor the active workspace's shared credential store.\n *\n * Lookup order:\n * 1. user scope for personal overrides\n * 2. active org scope (Dispatch vault sync writes here for org workspaces)\n * 3. active org workspace scope (legacy shared rows)\n * 4. solo workspace scope when no org is active\n */\nexport async function resolveKeyReferencesWithRequestScopes(\n text: string,\n userScopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [], resolvedKeys: [] };\n }\n\n const resolutions = new Map<string, string>();\n const resolvedKeys: ResolvedKeyReference[] = [];\n const secretValues: string[] = [];\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n const result = await readRequestScopedSecret(name, userScopeId);\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for this user or active workspace. Create it in Settings or the Dispatch vault before using this extension.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n resolvedKeys.push(result.ref);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues, resolvedKeys };\n}\n\nasync function readRequestScopedSecret(\n name: string,\n userScopeId: string,\n): Promise<{ value: string; ref: ResolvedKeyReference } | null> {\n const candidates = requestSecretCandidates(userScopeId);\n for (const ref of candidates) {\n const result = await readAppSecret({ key: name, ...ref });\n if (result) return { value: result.value, ref: { name, ...ref } };\n }\n return null;\n}\n\nfunction requestSecretCandidates(\n userScopeId: string,\n): Array<{ scope: SecretScope; scopeId: string }> {\n const orgId = getRequestOrgId();\n if (orgId) {\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"org\", scopeId: orgId },\n { scope: \"workspace\", scopeId: orgId },\n ];\n }\n\n const email = getRequestUserEmail() || userScopeId;\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"workspace\", scopeId: `solo:${email}` },\n ];\n}\n\n/**\n * Check if a URL is allowed by a key's URL allowlist. Returns true when no\n * allowlist is configured (permissive default — the allowlist is opt-in).\n *\n * Matching is exact on the URL's origin (scheme + host + port), so an entry\n * like `https://hooks.slack.com` blocks `https://evil.example.com` even if\n * the agent tries to redirect the request elsewhere.\n */\nexport function validateUrlAllowlist(\n url: string,\n allowlist: string[] | null,\n): boolean {\n if (!allowlist || allowlist.length === 0) return true;\n let origin: string;\n try {\n origin = new URL(url).origin;\n } catch {\n return false;\n }\n return allowlist.some((entry) => {\n try {\n return new URL(entry).origin === origin;\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Convenience helper: look up a key's allowlist by name+scope. Returns null\n * when the key doesn't exist or has no allowlist configured.\n *\n * SECURITY: workspace fallback obeys the same opt-in flag as\n * `resolveKeyReferences` so the allowlist check stays consistent with the\n * resolved secret. If a future caller queries the allowlist for a key the\n * resolver wouldn't return, we'd risk allowing requests that the resolver\n * would refuse — keep them aligned.\n */\nexport async function getKeyAllowlist(\n name: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<string[] | null> {\n let meta = await readAppSecretMeta({ key: name, scope, scopeId });\n if (!meta && scope === \"user\" && isWorkspaceFallbackEnabled()) {\n meta = await readAppSecretMeta({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n return meta?.urlAllowlist ?? null;\n}\n\nexport async function getResolvedKeyAllowlist(\n ref: ResolvedKeyReference,\n): Promise<string[] | null> {\n const meta = await readAppSecretMeta({\n key: ref.name,\n scope: ref.scope,\n scopeId: ref.scopeId,\n });\n return meta?.urlAllowlist ?? null;\n}\n\nfunction getWorkspaceScopeId(userScopeId: string): string {\n const orgId = getRequestOrgId();\n if (orgId) return orgId;\n const email = getRequestUserEmail() || userScopeId;\n return `solo:${email}`;\n}\n"]}
1
+ {"version":3,"file":"substitution.js","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side key substitution for automation tools.\n *\n * Resolves `${keys.NAME}` references in user-supplied strings (URLs, headers,\n * bodies, etc.) by looking up the named secret at tool-dispatch time. The\n * raw secret value NEVER enters the model's context — substitution happens\n * after the agent emits its tool call and before the request is dispatched.\n *\n * SECURITY — workspace-scope fallback (audit 05 H2):\n *\n * The user→workspace fallback is OPT-IN via the\n * `AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK=1` env flag. Default OFF.\n *\n * When a user (any org member) writes a workspace-scoped `OPENAI_API_KEY`,\n * a default-on fallback would let every other org member's tools that\n * reference `${keys.OPENAI_API_KEY}` start using the malicious key\n * (key-skimming, mirror requests, billing hijack). The previous\n * fix-wave gated workspace-scope WRITES behind an org-admin check; this\n * file is the read-side defense-in-depth.\n *\n * When the env flag is unset, `resolveKeyReferences(\"user\", scopeId)`\n * queries ONLY user-scope rows. Tools/automations that need shared\n * defaults must explicitly look up via `scope: \"workspace\"`. Most\n * installs benefit from the stricter default — opt in only after the\n * org-admin write-gate is verified to be active.\n */\n\nimport { readAppSecret, readAppSecretMeta } from \"./storage.js\";\nimport type { SecretScope } from \"./register.js\";\nimport { resolveCredentialForScope } from \"../credentials/index.js\";\nimport {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\n\nconst KEY_REFERENCE_REGEX = /\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g;\n\nfunction isWorkspaceFallbackEnabled(): boolean {\n const v = process.env.AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK;\n if (!v) return false;\n const normalized = v.trim().toLowerCase();\n return (\n normalized === \"1\" ||\n normalized === \"true\" ||\n normalized === \"yes\" ||\n normalized === \"on\"\n );\n}\n\nexport interface ResolveKeyReferencesResult {\n resolved: string;\n usedKeys: string[];\n secretValues: string[];\n resolvedKeys?: ResolvedKeyReference[];\n}\n\nexport interface ResolvedKeyReference {\n name: string;\n scope: SecretScope;\n scopeId: string;\n}\n\n/**\n * Resolve `${keys.NAME}` references in `text`. For each reference, looks up\n * the named secret at the given scope, falling back to workspace-scope when\n * the user-scope row doesn't exist. Throws when a referenced key is missing\n * so the agent receives a clear error rather than dispatching with the\n * literal placeholder.\n */\nexport async function resolveKeyReferences(\n text: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [] };\n }\n\n const resolutions = new Map<string, string>();\n const secretValues: string[] = [];\n const workspaceFallbackEnabled = isWorkspaceFallbackEnabled();\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n let result = await readAppSecret({ key: name, scope, scopeId });\n // SECURITY (audit 05 H2): user→workspace fallback is opt-in. Default\n // off prevents one malicious org member from poisoning every other\n // member's `${keys.NAME}` resolution with a workspace-scoped value.\n if (!result && scope === \"user\" && workspaceFallbackEnabled) {\n result = await readAppSecret({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for scope \"${scope}\". Create it in Settings or via the secrets API before using this automation.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues };\n}\n\n/**\n * Resolve `${keys.NAME}` for browser extension fetches and other request-bound\n * code paths that should honor the active workspace's shared credential store.\n *\n * Lookup order:\n * 1. user scope for personal overrides\n * 2. active org scope (Dispatch vault sync writes here for org workspaces)\n * 3. active org workspace scope (legacy shared rows)\n * 4. solo workspace scope when no org is active\n * 5. legacy app credential store user/org scopes\n */\nexport async function resolveKeyReferencesWithRequestScopes(\n text: string,\n userScopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [], resolvedKeys: [] };\n }\n\n const resolutions = new Map<string, string>();\n const resolvedKeys: ResolvedKeyReference[] = [];\n const secretValues: string[] = [];\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n const result = await readRequestScopedSecret(name, userScopeId);\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for this user or active workspace. Create it in Settings or the Dispatch vault before using this extension.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n resolvedKeys.push(result.ref);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues, resolvedKeys };\n}\n\nasync function readRequestScopedSecret(\n name: string,\n userScopeId: string,\n): Promise<{ value: string; ref: ResolvedKeyReference } | null> {\n const candidates = requestSecretCandidates(userScopeId);\n for (const ref of candidates) {\n const result = await readAppSecret({ key: name, ...ref });\n if (result) return { value: result.value, ref: { name, ...ref } };\n }\n const legacyCredential = await readLegacyCredential(name, userScopeId);\n if (legacyCredential) return legacyCredential;\n return null;\n}\n\nasync function readLegacyCredential(\n name: string,\n userScopeId: string,\n): Promise<{ value: string; ref: ResolvedKeyReference } | null> {\n const orgId = getRequestOrgId();\n const userValue = await resolveCredentialForScope(name, {\n userEmail: userScopeId,\n orgId,\n scope: \"user\",\n }).catch(() => undefined);\n if (userValue) {\n return {\n value: userValue,\n ref: { name, scope: \"user\", scopeId: userScopeId },\n };\n }\n\n if (!orgId) return null;\n const orgValue = await resolveCredentialForScope(name, {\n userEmail: userScopeId,\n orgId,\n scope: \"org\",\n }).catch(() => undefined);\n if (!orgValue) return null;\n\n return {\n value: orgValue,\n ref: { name, scope: \"org\", scopeId: orgId },\n };\n}\n\nfunction requestSecretCandidates(\n userScopeId: string,\n): Array<{ scope: SecretScope; scopeId: string }> {\n const orgId = getRequestOrgId();\n if (orgId) {\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"org\", scopeId: orgId },\n { scope: \"workspace\", scopeId: orgId },\n ];\n }\n\n const email = getRequestUserEmail() || userScopeId;\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"workspace\", scopeId: `solo:${email}` },\n ];\n}\n\n/**\n * Check if a URL is allowed by a key's URL allowlist. Returns true when no\n * allowlist is configured (permissive default — the allowlist is opt-in).\n *\n * Matching is exact on the URL's origin (scheme + host + port), so an entry\n * like `https://hooks.slack.com` blocks `https://evil.example.com` even if\n * the agent tries to redirect the request elsewhere.\n */\nexport function validateUrlAllowlist(\n url: string,\n allowlist: string[] | null,\n): boolean {\n if (!allowlist || allowlist.length === 0) return true;\n let origin: string;\n try {\n origin = new URL(url).origin;\n } catch {\n return false;\n }\n return allowlist.some((entry) => {\n try {\n return new URL(entry).origin === origin;\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Convenience helper: look up a key's allowlist by name+scope. Returns null\n * when the key doesn't exist or has no allowlist configured.\n *\n * SECURITY: workspace fallback obeys the same opt-in flag as\n * `resolveKeyReferences` so the allowlist check stays consistent with the\n * resolved secret. If a future caller queries the allowlist for a key the\n * resolver wouldn't return, we'd risk allowing requests that the resolver\n * would refuse — keep them aligned.\n */\nexport async function getKeyAllowlist(\n name: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<string[] | null> {\n let meta = await readAppSecretMeta({ key: name, scope, scopeId });\n if (!meta && scope === \"user\" && isWorkspaceFallbackEnabled()) {\n meta = await readAppSecretMeta({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n return meta?.urlAllowlist ?? null;\n}\n\nexport async function getResolvedKeyAllowlist(\n ref: ResolvedKeyReference,\n): Promise<string[] | null> {\n const meta = await readAppSecretMeta({\n key: ref.name,\n scope: ref.scope,\n scopeId: ref.scopeId,\n });\n return meta?.urlAllowlist ?? null;\n}\n\nfunction getWorkspaceScopeId(userScopeId: string): string {\n const orgId = getRequestOrgId();\n if (orgId) return orgId;\n const email = getRequestUserEmail() || userScopeId;\n return `solo:${email}`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.24.5",
3
+ "version": "0.24.6",
4
4
  "type": "module",
5
5
  "description": "Framework for agent-native application development — where AI agents and UI share state via files",
6
6
  "license": "MIT",