@jini-ai/core 0.4.8 → 0.6.10

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/README.md +5 -0
  3. package/dist/agent-tools.d.ts +1 -1
  4. package/dist/approval.d.ts +15 -0
  5. package/dist/approval.js +87 -22
  6. package/dist/batch-tool-inputs.d.ts +147 -0
  7. package/dist/batch-tool-inputs.js +257 -0
  8. package/dist/feature-flags.d.ts +21 -0
  9. package/dist/feature-flags.js +17 -0
  10. package/dist/fetch/index.d.ts +2 -1
  11. package/dist/fetch/index.js +1 -0
  12. package/dist/fetch/loader-read.d.ts +16 -0
  13. package/dist/fetch/loader-read.js +22 -0
  14. package/dist/fetch/types.d.ts +12 -2
  15. package/dist/gated-mutations/gateway.js +32 -27
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.js +7 -1
  18. package/dist/object-store.d.ts +115 -0
  19. package/dist/object-store.js +70 -0
  20. package/dist/origin-validation.js +64 -57
  21. package/dist/primitives/http.d.ts +2 -2
  22. package/dist/primitives/index.d.ts +1 -0
  23. package/dist/primitives/index.js +1 -0
  24. package/dist/primitives/paths.js +17 -10
  25. package/dist/primitives/tasks.d.ts +10 -0
  26. package/dist/primitives/tasks.js +8 -0
  27. package/dist/provider-catalog.d.ts +76 -0
  28. package/dist/provider-catalog.js +6 -0
  29. package/dist/provider-registry.d.ts +144 -0
  30. package/dist/provider-registry.js +138 -0
  31. package/dist/records.d.ts +24 -0
  32. package/dist/records.js +25 -0
  33. package/dist/redact.d.ts +32 -0
  34. package/dist/redact.js +91 -34
  35. package/dist/registration-kit.d.ts +7 -4
  36. package/dist/registration-kit.js +81 -62
  37. package/dist/settings/agent-value-write-tools.d.ts +19 -0
  38. package/dist/settings/agent-value-write-tools.js +131 -70
  39. package/dist/settings/ensure-definitions.d.ts +4 -2
  40. package/dist/settings/ensure-definitions.js +79 -63
  41. package/dist/settings/errors.d.ts +13 -0
  42. package/dist/settings/errors.js +15 -0
  43. package/dist/settings/express/cms-adapter.d.ts +2 -2
  44. package/dist/settings/express/cms-adapter.js +34 -22
  45. package/dist/settings/express/http-request.d.ts +33 -0
  46. package/dist/settings/express/http-request.js +49 -0
  47. package/dist/settings/express/http.d.ts +2 -2
  48. package/dist/settings/express/http.js +16 -39
  49. package/dist/settings/express/stream.d.ts +2 -2
  50. package/dist/settings/express/stream.js +49 -27
  51. package/dist/settings/index.d.ts +2 -2
  52. package/dist/settings/index.js +1 -1
  53. package/dist/settings/ports.d.ts +7 -1
  54. package/dist/settings/repo.memory.js +7 -4
  55. package/dist/settings/settings.js +54 -60
  56. package/dist/settings/tool-registrations.d.ts +2 -3
  57. package/dist/settings/tool-registrations.js +64 -39
  58. package/dist/settings/write-service.d.ts +42 -4
  59. package/dist/settings/write-service.js +72 -5
  60. package/dist/target-scope.d.ts +2 -0
  61. package/dist/target-scope.js +18 -2
  62. package/dist/text/escape.js +2 -2
  63. package/dist/text/sanitize-untrusted-text.d.ts +5 -0
  64. package/dist/text/sanitize-untrusted-text.js +55 -35
  65. package/dist/tool-exposure.d.ts +79 -0
  66. package/dist/tool-exposure.js +163 -0
  67. package/dist/tool-input-errors.d.ts +23 -0
  68. package/dist/tool-input-errors.js +24 -0
  69. package/dist/tool-input-normalization.d.ts +37 -0
  70. package/dist/tool-input-normalization.js +188 -0
  71. package/dist/tool-metadata-types.d.ts +26 -0
  72. package/dist/tool-metadata-types.js +2 -0
  73. package/dist/tool-metadata.d.ts +2 -23
  74. package/dist/tool-registry.d.ts +38 -22
  75. package/dist/tool-registry.js +35 -20
  76. package/package.json +20 -2
  77. package/plugin-directory.js +72 -33
@@ -26,18 +26,23 @@ function splitAllowedOriginsEnv(config, env) {
26
26
  .map((origin) => origin.trim())
27
27
  .filter(Boolean);
28
28
  }
29
- /** Normalizes one entry to a protocol://host origin; invalid or non-HTTP(S) entries are rejected. */
30
- function parseAllowedOrigin(entry) {
29
+ /** Parses `value` as a URL, or `undefined` when it is unparseable or not HTTP(S). Shared by
30
+ * allow-list parsing and the Origin check so both agree on what counts as a web origin. */
31
+ function parseHttpUrl(value) {
31
32
  let parsed;
32
33
  try {
33
- parsed = new URL(entry);
34
+ parsed = new URL(value);
34
35
  }
35
36
  catch {
36
37
  return undefined;
37
38
  }
38
39
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
39
40
  return undefined;
40
- return parsed.origin;
41
+ return parsed;
42
+ }
43
+ /** Normalizes one entry to a protocol://host origin; invalid or non-HTTP(S) entries are rejected. */
44
+ function parseAllowedOrigin(entry) {
45
+ return parseHttpUrl(entry)?.origin;
41
46
  }
42
47
  /**
43
48
  * Parses {@link OriginValidationEnvConfig.allowedOriginsEnvVar} into normalized origins.
@@ -93,6 +98,8 @@ export function allowedBrowserPorts({ config, port, env }) {
93
98
  ports.push(webPort);
94
99
  return ports;
95
100
  }
101
+ /** The loopback spellings a locally-served page's Host or Origin can carry. */
102
+ const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '[::1]'];
96
103
  function headerValue(value) {
97
104
  if (Array.isArray(value)) {
98
105
  const first = value[0];
@@ -113,20 +120,28 @@ export function parseHostHeader({ value }) {
113
120
  return null;
114
121
  }
115
122
  }
123
+ /** Parses an exact dotted-quad (four all-digit parts, each 0-255) into octets, else `undefined`.
124
+ * Shared by the private-range and IP-literal checks so both agree on what an IPv4 literal is.
125
+ * `/^\d+$/` admits only non-negative integers, so the octet range check needs only an upper bound. */
126
+ function parseDottedQuad(host) {
127
+ const parts = host.split('.');
128
+ if (parts.length !== 4)
129
+ return undefined;
130
+ if (!parts.every((part) => /^\d+$/.test(part)))
131
+ return undefined;
132
+ const octets = parts.map(Number);
133
+ return octets.every((n) => n <= 255) ? octets : undefined;
134
+ }
135
+ /** RFC 1918 (10/8, 172.16/12, 192.168/16) or link-local (169.254/16), from the first two octets. */
136
+ function isPrivateIpv4Prefix([a, b]) {
137
+ return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
138
+ }
116
139
  /** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
117
140
  * Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
118
141
  * for SSRF address classification; private addresses are allowed here for local development. */
119
142
  export function isPrivateIpv4({ hostname }) {
120
- const parts = String(hostname || '').split('.');
121
- if (parts.length !== 4)
122
- return false;
123
- if (!parts.every((part) => /^\d+$/.test(part)))
124
- return false;
125
- const octets = parts.map((part) => Number(part));
126
- if (!octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255))
127
- return false;
128
- const [a, b] = octets;
129
- return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
143
+ const octets = parseDottedQuad(String(hostname || ''));
144
+ return octets !== undefined && isPrivateIpv4Prefix(octets);
130
145
  }
131
146
  /** Whether `hostname` is a literal IPv4 address or bracketed IPv6 literal (as opposed to a DNS name). */
132
147
  export function isIpLiteralHostname({ hostname }) {
@@ -135,12 +150,7 @@ export function isIpLiteralHostname({ hostname }) {
135
150
  return false;
136
151
  if (host.startsWith('[') && host.endsWith(']'))
137
152
  return true;
138
- const parts = host.split('.');
139
- if (parts.length !== 4)
140
- return false;
141
- if (!parts.every((part) => /^\d+$/.test(part)))
142
- return false;
143
- return parts.map(Number).every((n) => Number.isInteger(n) && n >= 0 && n <= 255);
153
+ return parseDottedQuad(host) !== undefined;
144
154
  }
145
155
  /** Whether `hostname` is loopback, unspecified, or a private-LAN address.
146
156
  * Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
@@ -159,9 +169,8 @@ export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowed
159
169
  const requestHost = parseHostHeader({ value: hostHeader });
160
170
  if (!requestHost)
161
171
  return false;
162
- const loopbackHosts = ['127.0.0.1', 'localhost', '[::1]'];
163
172
  const explicitHosts = new Set([
164
- ...ports.flatMap((p) => [...loopbackHosts.map((h) => `${h}:${p}`), `${bindHost}:${p}`]),
173
+ ...ports.flatMap((p) => [...LOOPBACK_HOSTS.map((h) => `${h}:${p}`), `${bindHost}:${p}`]),
165
174
  ...configuredAllowedHosts({ origins: extraAllowedOrigins }),
166
175
  ]);
167
176
  if (explicitHosts.has(requestHost.host))
@@ -170,37 +179,51 @@ export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowed
170
179
  return false;
171
180
  return isLoopbackOrPrivateLanHost({ hostname: requestHost.hostname });
172
181
  }
182
+ /** Every `scheme://host:port` origin a locally-served page can have: loopback spellings and the bind host, over both schemes. */
183
+ function explicitLocalOrigins(ports, bindHost) {
184
+ const hosts = [...LOOPBACK_HOSTS, bindHost];
185
+ return new Set(ports.flatMap((p) => ['http', 'https'].flatMap((s) => hosts.map((h) => `${s}://${h}:${p}`))));
186
+ }
187
+ /** The URL's port, or its scheme's default when the URL omits it (only http/https reach here). */
188
+ function effectivePort(url) {
189
+ return url.port || (url.protocol === 'https:' ? '443' : '80');
190
+ }
173
191
  /** Whether an `Origin` header value names a locally-served or explicitly allow-listed origin. */
174
192
  export function isAllowedBrowserOrigin({ origin, hostHeader, ports, bindHost, extraAllowedOrigins }) {
175
- if (extraAllowedOrigins.includes(String(origin)))
193
+ const originText = String(origin);
194
+ if (extraAllowedOrigins.includes(originText))
176
195
  return true;
177
- let parsedOrigin;
178
- try {
179
- parsedOrigin = new URL(String(origin));
180
- }
181
- catch {
182
- return false;
183
- }
184
- if (parsedOrigin.protocol !== 'http:' && parsedOrigin.protocol !== 'https:')
196
+ const parsedOrigin = parseHttpUrl(originText);
197
+ if (!parsedOrigin)
185
198
  return false;
186
199
  const requestHost = parseHostHeader({ value: hostHeader });
187
200
  if (!requestHost)
188
201
  return false;
189
- const schemes = ['http', 'https'];
190
- const loopbackHosts = ['127.0.0.1', 'localhost', '[::1]'];
191
- const explicitOrigins = new Set(ports.flatMap((p) => [
192
- ...schemes.flatMap((s) => loopbackHosts.map((h) => `${s}://${h}:${p}`)),
193
- ...schemes.map((s) => `${s}://${bindHost}:${p}`),
194
- ]));
195
- if (explicitOrigins.has(String(origin)))
202
+ if (explicitLocalOrigins(ports, bindHost).has(originText))
196
203
  return true;
197
- const originPort = parsedOrigin.port || (parsedOrigin.protocol === 'https:' ? '443' : '80');
198
- if (!ports.map(String).includes(originPort))
204
+ if (!ports.map(String).includes(effectivePort(parsedOrigin)))
199
205
  return false;
200
206
  if (parsedOrigin.hostname !== requestHost.hostname)
201
207
  return false;
202
208
  return isLoopbackOrPrivateLanHost({ hostname: parsedOrigin.hostname });
203
209
  }
210
+ /** The decision for a request carrying no `Origin` header (as a same-origin GET subresource does). */
211
+ function isAllowedWithoutOrigin({ req, hostArgs, extraAllowedOrigins }) {
212
+ const ipOnlyExtraOrigins = extraAllowedOrigins.filter((o) => isIpLiteralHostname({ hostname: new URL(o).hostname }));
213
+ if (isAllowedBrowserHost({ ...hostArgs, extraAllowedOrigins: ipOnlyExtraOrigins }))
214
+ return true;
215
+ // Browsers (Firefox, Chrome) omit Origin on same-origin GET subresource
216
+ // requests per the Fetch spec, which makes hostname entries in the
217
+ // allow-list unreachable for legitimate same-origin GETs through a
218
+ // reverse proxy. Sec-Fetch-Site is set by the user agent and cannot be
219
+ // modified by JavaScript, so a value of "same-origin" attests that the
220
+ // request originated from the same origin as the target — a cross-site
221
+ // `<img>`/`<script>` exploit would carry "cross-site" instead. Only
222
+ // consult the broader allow-list once that signal is present.
223
+ if (headerValue(req.headers?.['sec-fetch-site']) !== 'same-origin')
224
+ return false;
225
+ return isAllowedBrowserHost({ ...hostArgs, extraAllowedOrigins });
226
+ }
204
227
  /**
205
228
  * The top-level same-origin gate: is `req` a request the daemon should treat
206
229
  * as coming from one of its own locally-served (or explicitly allow-listed)
@@ -212,24 +235,8 @@ export function isLocalSameOrigin({ config, req, port, env }, { logger } = {}) {
212
235
  const ports = allowedBrowserPorts({ config, port, env });
213
236
  const bindHost = env[config.bindHostEnvVar] || '127.0.0.1';
214
237
  const extraAllowedOrigins = configuredAllowedOrigins({ config, env }, { logger });
215
- const ipOnlyExtraOrigins = extraAllowedOrigins.filter((o) => isIpLiteralHostname({ hostname: new URL(o).hostname }));
216
- const localHostAllowed = isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins: ipOnlyExtraOrigins });
217
238
  if (origin == null || origin === '') {
218
- if (localHostAllowed)
219
- return true;
220
- // Browsers (Firefox, Chrome) omit Origin on same-origin GET subresource
221
- // requests per the Fetch spec, which makes hostname entries in the
222
- // allow-list unreachable for legitimate same-origin GETs through a
223
- // reverse proxy. Sec-Fetch-Site is set by the user agent and cannot be
224
- // modified by JavaScript, so a value of "same-origin" attests that the
225
- // request originated from the same origin as the target — a cross-site
226
- // `<img>`/`<script>` exploit would carry "cross-site" instead. Only
227
- // consult the broader allow-list once that signal is present.
228
- const fetchSite = headerValue(req.headers?.['sec-fetch-site']);
229
- if (fetchSite === 'same-origin') {
230
- return isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins });
231
- }
232
- return false;
239
+ return isAllowedWithoutOrigin({ req, hostArgs: { config, hostHeader: host, ports, bindHost }, extraAllowedOrigins });
233
240
  }
234
241
  // Reverse-proxy deployments terminate the browser connection at the proxy
235
242
  // and open a fresh upstream connection to the daemon. The Host header the
@@ -5,8 +5,8 @@ export type HttpRequest = {
5
5
  method: "POST" | "GET" | "HEAD" | "PUT" | "PATCH" | "DELETE";
6
6
  url: string;
7
7
  headers: Readonly<Record<string, string>>;
8
- /** Preserve the signed bytes: re-serialization between signing and sending changes the signature. */
9
- body?: string;
8
+ /** Preserve signed text and raw binary uploads without base64 expansion or UTF-8 corruption. */
9
+ body?: string | Uint8Array;
10
10
  /** Whole-operation deadline, including DNS, redirects and body reads. Off by default.
11
11
  * Retry/backoff belongs to the caller; its deadline can cover the whole retry operation. */
12
12
  totalDeadlineMs?: number;
@@ -7,4 +7,5 @@ export type { DecodedSseEvent, DecodedSseFrame, SseFramingOptions } from './sse.
7
7
  export * from './logger.js';
8
8
  export * from './result.js';
9
9
  export * from './paths.js';
10
+ export * from './tasks.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@ export { decodeSseFrames, decodeSseStream, parseSseRecord } from './sse.js';
6
6
  export * from './logger.js';
7
7
  export * from './result.js';
8
8
  export * from './paths.js';
9
+ export * from './tasks.js';
9
10
  //# sourceMappingURL=index.js.map
@@ -1,13 +1,11 @@
1
- /** Pure lexical normalization of an already absolute path; no cwd, filesystem or symlink access. */
2
- function normalizeAbsolutePath(value, separator) {
3
- const path = separator === '\\' ? value.replace(/\\/g, '/') : value;
4
- const drive = separator === '\\' ? /^[A-Za-z]:\//.exec(path)?.[0] : undefined;
5
- const unc = separator === '\\' ? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0] : undefined;
6
- const prefix = drive ?? unc ?? (path.startsWith('/') ? '/' : undefined);
7
- if (!prefix)
8
- throw new RangeError('pathContains requires resolved absolute paths');
1
+ /** Root of an already `/`-separated Windows path: a drive root (`C:/`) or a UNC share root. */
2
+ function windowsRoot(path) {
3
+ return /^[A-Za-z]:\//.exec(path)?.[0] ?? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0];
4
+ }
5
+ /** Lexically resolve `.` and `..` segments; `..` never climbs above the root. */
6
+ function resolveSegments(rest) {
9
7
  const segments = [];
10
- for (const segment of path.slice(prefix.length).split('/')) {
8
+ for (const segment of rest.split('/')) {
11
9
  if (!segment || segment === '.')
12
10
  continue;
13
11
  if (segment === '..')
@@ -15,7 +13,16 @@ function normalizeAbsolutePath(value, separator) {
15
13
  else
16
14
  segments.push(segment);
17
15
  }
18
- return prefix.replace(/\/+$/, '') + '/' + segments.join('/');
16
+ return segments;
17
+ }
18
+ /** Pure lexical normalization of an already absolute path; no cwd, filesystem or symlink access. */
19
+ function normalizeAbsolutePath(value, separator) {
20
+ const windows = separator === '\\';
21
+ const path = windows ? value.replace(/\\/g, '/') : value;
22
+ const prefix = (windows ? windowsRoot(path) : undefined) ?? (path.startsWith('/') ? '/' : undefined);
23
+ if (!prefix)
24
+ throw new RangeError('pathContains requires resolved absolute paths');
25
+ return prefix.replace(/\/+$/, '') + '/' + resolveSegments(path.slice(prefix.length)).join('/');
19
26
  }
20
27
  /**
21
28
  * Check lexical containment using whole directory segments, including equality.
@@ -0,0 +1,10 @@
1
+ /** Append work to a fulfilled serial tail and report failures without rejecting the next tail.
2
+ * The observer owns failure policy and must not throw. Callers retain the returned tail to drain.
3
+ * @complexity O(1) scheduling time and space per task, excluding task work.
4
+ */
5
+ export declare function appendSerialTask({ previous, task, onError }: {
6
+ previous: Promise<void>;
7
+ task: () => Promise<unknown>;
8
+ onError: (error: unknown) => void;
9
+ }, _optional?: Record<string, never>): Promise<void>;
10
+ //# sourceMappingURL=tasks.d.ts.map
@@ -0,0 +1,8 @@
1
+ /** Append work to a fulfilled serial tail and report failures without rejecting the next tail.
2
+ * The observer owns failure policy and must not throw. Callers retain the returned tail to drain.
3
+ * @complexity O(1) scheduling time and space per task, excluding task work.
4
+ */
5
+ export function appendSerialTask({ previous, task, onError }, _optional = {}) {
6
+ return previous.then(task).then(() => undefined, onError);
7
+ }
8
+ //# sourceMappingURL=tasks.js.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Neutral catalog contracts consumed by UI and implemented by host adapters.
3
+ * Vendor identities, aliases and endpoint defaults belong to integrations.
4
+ */
5
+ /** UI catalog identity policy. Operations are pure, synchronous and must agree on IDs.
6
+ * Normalization returns a new map, preserves values, and resolves alias collisions
7
+ * deterministically. The host keeps one policy for the lifetime of a mounted tab.
8
+ *
9
+ * @example
10
+ * MediaProvidersTab({ port, catalog, catalogPolicy: mediaProviderCatalogPolicy });
11
+ */
12
+ export interface MediaProviderCatalogPolicy {
13
+ /** Canonicalize catalog entries, pinned IDs, edits and clears; preserve unknown IDs. */
14
+ canonicalMediaProviderId(input: {
15
+ id: string;
16
+ }, options?: Record<string, never>): string;
17
+ /** Normalize initial state and daemon replies before reconciliation. Canonical entries win aliases. */
18
+ normalizeMediaProviderMap<T>(input: {
19
+ providers: Readonly<Record<string, T>>;
20
+ }, options?: Record<string, never>): Record<string, T>;
21
+ }
22
+ /**
23
+ * One selectable provider in the host-supplied catalog.
24
+ */
25
+ export interface MediaProviderOption {
26
+ id: string;
27
+ label: string;
28
+ /** Prefilled when the operator picks this provider and typed nothing. */
29
+ defaultBaseUrl?: string;
30
+ /** Model ids this provider advertises. A host that discovers models at
31
+ * runtime can leave this empty. */
32
+ models?: readonly string[];
33
+ }
34
+ /** Whether a preset appears in the protocol or gateway chip row. */
35
+ export type ProviderPresetKind = 'protocol' | 'gateway';
36
+ /** One host-supplied endpoint preset, parameterized by the consumer's wire protocols. */
37
+ export interface ProviderPreset<Protocol extends string = string> {
38
+ id: string;
39
+ title: string;
40
+ protocol: Protocol;
41
+ baseUrl: string;
42
+ /** Ranked suggestions shown in the model field. Never authoritative — a
43
+ * host with live model discovery should pass `ExecutionPort.listModels`. */
44
+ preferredModels: readonly string[];
45
+ kind?: ProviderPresetKind;
46
+ /** Fixed-origin gateways resolve their base URL themselves, so the Base URL
47
+ * field is hidden rather than shown-and-ignored (origin: `isFixedOriginGateway`). */
48
+ fixedOrigin?: boolean;
49
+ /** Some local/self-hosted endpoints need no bearer credential. Defaults to
50
+ * `true` — i.e. an API key is required unless a preset opts out. */
51
+ requiresApiKey?: boolean;
52
+ /** Optional "Get key" deep link rendered beside the API-key field. */
53
+ apiKeyConsoleUrl?: string;
54
+ /**
55
+ * The literal prefix that positively identifies a pasted key as belonging to THIS provider —
56
+ * `'AIza'` for Google, `'sk-ant-'` for Anthropic. Omit it for a vendor whose keys carry no
57
+ * distinguishing prefix (Azure OpenAI's are bare hex) rather than inventing one.
58
+ *
59
+ * Read in exactly one direction: to recognise a key as SOMEONE ELSE'S. It is deliberately NOT an
60
+ * allowlist, and no code may ask "does the selected preset's key match this?" as a validity test.
61
+ * That is the shape this field replaced, and it shipped a false positive within hours of landing:
62
+ * a real Google Gemini key in a newer shape than `/^AIza/` was reported to its owner as not
63
+ * looking like a Google key. Vendors add key formats without telling anyone, so a catalog can
64
+ * prove a string belongs to vendor X, but never that it belongs to nobody. See
65
+ * `apiKeyFormatWarning`.
66
+ *
67
+ * Data rather than an `if (providerId === …)` chain in the rule, so adding a provider stays a row
68
+ * in the preset catalog — including for a host that supplies its own `presets` and never touches
69
+ * this package.
70
+ */
71
+ apiKeyPrefix?: string;
72
+ /** The synthetic "Custom" preset. Exactly one may carry this flag; it is
73
+ * never treated as configured-by-preset and never hides the Base URL. */
74
+ custom?: boolean;
75
+ }
76
+ //# sourceMappingURL=provider-catalog.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Neutral catalog contracts consumed by UI and implemented by host adapters.
3
+ * Vendor identities, aliases and endpoint defaults belong to integrations.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=provider-catalog.js.map
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @file The one keyed provider registry every provider seam shares (FROZEN CONTRACT, 2026-10-21).
3
+ *
4
+ * Four registries with four shapes grew independently (media `VendorAdapterRegistry`, visitor-auth,
5
+ * `OAuthProviderRegistry`, commerce `createPaymentProviderRegistry`). This is the shape new seams use
6
+ * and the four migrate to; `docs/provider-registry.md` in this package records the contract and the
7
+ * migration path for each. Other builds depend on the exported names and semantics below: change
8
+ * them only additively.
9
+ *
10
+ * Error policy is split by origin. A core/built-in registration that is malformed or duplicated is a
11
+ * composition-root programming error, so it THROWS at boot (same class as commerce's registry). A
12
+ * plugin contribution is third-party input, so a broken or duplicate one is recorded in
13
+ * `refusals()` and never crashes boot (same policy as `createPluginContributionLoader`).
14
+ */
15
+ /** Provider ids share the plugin-tool name grammar (agent-plugins lifecycle/tools/manifest.ts). */
16
+ export declare const PROVIDER_ID_PATTERN: RegExp;
17
+ export type ProviderOrigin = 'core' | 'plugin';
18
+ /** One registered provider. Stored frozen; the `provider` value keeps its identity. */
19
+ export interface ProviderDefinition<T> {
20
+ readonly id: string;
21
+ readonly provider: T;
22
+ readonly label?: string;
23
+ }
24
+ export interface RegisteredProvider<T> extends ProviderDefinition<T> {
25
+ readonly origin: ProviderOrigin;
26
+ /** Set only for `origin: 'plugin'`. */
27
+ readonly pluginId?: string;
28
+ }
29
+ export type ProviderRefusalCode = 'invalid_id' | 'invalid_provider' | 'duplicate' | 'contract_mismatch' | 'displaced_by_core';
30
+ export interface ProviderRefusal {
31
+ readonly code: ProviderRefusalCode;
32
+ readonly pluginId: string;
33
+ readonly providerId: string;
34
+ readonly message: string;
35
+ }
36
+ export interface ProviderRegistry<T> {
37
+ readonly contract: string;
38
+ /** Core/built-in registration. @throws {ProviderRegistryError} on a bad id, invalid provider or duplicate. */
39
+ register(required: {
40
+ readonly definition: ProviderDefinition<T>;
41
+ }, _optional?: Record<string, never>): void;
42
+ /** Plugin registration. Never throws (a throwing validator or provider getter is an `invalid_provider`
43
+ * refusal); returns false and records a refusal instead. */
44
+ contribute(required: {
45
+ readonly definition: ProviderDefinition<T>;
46
+ readonly pluginId: string;
47
+ readonly contract?: string;
48
+ }, _optional?: Record<string, never>): boolean;
49
+ get(required: {
50
+ readonly id: string;
51
+ }, _optional?: Record<string, never>): RegisteredProvider<T> | undefined;
52
+ has(required: {
53
+ readonly id: string;
54
+ }, _optional?: Record<string, never>): boolean;
55
+ /** Frozen snapshot in registration order. */
56
+ list(_required?: Record<string, never>, _optional?: Record<string, never>): readonly RegisteredProvider<T>[];
57
+ /** The configured default provider, if one was named and is registered. */
58
+ getDefault(_required?: Record<string, never>, _optional?: Record<string, never>): RegisteredProvider<T> | undefined;
59
+ /** Frozen snapshot of every refused plugin contribution, in refusal order. */
60
+ refusals(_required?: Record<string, never>, _optional?: Record<string, never>): readonly ProviderRefusal[];
61
+ }
62
+ export declare class ProviderRegistryError extends Error {
63
+ readonly code: ProviderRefusalCode;
64
+ constructor(required: {
65
+ readonly code: ProviderRefusalCode;
66
+ readonly message: string;
67
+ });
68
+ }
69
+ /**
70
+ * Build an independent registry for one provider contract (e.g. `mail`, `dns`, `payments`).
71
+ * Core always wins an id: a core registration displaces an earlier plugin entry (recording a
72
+ * `displaced_by_core` refusal) so a plugin loaded first can never turn a later core registration
73
+ * into a boot crash.
74
+ * @param required.contract - Contract name used in messages and checked against contributions.
75
+ * @param optional.defaultProviderId - Validated eagerly; need not be registered yet.
76
+ * @param optional.validate - Contract-specific shape check; a string is the reason it is invalid.
77
+ * @param optional.seed - Core definitions registered in order (throwing policy).
78
+ * @throws {ProviderRegistryError} When the default id or a seed definition is invalid.
79
+ * @complexity register/contribute/get/has O(1); list/refusals O(n) snapshot.
80
+ */
81
+ export declare function createProviderRegistry<T>(required: {
82
+ readonly contract: string;
83
+ }, optional?: {
84
+ readonly defaultProviderId?: string;
85
+ readonly validate?: (required: {
86
+ readonly provider: T;
87
+ }) => string | undefined;
88
+ readonly seed?: readonly ProviderDefinition<T>[];
89
+ }): ProviderRegistry<T>;
90
+ export type ProviderMode = 'test' | 'live';
91
+ /** Who a provider call acts for. Credentials are pre-resolved by the host, never carried here. */
92
+ export interface ProviderContext {
93
+ readonly workspaceId: string;
94
+ readonly siteId?: string;
95
+ /** The connected account at the provider (e.g. a Stripe account or a mail domain id). */
96
+ readonly providerAccountId?: string;
97
+ readonly mode: ProviderMode;
98
+ }
99
+ /** The provider definitively refused: nothing happened, the caller may correct and retry. */
100
+ export type ProviderRejectedCode = 'invalid_request' | 'unauthorized' | 'forbidden' | 'not_found' | 'conflict' | 'rate_limited' | 'quota_exceeded' | 'unsupported' | 'not_configured' | 'provider_error';
101
+ /** The outcome is indeterminate (it may have happened): the caller must reconcile, not blindly retry. */
102
+ export type ProviderUnknownCode = 'timeout' | 'network' | 'provider_unavailable' | 'unexpected_response';
103
+ export interface ProviderError<C extends string> {
104
+ readonly code: C;
105
+ /** Caller-safe: no credentials, tokens, or raw provider payloads. */
106
+ readonly message: string;
107
+ readonly retryAfterMs?: number;
108
+ }
109
+ export type ProviderResult<T> = {
110
+ readonly status: 'ok';
111
+ readonly value: T;
112
+ } | {
113
+ readonly status: 'rejected';
114
+ readonly error: ProviderError<ProviderRejectedCode>;
115
+ } | {
116
+ readonly status: 'unknown';
117
+ readonly error: ProviderError<ProviderUnknownCode>;
118
+ };
119
+ export declare function providerOk<T>(required: {
120
+ readonly value: T;
121
+ }, _optional?: Record<string, never>): ProviderResult<T>;
122
+ export declare function providerRejected<T = never>(required: ProviderError<ProviderRejectedCode>, _optional?: Record<string, never>): ProviderResult<T>;
123
+ export declare function providerUnknown<T = never>(required: ProviderError<ProviderUnknownCode>, _optional?: Record<string, never>): ProviderResult<T>;
124
+ export type DomainPurpose = 'mail' | 'site' | 'tls';
125
+ export type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX' | 'CAA' | 'NS' | 'SRV';
126
+ export interface DomainRecordRequirement {
127
+ readonly type: DnsRecordType;
128
+ /** Fully-qualified record name, e.g. `send.example.com`. */
129
+ readonly name: string;
130
+ readonly value: string;
131
+ readonly ttl?: number;
132
+ /** MX / SRV priority. */
133
+ readonly priority?: number;
134
+ /** Operator-facing explanation, e.g. "DKIM signing key". */
135
+ readonly note?: string;
136
+ }
137
+ /** What one provider needs from a domain before it can serve `purpose` for it. */
138
+ export interface DomainRequirement {
139
+ readonly purpose: DomainPurpose;
140
+ readonly domain: string;
141
+ readonly providerId: string;
142
+ readonly records: readonly DomainRecordRequirement[];
143
+ }
144
+ //# sourceMappingURL=provider-registry.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @file The one keyed provider registry every provider seam shares (FROZEN CONTRACT, 2026-10-21).
3
+ *
4
+ * Four registries with four shapes grew independently (media `VendorAdapterRegistry`, visitor-auth,
5
+ * `OAuthProviderRegistry`, commerce `createPaymentProviderRegistry`). This is the shape new seams use
6
+ * and the four migrate to; `docs/provider-registry.md` in this package records the contract and the
7
+ * migration path for each. Other builds depend on the exported names and semantics below: change
8
+ * them only additively.
9
+ *
10
+ * Error policy is split by origin. A core/built-in registration that is malformed or duplicated is a
11
+ * composition-root programming error, so it THROWS at boot (same class as commerce's registry). A
12
+ * plugin contribution is third-party input, so a broken or duplicate one is recorded in
13
+ * `refusals()` and never crashes boot (same policy as `createPluginContributionLoader`).
14
+ */
15
+ /** Provider ids share the plugin-tool name grammar (agent-plugins lifecycle/tools/manifest.ts). */
16
+ export const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
17
+ export class ProviderRegistryError extends Error {
18
+ code;
19
+ constructor(required) {
20
+ super(required.message);
21
+ this.name = 'ProviderRegistryError';
22
+ this.code = required.code;
23
+ }
24
+ }
25
+ /** @returns The first validation fault of a definition, or undefined when it may be registered. */
26
+ function definitionFault(required, optional = {}) {
27
+ const { contract, definition } = required;
28
+ if (typeof definition.id !== 'string' || !PROVIDER_ID_PATTERN.test(definition.id)) {
29
+ return { code: 'invalid_id', message: `${contract} provider id '${String(definition.id)}' must match ${PROVIDER_ID_PATTERN.source}` };
30
+ }
31
+ const reason = optional.validate?.({ provider: definition.provider });
32
+ if (reason !== undefined)
33
+ return { code: 'invalid_provider', message: `${contract} provider '${definition.id}' is invalid: ${reason}` };
34
+ return undefined;
35
+ }
36
+ /** The id for a refusal line; a hostile definition whose id cannot even be read is recorded as ''. */
37
+ function safeId(required, _optional = {}) {
38
+ try {
39
+ return String(required.definition.id);
40
+ }
41
+ catch {
42
+ return '';
43
+ }
44
+ }
45
+ function frozenEntry(required, optional = {}) {
46
+ const { id, provider, label } = required.definition;
47
+ return Object.freeze({
48
+ id, provider, origin: required.origin,
49
+ ...(label === undefined ? {} : { label }),
50
+ ...(optional.pluginId === undefined ? {} : { pluginId: optional.pluginId }),
51
+ });
52
+ }
53
+ /**
54
+ * Build an independent registry for one provider contract (e.g. `mail`, `dns`, `payments`).
55
+ * Core always wins an id: a core registration displaces an earlier plugin entry (recording a
56
+ * `displaced_by_core` refusal) so a plugin loaded first can never turn a later core registration
57
+ * into a boot crash.
58
+ * @param required.contract - Contract name used in messages and checked against contributions.
59
+ * @param optional.defaultProviderId - Validated eagerly; need not be registered yet.
60
+ * @param optional.validate - Contract-specific shape check; a string is the reason it is invalid.
61
+ * @param optional.seed - Core definitions registered in order (throwing policy).
62
+ * @throws {ProviderRegistryError} When the default id or a seed definition is invalid.
63
+ * @complexity register/contribute/get/has O(1); list/refusals O(n) snapshot.
64
+ */
65
+ export function createProviderRegistry(required, optional = {}) {
66
+ const { contract } = required;
67
+ const { defaultProviderId, validate } = optional;
68
+ if (defaultProviderId !== undefined && !PROVIDER_ID_PATTERN.test(defaultProviderId)) {
69
+ throw new ProviderRegistryError({ code: 'invalid_id', message: `${contract} default provider id '${defaultProviderId}' must match ${PROVIDER_ID_PATTERN.source}` });
70
+ }
71
+ const providers = new Map();
72
+ const refused = [];
73
+ const refuse = (input) => {
74
+ refused.push(Object.freeze({ ...input.fault, pluginId: input.pluginId, providerId: input.providerId }));
75
+ return false;
76
+ };
77
+ const registry = {
78
+ contract,
79
+ register({ definition }) {
80
+ const fault = definitionFault({ contract, definition }, { ...(validate ? { validate } : {}) });
81
+ if (fault)
82
+ throw new ProviderRegistryError(fault);
83
+ const existing = providers.get(definition.id);
84
+ if (existing?.origin === 'core') {
85
+ throw new ProviderRegistryError({ code: 'duplicate', message: `${contract} provider '${definition.id}' is already registered` });
86
+ }
87
+ if (existing?.pluginId !== undefined) {
88
+ refuse({ fault: { code: 'displaced_by_core', message: `${contract} provider '${definition.id}' from plugin '${existing.pluginId}' was replaced by the built-in provider` }, pluginId: existing.pluginId, providerId: definition.id });
89
+ providers.delete(definition.id);
90
+ }
91
+ providers.set(definition.id, frozenEntry({ definition, origin: 'core' }));
92
+ },
93
+ contribute(input) {
94
+ // Third-party input: a throwing validator, id or provider getter is refused, never propagated
95
+ // (core registrations keep the throwing policy above).
96
+ try {
97
+ return contributeChecked(input);
98
+ }
99
+ catch {
100
+ return refuse({ fault: { code: 'invalid_provider', message: `${contract} provider from plugin '${input.pluginId}' was not loaded: its definition could not be validated` }, pluginId: input.pluginId, providerId: safeId({ definition: input.definition }) });
101
+ }
102
+ },
103
+ get({ id }) { return providers.get(id); },
104
+ has({ id }) { return providers.has(id); },
105
+ list() { return Object.freeze([...providers.values()]); },
106
+ getDefault() { return defaultProviderId === undefined ? undefined : providers.get(defaultProviderId); },
107
+ refusals() { return Object.freeze([...refused]); },
108
+ };
109
+ function contributeChecked({ definition, pluginId, contract: declared }) {
110
+ const providerId = String(definition.id);
111
+ if (declared !== undefined && declared !== contract) {
112
+ return refuse({ fault: { code: 'contract_mismatch', message: `plugin '${pluginId}' contributed provider '${providerId}' for contract '${declared}', not '${contract}'` }, pluginId, providerId });
113
+ }
114
+ const fault = definitionFault({ contract, definition }, { ...(validate ? { validate } : {}) });
115
+ if (fault)
116
+ return refuse({ fault, pluginId, providerId });
117
+ const existing = providers.get(providerId);
118
+ if (existing) {
119
+ const owner = existing.pluginId === undefined ? 'the built-in provider' : `plugin '${existing.pluginId}'`;
120
+ return refuse({ fault: { code: 'duplicate', message: `${contract} provider '${providerId}' from plugin '${pluginId}' was not loaded: already registered by ${owner}` }, pluginId, providerId });
121
+ }
122
+ providers.set(providerId, frozenEntry({ definition, origin: 'plugin' }, { pluginId }));
123
+ return true;
124
+ }
125
+ for (const definition of optional.seed ?? [])
126
+ registry.register({ definition });
127
+ return registry;
128
+ }
129
+ export function providerOk(required, _optional = {}) {
130
+ return { status: 'ok', value: required.value };
131
+ }
132
+ export function providerRejected(required, _optional = {}) {
133
+ return { status: 'rejected', error: { ...required } };
134
+ }
135
+ export function providerUnknown(required, _optional = {}) {
136
+ return { status: 'unknown', error: { ...required } };
137
+ }
138
+ //# sourceMappingURL=provider-registry.js.map