@jini-ai/platform 0.5.0 → 0.5.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @jini-ai/platform
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Tovu clean-up release (2026-10-08): code Tovu moved into Jini, plus the Jini clean-up (see the commit log since 0.5.1).
8
+
9
+ ## 0.5.1 — 2026-10-04
10
+
11
+ - `./secrets` exports the canonical site-key names (`SiteKeyHandle`, `FixedSiteKeyKeyring`, `UnusableSiteKeyError`, `SiteKeyFileAlreadyExistsError`, `parseSiteKeyHex`, `fingerprintSiteKeyHex`, `inspectSiteKeyMaterial`, `revealSiteKeyMaterial`, `generateFileSiteKey`, `deriveFromSiteKey`, `deriveSigningSecretFromSiteKey` and the matching types). Every `RootKey*` name stays exported as a `@deprecated` alias of the same binding, to be removed on or after 2026-11-01. Additive: no behavior or signature change.
12
+
3
13
  ## 0.5.0 — 2026-10-02
4
14
 
5
15
  ### BREAKING
@@ -1,5 +1,5 @@
1
1
  export declare function resolvePathWithin({ root, segment }: {
2
2
  root: string;
3
3
  segment: string;
4
- }): string | null;
4
+ }, _optional?: Record<string, never>): string | null;
5
5
  //# sourceMappingURL=strict-containment.d.ts.map
@@ -1,6 +1,6 @@
1
1
  /** Preserve strict lexical containment: relative roots, root equality and traversal are refused (except the filesystem-root edge case). Symlinks require a separate physical-path guard. */
2
2
  import path from "node:path";
3
- export function resolvePathWithin({ root, segment }) {
3
+ export function resolvePathWithin({ root, segment }, _optional = {}) {
4
4
  const resolved = path.resolve(root, segment);
5
5
  if (resolved !== path.join(root, segment))
6
6
  return null;
@@ -1,8 +1,20 @@
1
+ import { type ClientRequest, type IncomingMessage } from "node:http";
2
+ import { type RequestOptions } from "node:https";
1
3
  import type { HttpRequest, HttpResponse } from "@jini-ai/core/primitives";
2
4
  import type { PinnedPeer } from "./types.js";
3
5
  import type { HttpTransportAdapter } from "./ports.js";
6
+ /** Native request effect port; peer pinning and TLS options remain owned by this adapter. */
7
+ export interface NativeHttpTransportOptions {
8
+ /** Like node:http, invoke onResponse asynchronously, after returning the request handle. */
9
+ request?: (input: {
10
+ secure: boolean;
11
+ options: RequestOptions;
12
+ onResponse: (response: IncomingMessage) => void;
13
+ }, optional?: Record<string, never>) => ClientRequest;
14
+ }
4
15
  export declare class FetchHttpTransportAdapter implements HttpTransportAdapter {
5
- constructor(_required: Record<string, never>);
16
+ private readonly optional;
17
+ constructor(_required: Record<string, never>, optional?: NativeHttpTransportOptions);
6
18
  requestPinned({ request: req, peer }: {
7
19
  request: HttpRequest;
8
20
  peer: PinnedPeer;
@@ -11,7 +11,10 @@ import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib";
11
11
  /** Last-resort decoded-byte bound independent of any caller's response policy. */
12
12
  const ABSOLUTE_MAX_BYTES = 100 * 1024 * 1024;
13
13
  export class FetchHttpTransportAdapter {
14
- constructor(_required) { }
14
+ optional;
15
+ constructor(_required, optional = {}) {
16
+ this.optional = optional;
17
+ }
15
18
  async requestPinned({ request: req, peer }) {
16
19
  const url = new URL(req.url);
17
20
  const isHttps = url.protocol === "https:";
@@ -31,7 +34,7 @@ export class FetchHttpTransportAdapter {
31
34
  ...(isHttps && peer.tlsServerName ? { servername: peer.tlsServerName } : {}),
32
35
  };
33
36
  return new Promise((resolve, reject) => {
34
- const clientRequest = requestFn(options, (res) => {
37
+ const onResponse = (res) => {
35
38
  // HEAD can advertise GET's compression but has no body to decompress.
36
39
  const decoded = req.method === "HEAD" ? res : decodeBody(res.headers["content-encoding"], res);
37
40
  const chunks = [];
@@ -74,7 +77,10 @@ export class FetchHttpTransportAdapter {
74
77
  });
75
78
  decoded.on("end", () => finish(false));
76
79
  decoded.on("error", reject);
77
- });
80
+ };
81
+ const clientRequest = this.optional.request
82
+ ? this.optional.request({ secure: isHttps, options, onResponse }, {})
83
+ : requestFn(options, onResponse);
78
84
  clientRequest.on("timeout", () => {
79
85
  clientRequest.destroy(new Error(`request timed out after ${idleTimeoutMs}ms`));
80
86
  });
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - `fs` — filesystem containment, atomic copy, removal, log tails.
11
11
  * - `http` — HTTP readiness polling.
12
12
  * - `fetch-with-timeout` — the shared `AbortSignal`-timeout wrapper every outbound `fetch()` call
13
- * in this monorepo is meant to go through (2026-08-16 failure-mode audit, Finding 2).
13
+ * in this monorepo is meant to go through.
14
14
  * - `toolchain` — user-level toolchain bin discovery.
15
15
  * - `asset-cache` — SSRF-safe same-origin cache/proxy for sandboxed content's
16
16
  * external media references.
@@ -26,59 +26,10 @@
26
26
  * - `blob-storage` — a backend-agnostic blob storage port + local-disk and
27
27
  * S3-compatible implementations.
28
28
  *
29
- * The set of names exported here is intentionally identical to the pre-split
30
- * public surface; importers see no change.
31
29
 
32
- * Archived provenance rationale:
33
- * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
- *
35
- * The task brief asked to verify the recon's "leans OD" flag on
36
- * `project-storage.ts` firsthand and either drop it or extract a generic core
37
- * with OD's project-specific parts as an adapter, "depending on what you
38
- * actually find." Read in full: the `ProjectStorage` interface,
39
- * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
- * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
- * list/delete/stat under a scoping key) plus a local-disk and an
42
- * S3-compatible implementation, both traversal-guarded. The **only** OD
43
- * coupling in the file is the bottom `resolveProjectStorage()` factory
44
- * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
- * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
- * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
- *
48
- * **Extracted, not dropped:** the interface + both implementations, ported to
49
- * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
- * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
- * `projectId: string` parameter renamed to `namespace: string` — per
52
- * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
- * model; engine needs a generic workspace/session store interface") and §2.1's
54
- * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
- * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
- * the engine's — `namespace` carries no domain meaning here beyond "a
57
- * top-level grouping/scoping key," which is what the parameter actually is
58
- * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
- * it means a design project).
60
- *
61
- * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
- * wiring — a specific env-var-naming convention and the choice to read
63
- * `process.env` directly inside the engine layer — not generic engine
64
- * behavior. It has no equivalent anywhere else in this porting session's
65
- * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
- * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
- * `@jini/*`'s existing adapters read `process.env` inside their own
68
- * constructors). A Jini host application composes its own equivalent
69
- * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
- * credentials, ... })` called directly, with whatever env-var convention that
71
- * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
- * env-selection shape.
73
- *
74
- * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
- * blob-storage primitive with no SQL involvement, parallel to this package's
76
- * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
- * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
- * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
- * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
- * alongside it in the same package since it's `S3BlobStorage`'s only
81
- * dependency and has no other consumer yet.
30
+ * Blob storage uses opaque namespace scoping rather than a product's project model. Hosts supply
31
+ * configuration/credentials explicitly instead of inheriting product-prefixed environment names.
32
+ * Filesystem/network storage and its SigV4 signing belong here, with no kernel SQL-port dependency.
82
33
  */
83
34
  export type { CommandInvocation, CommandInvocationRequest } from "./command.js";
84
35
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * - `fs` — filesystem containment, atomic copy, removal, log tails.
11
11
  * - `http` — HTTP readiness polling.
12
12
  * - `fetch-with-timeout` — the shared `AbortSignal`-timeout wrapper every outbound `fetch()` call
13
- * in this monorepo is meant to go through (2026-08-16 failure-mode audit, Finding 2).
13
+ * in this monorepo is meant to go through.
14
14
  * - `toolchain` — user-level toolchain bin discovery.
15
15
  * - `asset-cache` — SSRF-safe same-origin cache/proxy for sandboxed content's
16
16
  * external media references.
@@ -26,59 +26,10 @@
26
26
  * - `blob-storage` — a backend-agnostic blob storage port + local-disk and
27
27
  * S3-compatible implementations.
28
28
  *
29
- * The set of names exported here is intentionally identical to the pre-split
30
- * public surface; importers see no change.
31
29
 
32
- * Archived provenance rationale:
33
- * ## Design decision: `project-storage.ts` was split, not ported wholesale or dropped
34
- *
35
- * The task brief asked to verify the recon's "leans OD" flag on
36
- * `project-storage.ts` firsthand and either drop it or extract a generic core
37
- * with OD's project-specific parts as an adapter, "depending on what you
38
- * actually find." Read in full: the `ProjectStorage` interface,
39
- * `LocalProjectStorage`, and `S3ProjectStorage` classes carry no OD nouns in
40
- * their own logic — they're a backend-agnostic blob CRUD port (read/write/
41
- * list/delete/stat under a scoping key) plus a local-disk and an
42
- * S3-compatible implementation, both traversal-guarded. The **only** OD
43
- * coupling in the file is the bottom `resolveProjectStorage()` factory
44
- * function, which reads `OD_PROJECT_STORAGE`/`OD_S3_BUCKET`/`OD_S3_REGION`/
45
- * `OD_S3_PREFIX`/`OD_S3_ENDPOINT`/`OD_S3_ACCESS_KEY_ID`/
46
- * `OD_S3_SECRET_ACCESS_KEY`/`OD_S3_SESSION_TOKEN` directly from `process.env`.
47
- *
48
- * **Extracted, not dropped:** the interface + both implementations, ported to
49
- * `src/blob-storage.ts` as `BlobStorage`/`BlobFileMeta`/`LocalBlobStorage`/
50
- * `S3BlobStorage`/`S3BlobStorageOptions`/`StorageError`, with every
51
- * `projectId: string` parameter renamed to `namespace: string` — per
52
- * extraction-plan.md's Task-4 Port-2 finding ("OD's 'project' is a product
53
- * model; engine needs a generic workspace/session store interface") and §2.1's
54
- * "Runs key on an opaque `contextRef`, never `projectId`" convention, "project"
55
- * specifically is flagged elsewhere in this porting effort as OD's noun, not
56
- * the engine's — `namespace` carries no domain meaning here beyond "a
57
- * top-level grouping/scoping key," which is what the parameter actually is
58
- * structurally (nothing in `LocalBlobStorage`/`S3BlobStorage`'s logic assumes
59
- * it means a design project).
60
- *
61
- * **Dropped, not ported:** `resolveProjectStorage()`. This is OD adapter
62
- * wiring — a specific env-var-naming convention and the choice to read
63
- * `process.env` directly inside the engine layer — not generic engine
64
- * behavior. It has no equivalent anywhere else in this porting session's
65
- * established pattern (`createSqliteEventLog`, `LocalBlobStorage`,
66
- * `S3BlobStorage` are all called with explicit constructor arguments; none of
67
- * `@jini/*`'s existing adapters read `process.env` inside their own
68
- * constructors). A Jini host application composes its own equivalent
69
- * (`new LocalBlobStorage(root)` or `new S3BlobStorage({ bucket, region,
70
- * credentials, ... })` called directly, with whatever env-var convention that
71
- * host prefers) rather than inheriting OD's `OD_S3_*` names or its
72
- * env-selection shape.
73
- *
74
- * Landed in `@jini/platform`, not `@jini/sqlite`: this is a filesystem/network
75
- * blob-storage primitive with no SQL involvement, parallel to this package's
76
- * existing `fs.ts` (path containment / atomic copy) and `http.ts` (readiness
77
- * polling) roles — `@jini/sqlite` is reserved for the `EventLog` port adapter
78
- * and `better-sqlite3`-specific helpers (`db-inspect.ts`,
79
- * `backend-config.ts`), none of which this needs. `aws-sigv4.ts` lives
80
- * alongside it in the same package since it's `S3BlobStorage`'s only
81
- * dependency and has no other consumer yet.
30
+ * Blob storage uses opaque namespace scoping rather than a product's project model. Hosts supply
31
+ * configuration/credentials explicitly instead of inheriting product-prefixed environment names.
32
+ * Filesystem/network storage and its SigV4 signing belong here, with no kernel SQL-port dependency.
82
33
  */
83
34
  export { createCommandInvocation, createPackageManagerInvocation } from "./command.js";
84
35
  export { mergeProxyAwareEnv, parseMacosScutilProxyOutput, parseWindowsInternetSettingsProxyOutput, resolveSystemProxyEnv, } from "./proxy-env.js";
@@ -23,14 +23,7 @@ export declare function expandIpv6({ address: addr }: {
23
23
  export declare function isPrivateAddress({ address: addr }: {
24
24
  address: string;
25
25
  }): boolean;
26
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
27
- export declare function isLoopbackApiHost({ hostname }: {
28
- hostname: string;
29
- }): boolean;
30
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
31
- export declare function isBlockedExternalApiHostname({ hostname }: {
32
- hostname: string;
33
- }): boolean;
26
+ export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
34
27
  /** True when `hostname` is `localhost`, an IPv6 loopback literal, or a `127.0.0.0/8` IPv4 literal. */
35
28
  export declare function isLoopbackHostname({ hostname }: {
36
29
  readonly hostname: unknown;
@@ -135,95 +135,8 @@ export function isPrivateAddress({ address: addr }) {
135
135
  }
136
136
  return true; // not a literal IP — caller must resolve before trusting it
137
137
  }
138
- function normalizeBracketedIpv6(hostname) {
139
- const stripped = hostname.startsWith('[') && hostname.endsWith(']')
140
- ? hostname.slice(1, -1)
141
- : hostname;
142
- // FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
143
- // form, so `localhost.` must normalize to `localhost` before the equality
144
- // check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
145
- // isBlockedIpv4 parses them. Strips one or more trailing dots.
146
- return stripped.toLowerCase().replace(/\.+$/, '');
147
- }
148
- function parseIpv4(hostname) {
149
- const parts = hostname.split('.');
150
- if (parts.length !== 4)
151
- return null;
152
- const parsed = parts.map((part) => {
153
- if (!/^\d{1,3}$/.test(part))
154
- return null;
155
- const value = Number(part);
156
- return value >= 0 && value <= 255 ? value : null;
157
- });
158
- if (parsed.some((part) => part === null))
159
- return null;
160
- return [parsed[0], parsed[1], parsed[2], parsed[3]];
161
- }
162
- function isLoopbackIpv4(hostname) {
163
- const parts = parseIpv4(hostname);
164
- return Boolean(parts && parts[0] === 127);
165
- }
166
- function isBlockedIpv4(hostname) {
167
- const parts = parseIpv4(hostname);
168
- if (!parts)
169
- return false;
170
- const [a, b] = parts;
171
- return (a === 0 ||
172
- (a === 100 && b >= 64 && b <= 127) ||
173
- (a === 169 && b === 254) ||
174
- a === 10 ||
175
- (a === 192 && b === 168) ||
176
- (a === 172 && b >= 16 && b <= 31) ||
177
- a >= 224);
178
- }
179
- function ipv4MappedToDotted(hostname) {
180
- const host = normalizeBracketedIpv6(hostname);
181
- const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
182
- if (!mapped)
183
- return null;
184
- if (parseIpv4(mapped.toLowerCase()))
185
- return mapped.toLowerCase();
186
- const hexParts = mapped.split(':');
187
- if (hexParts.length !== 2 ||
188
- !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
189
- return null;
190
- }
191
- // Non-null assertions, not a runtime guard: the length/regex checks above
192
- // already guarantee exactly two non-empty hex segments here.
193
- const hi = hexParts[0];
194
- const lo = hexParts[1];
195
- const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
196
- return [
197
- (value >>> 24) & 255,
198
- (value >>> 16) & 255,
199
- (value >>> 8) & 255,
200
- value & 255,
201
- ].join('.');
202
- }
203
- /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
204
- export function isLoopbackApiHost({ hostname }) {
205
- const host = normalizeBracketedIpv6(hostname);
206
- if (host === 'localhost' || host === '::1')
207
- return true;
208
- if (isLoopbackIpv4(host))
209
- return true;
210
- const mapped = ipv4MappedToDotted(host);
211
- return Boolean(mapped && isLoopbackIpv4(mapped));
212
- }
213
- /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
214
- export function isBlockedExternalApiHostname({ hostname }) {
215
- const host = normalizeBracketedIpv6(hostname);
216
- if (host === '::')
217
- return true;
218
- if (isBlockedIpv4(host))
219
- return true;
220
- if (/^f[cd][0-9a-f]{2}:/i.test(host))
221
- return true;
222
- if (/^fe[89ab][0-9a-f]:/i.test(host))
223
- return true;
224
- const mapped = ipv4MappedToDotted(host);
225
- return Boolean(mapped && isBlockedIpv4(mapped));
226
- }
138
+ // Preserve the Node entry's published names while sharing the literal-host owner with browser consumers.
139
+ export { isLoopbackApiHost, isBlockedExternalApiHostname } from './endpoint-policy.js';
227
140
  /** True when `hostname` is `localhost`, an IPv6 loopback literal, or a `127.0.0.0/8` IPv4 literal. */
228
141
  export function isLoopbackHostname({ hostname }, _optional = {}) {
229
142
  const normalized = String(hostname || '')
@@ -0,0 +1,9 @@
1
+ /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
2
+ export declare function isLoopbackApiHost({ hostname }: {
3
+ hostname: string;
4
+ }): boolean;
5
+ /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
6
+ export declare function isBlockedExternalApiHostname({ hostname }: {
7
+ hostname: string;
8
+ }): boolean;
9
+ //# sourceMappingURL=endpoint-policy.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Browser-safe literal endpoint hostname policy. No Node imports or DNS effects:
3
+ * operator-configured local model servers deliberately keep their loopback
4
+ * exception. Resolved-peer validation and strict IP parsing belong to the Node
5
+ * adapters, whose different trust boundaries must not be collapsed into this.
6
+ * SSRF peer classification is distinct from core's browser-development LAN origin allow-list.
7
+ */
8
+ function normalizeBracketedIpv6(hostname) {
9
+ const stripped = hostname.startsWith('[') && hostname.endsWith(']')
10
+ ? hostname.slice(1, -1)
11
+ : hostname;
12
+ // FQDN trailing-dot form (RFC 1034) resolves identically to the dotless
13
+ // form, so `localhost.` must normalize to `localhost` before the equality
14
+ // check below — and `0.0.0.0.`, `10.0.0.1.`, etc. must normalize before
15
+ // isBlockedIpv4 parses them. Strips one or more trailing dots.
16
+ return stripped.toLowerCase().replace(/\.+$/, '');
17
+ }
18
+ function parseIpv4(hostname) {
19
+ const parts = hostname.split('.');
20
+ if (parts.length !== 4)
21
+ return null;
22
+ const parsed = parts.map((part) => {
23
+ if (!/^\d{1,3}$/.test(part))
24
+ return null;
25
+ const value = Number(part);
26
+ return value >= 0 && value <= 255 ? value : null;
27
+ });
28
+ if (parsed.some((part) => part === null))
29
+ return null;
30
+ return [parsed[0], parsed[1], parsed[2], parsed[3]];
31
+ }
32
+ function isLoopbackIpv4(hostname) {
33
+ const parts = parseIpv4(hostname);
34
+ return Boolean(parts && parts[0] === 127);
35
+ }
36
+ function isBlockedIpv4(hostname) {
37
+ const parts = parseIpv4(hostname);
38
+ if (!parts)
39
+ return false;
40
+ const [a, b] = parts;
41
+ return (a === 0 ||
42
+ (a === 100 && b >= 64 && b <= 127) ||
43
+ (a === 169 && b === 254) ||
44
+ a === 10 ||
45
+ (a === 192 && b === 168) ||
46
+ (a === 172 && b >= 16 && b <= 31) ||
47
+ a >= 224);
48
+ }
49
+ /** `::ffff:10.0.0.1` and `::ffff:a00:1` denote the same address; return dotted form so the v4 rules apply. */
50
+ function ipv4MappedToDotted(hostname) {
51
+ const host = normalizeBracketedIpv6(hostname);
52
+ const mapped = /^::ffff:(.+)$/i.exec(host)?.[1];
53
+ if (!mapped)
54
+ return null;
55
+ if (parseIpv4(mapped.toLowerCase()))
56
+ return mapped.toLowerCase();
57
+ const hexParts = mapped.split(':');
58
+ if (hexParts.length !== 2 ||
59
+ !hexParts.every((part) => /^[0-9a-f]{1,4}$/i.test(part))) {
60
+ return null;
61
+ }
62
+ // Non-null assertions, not a runtime guard: the length/regex checks above
63
+ // already guarantee exactly two non-empty hex segments here.
64
+ const hi = hexParts[0];
65
+ const lo = hexParts[1];
66
+ const value = (Number.parseInt(hi, 16) << 16) | Number.parseInt(lo, 16);
67
+ return [
68
+ (value >>> 24) & 255,
69
+ (value >>> 16) & 255,
70
+ (value >>> 8) & 255,
71
+ value & 255,
72
+ ].join('.');
73
+ }
74
+ /** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
75
+ export function isLoopbackApiHost({ hostname }) {
76
+ const host = normalizeBracketedIpv6(hostname);
77
+ if (host === 'localhost' || host === '::1')
78
+ return true;
79
+ if (isLoopbackIpv4(host))
80
+ return true;
81
+ const mapped = ipv4MappedToDotted(host);
82
+ return Boolean(mapped && isLoopbackIpv4(mapped));
83
+ }
84
+ /** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
85
+ export function isBlockedExternalApiHostname({ hostname }) {
86
+ const host = normalizeBracketedIpv6(hostname);
87
+ if (host === '::')
88
+ return true;
89
+ if (isBlockedIpv4(host))
90
+ return true;
91
+ if (/^f[cd][0-9a-f]{2}:/i.test(host))
92
+ return true;
93
+ if (/^fe[89ab][0-9a-f]:/i.test(host))
94
+ return true;
95
+ const mapped = ipv4MappedToDotted(host);
96
+ return Boolean(mapped && isBlockedIpv4(mapped));
97
+ }
98
+ //# sourceMappingURL=endpoint-policy.js.map
@@ -1,36 +1,44 @@
1
1
  import type { VendorCredentialSetRecord, VendorCredentialSetRepoPort } from "./types.js";
2
- /** In-memory repository preserving label uniqueness and group default promotion. */
2
+ /**
3
+ * @file `VendorCredentialSetRepoPort`'s in-memory adapter — the ADR-006 rule-of-two test double,
4
+ * mirroring `../deployments/publish-credentials/repo.memory.ts`'s shape byte-for-byte with
5
+ * `providerId` swapped for `vendorId`. See that file's own header for the full reasoning (row
6
+ * keying, the enforced UNIQUE constraint matching better-sqlite3's own error wording, why no
7
+ * explicit lock is needed for the `isDefault` group invariant in a single-threaded JS process).
8
+ */
3
9
  export declare class InMemoryVendorCredentialSetRepo implements VendorCredentialSetRepoPort {
4
- constructor(_required: Record<string, never>);
10
+ constructor(_required: Record<string, never>, _optional?: Record<string, never>);
5
11
  private readonly rows;
6
12
  private static rowKey;
7
13
  private assertLabelAvailable;
8
14
  private clearOtherDefaults;
9
- insert(record: VendorCredentialSetRecord): Promise<void>;
10
- update(record: VendorCredentialSetRecord): Promise<void>;
15
+ insert(record: VendorCredentialSetRecord, _optional?: Record<string, never>): Promise<void>;
16
+ update(record: VendorCredentialSetRecord, _optional?: Record<string, never>): Promise<void>;
11
17
  findById(input: {
12
18
  workspaceId: string;
13
19
  id: string;
14
- }): Promise<VendorCredentialSetRecord | null>;
20
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord | null>;
15
21
  findDefaultByVendor(input: {
16
22
  workspaceId: string;
17
23
  vendorId: string;
18
- }): Promise<VendorCredentialSetRecord | null>;
24
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord | null>;
19
25
  listByVendor(input: {
20
26
  workspaceId: string;
21
27
  vendorId: string;
22
- }): Promise<VendorCredentialSetRecord[]>;
28
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord[]>;
23
29
  listByWorkspace(input: {
24
30
  workspaceId: string;
25
- }): Promise<VendorCredentialSetRecord[]>;
31
+ }, _optional?: Record<string, never>): Promise<VendorCredentialSetRecord[]>;
26
32
  delete(input: {
27
33
  workspaceId: string;
28
34
  id: string;
29
- }): Promise<void>;
35
+ }, _optional?: Record<string, never>): Promise<void>;
36
+ /** Mirrors the SQLite adapter's targeted single-column write — see
37
+ * `VendorCredentialSetRepoPort.updateAccountLabel`'s own doc. No-op if the row vanished. */
30
38
  updateAccountLabel(input: {
31
39
  workspaceId: string;
32
40
  id: string;
33
41
  accountLabel: string;
34
- }): Promise<void>;
42
+ }, _optional?: Record<string, never>): Promise<void>;
35
43
  }
36
44
  //# sourceMappingURL=repo.memory.d.ts.map
@@ -1,6 +1,12 @@
1
- /** In-memory repository preserving label uniqueness and group default promotion. */
1
+ /**
2
+ * @file `VendorCredentialSetRepoPort`'s in-memory adapter — the ADR-006 rule-of-two test double,
3
+ * mirroring `../deployments/publish-credentials/repo.memory.ts`'s shape byte-for-byte with
4
+ * `providerId` swapped for `vendorId`. See that file's own header for the full reasoning (row
5
+ * keying, the enforced UNIQUE constraint matching better-sqlite3's own error wording, why no
6
+ * explicit lock is needed for the `isDefault` group invariant in a single-threaded JS process).
7
+ */
2
8
  export class InMemoryVendorCredentialSetRepo {
3
- constructor(_required) { }
9
+ constructor(_required, _optional = {}) { }
4
10
  rows = new Map();
5
11
  static rowKey(workspaceId, id) {
6
12
  return `${workspaceId}::${id}`;
@@ -21,35 +27,35 @@ export class InMemoryVendorCredentialSetRepo {
21
27
  this.rows.set(key, { ...row, isDefault: false });
22
28
  }
23
29
  }
24
- async insert(record) {
30
+ async insert(record, _optional = {}) {
25
31
  this.assertLabelAvailable(record.workspaceId, record.vendorId, record.label);
26
32
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(record.workspaceId, record.id), { ...record });
27
33
  if (record.isDefault)
28
34
  this.clearOtherDefaults(record.workspaceId, record.vendorId, record.id);
29
35
  }
30
- async update(record) {
36
+ async update(record, _optional = {}) {
31
37
  this.assertLabelAvailable(record.workspaceId, record.vendorId, record.label, record.id);
32
38
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(record.workspaceId, record.id), { ...record });
33
39
  if (record.isDefault)
34
40
  this.clearOtherDefaults(record.workspaceId, record.vendorId, record.id);
35
41
  }
36
- async findById(input) {
42
+ async findById(input, _optional = {}) {
37
43
  return this.rows.get(InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id)) ?? null;
38
44
  }
39
- async findDefaultByVendor(input) {
45
+ async findDefaultByVendor(input, _optional = {}) {
40
46
  for (const row of this.rows.values()) {
41
47
  if (row.workspaceId === input.workspaceId && row.vendorId === input.vendorId && row.isDefault)
42
48
  return row;
43
49
  }
44
50
  return null;
45
51
  }
46
- async listByVendor(input) {
52
+ async listByVendor(input, _optional = {}) {
47
53
  return [...this.rows.values()].filter((row) => row.workspaceId === input.workspaceId && row.vendorId === input.vendorId);
48
54
  }
49
- async listByWorkspace(input) {
55
+ async listByWorkspace(input, _optional = {}) {
50
56
  return [...this.rows.values()].filter((row) => row.workspaceId === input.workspaceId);
51
57
  }
52
- async delete(input) {
58
+ async delete(input, _optional = {}) {
53
59
  const key = InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id);
54
60
  const removed = this.rows.get(key);
55
61
  this.rows.delete(key);
@@ -61,7 +67,9 @@ export class InMemoryVendorCredentialSetRepo {
61
67
  const promoted = remaining.reduce((latest, row) => (row.updatedAt > latest.updatedAt ? row : latest));
62
68
  this.rows.set(InMemoryVendorCredentialSetRepo.rowKey(promoted.workspaceId, promoted.id), { ...promoted, isDefault: true });
63
69
  }
64
- async updateAccountLabel(input) {
70
+ /** Mirrors the SQLite adapter's targeted single-column write — see
71
+ * `VendorCredentialSetRepoPort.updateAccountLabel`'s own doc. No-op if the row vanished. */
72
+ async updateAccountLabel(input, _optional = {}) {
65
73
  const key = InMemoryVendorCredentialSetRepo.rowKey(input.workspaceId, input.id);
66
74
  const existing = this.rows.get(key);
67
75
  if (!existing)
@@ -0,0 +1,64 @@
1
+ /** Shared credential validation; application detection policy is injected at the metadata boundary. */
2
+ export declare const CREDENTIAL_TOKEN_LIMIT = 8192;
3
+ export declare const CREDENTIAL_MESSAGES: {
4
+ readonly showSecret: "Show";
5
+ readonly hideSecret: "Hide";
6
+ readonly generationUnavailable: "Generation is not available yet for this provider.";
7
+ readonly secretValue: "Secret value";
8
+ readonly saveSecret: "Save secret";
9
+ readonly cancel: "Cancel";
10
+ readonly secretForm: "Enter the value in this secure form.";
11
+ readonly env: "Enter environment variables as NAME=VALUE lines or a JSON object of strings.";
12
+ readonly url: "The server URL must be a valid absolute URL.";
13
+ readonly set: "Access token set";
14
+ readonly storage: "The credential could not be saved or unlocked. Check the site credential store.";
15
+ readonly savedTitle: "Saved.";
16
+ readonly blank: "Enter a token. Spaces alone are not a token.";
17
+ readonly limit: "The token exceeds the 8192-character limit. Copy only the token.";
18
+ readonly ascii: "Hosted tokens must use visible ASCII characters (0x21–0x7E).";
19
+ readonly control: "The token contains an invalid control character.";
20
+ readonly plain: "Put credentials in the secret field, not in this field.";
21
+ readonly saved: "Saved, not tested.";
22
+ readonly connected: "Connected.";
23
+ readonly auth: "The server rejected this token.";
24
+ readonly timeout: "The connection timed out.";
25
+ readonly unreachable: "Could not reach the server.";
26
+ readonly chars: "{length} chars";
27
+ };
28
+ export type CredentialMessageId = keyof typeof CREDENTIAL_MESSAGES;
29
+ /** Errors carry only a rule and field, never the submitted value. */
30
+ export declare class CredentialInputError extends Error {
31
+ readonly rule: CredentialMessageId;
32
+ readonly field: string;
33
+ constructor(rule: CredentialMessageId, field: string);
34
+ }
35
+ /** Validate without normalizing: cards may call this before a probe; only the store trims. */
36
+ export declare function assertCredentialToken({ value, field, hosted }: {
37
+ value: unknown;
38
+ field?: string;
39
+ hosted?: boolean;
40
+ }, _optional?: {}): void;
41
+ /** The single normalization boundary. Interior characters are never rewritten or truncated. */
42
+ export declare function normalizeCredentialToken(required: {
43
+ value: unknown;
44
+ field?: string;
45
+ hosted?: boolean;
46
+ }, _optional?: {}): string;
47
+ export interface CredentialTokenHint {
48
+ length: number;
49
+ last4: string | null;
50
+ }
51
+ /** Call only on a server-side unsealed value; short tokens expose no characters. */
52
+ export declare function credentialTokenHint({ token }: {
53
+ token: string;
54
+ }, _optional?: {}): CredentialTokenHint;
55
+ /** Protect plaintext metadata, using Jini's detector plus structural URL/CLI rules. */
56
+ export declare function assertCredentialFreeField({ value, field, containsSecret }: {
57
+ value: string;
58
+ field: string;
59
+ containsSecret: (required: {
60
+ text: string;
61
+ }) => boolean;
62
+ }, _optional?: {}): void;
63
+ export type CredentialConnection = 'connected' | 'auth' | 'timeout' | 'unreachable' | 'saved';
64
+ //# sourceMappingURL=credential-token.d.ts.map
@@ -0,0 +1,75 @@
1
+ /** Shared credential validation; application detection policy is injected at the metadata boundary. */
2
+ export const CREDENTIAL_TOKEN_LIMIT = 8192;
3
+ export const CREDENTIAL_MESSAGES = {
4
+ showSecret: "Show",
5
+ hideSecret: "Hide",
6
+ generationUnavailable: "Generation is not available yet for this provider.",
7
+ secretValue: "Secret value",
8
+ saveSecret: "Save secret",
9
+ cancel: "Cancel",
10
+ secretForm: "Enter the value in this secure form.",
11
+ env: 'Enter environment variables as NAME=VALUE lines or a JSON object of strings.',
12
+ url: 'The server URL must be a valid absolute URL.',
13
+ set: 'Access token set',
14
+ storage: 'The credential could not be saved or unlocked. Check the site credential store.',
15
+ savedTitle: 'Saved.',
16
+ blank: 'Enter a token. Spaces alone are not a token.',
17
+ limit: 'The token exceeds the 8192-character limit. Copy only the token.',
18
+ ascii: 'Hosted tokens must use visible ASCII characters (0x21–0x7E).',
19
+ control: 'The token contains an invalid control character.',
20
+ plain: 'Put credentials in the secret field, not in this field.',
21
+ saved: 'Saved, not tested.',
22
+ connected: 'Connected.',
23
+ auth: 'The server rejected this token.',
24
+ timeout: 'The connection timed out.',
25
+ unreachable: 'Could not reach the server.',
26
+ chars: '{length} chars',
27
+ };
28
+ /** Errors carry only a rule and field, never the submitted value. */
29
+ export class CredentialInputError extends Error {
30
+ rule;
31
+ field;
32
+ constructor(rule, field) {
33
+ super(CREDENTIAL_MESSAGES[rule]);
34
+ this.rule = rule;
35
+ this.field = field;
36
+ this.name = 'CredentialInputError';
37
+ }
38
+ }
39
+ /** Validate without normalizing: cards may call this before a probe; only the store trims. */
40
+ export function assertCredentialToken({ value, field = 'token', hosted = true }, _optional = {}) {
41
+ if (typeof value !== 'string' || value.trim() === '')
42
+ throw new CredentialInputError('blank', field);
43
+ // Validate the stored length, so incidental edge whitespace does not consume the token limit.
44
+ const token = value.trim();
45
+ if (token.length > CREDENTIAL_TOKEN_LIMIT)
46
+ throw new CredentialInputError('limit', field);
47
+ if (hosted && /[^\x21-\x7e]/.test(token))
48
+ throw new CredentialInputError('ascii', field);
49
+ if (/[\x00-\x1f\x7f-\x9f]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(token)) {
50
+ throw new CredentialInputError('control', field);
51
+ }
52
+ }
53
+ /** The single normalization boundary. Interior characters are never rewritten or truncated. */
54
+ export function normalizeCredentialToken(required, _optional = {}) {
55
+ assertCredentialToken(required);
56
+ return required.value.trim();
57
+ }
58
+ /** Call only on a server-side unsealed value; short tokens expose no characters. */
59
+ export function credentialTokenHint({ token }, _optional = {}) {
60
+ return { length: token.length, last4: token.length >= 12 ? token.slice(-4) : null };
61
+ }
62
+ const SECRET_NAME = /(?:^|[_-])(?:key|api[_-]?key|token|access[_-]?token|secret|password|auth|sig)$/i;
63
+ const SECRET_ARG = /(?:^|\s)--[\w-]*(?:api[_-]?key|token|secret|password)(?:=|\s+)(?!\s*$)\S/i;
64
+ /** Protect plaintext metadata, using Jini's detector plus structural URL/CLI rules. */
65
+ export function assertCredentialFreeField({ value, field, containsSecret }, _optional = {}) {
66
+ let credential = containsSecret({ text: value }) || SECRET_ARG.test(value);
67
+ try {
68
+ const url = new URL(value);
69
+ credential ||= url.username !== '' || url.password !== '' || [...url.searchParams.keys()].some(key => SECRET_NAME.test(key));
70
+ }
71
+ catch { /* A label/argument is ordinarily not an absolute URL. */ }
72
+ if (credential)
73
+ throw new CredentialInputError('plain', field);
74
+ }
75
+ //# sourceMappingURL=credential-token.js.map
@@ -1,9 +1,64 @@
1
1
  export type { SealedSecret } from "./types.js";
2
- export type { RootKeyHandle, KeyringPort, SecretSealerPort } from "./ports.js";
2
+ export type { KeyringPort, SecretSealerPort } from "./ports.js";
3
3
  export { AesGcmSecretSealer } from "./secret-sealer.aesgcm.js";
4
- export { EnvOrFileKeyring, FixedRootKeyKeyring, UnusableRootKeyError, RootKeyFileAlreadyExistsError, parseRootKeyHex, fingerprintRootKeyHex, inspectRootKeyMaterial, revealRootKeyMaterial, generateFileRootKey, deriveFromRootKey, deriveSigningSecretFromRootKey, } from "./keyring.env.js";
5
- export type { EnvOrFileKeyringRequired, RootKeyEnvironmentPort, EnvOrFileKeyringOptions, RootKeyRejection, ParsedRootKeyHex, RootKeyStatus, RootKeyReveal, InspectRootKeyMaterialOptions, GeneratedFileRootKey, } from "./keyring.env.js";
4
+ export { EnvOrFileKeyring } from "./keyring.env.js";
5
+ export type { EnvOrFileKeyringRequired, EnvOrFileKeyringOptions } from "./keyring.env.js";
6
+ export type { RootKeyHandle as SiteKeyHandle } from "./ports.js";
7
+ export { FixedRootKeyKeyring as FixedSiteKeyKeyring } from "./keyring.env.js";
8
+ export { UnusableRootKeyError as UnusableSiteKeyError } from "./keyring.env.js";
9
+ export { RootKeyFileAlreadyExistsError as SiteKeyFileAlreadyExistsError } from "./keyring.env.js";
10
+ export { parseRootKeyHex as parseSiteKeyHex } from "./keyring.env.js";
11
+ export { fingerprintRootKeyHex as fingerprintSiteKeyHex } from "./keyring.env.js";
12
+ export { inspectRootKeyMaterial as inspectSiteKeyMaterial } from "./keyring.env.js";
13
+ export { revealRootKeyMaterial as revealSiteKeyMaterial } from "./keyring.env.js";
14
+ export { generateFileRootKey as generateFileSiteKey } from "./keyring.env.js";
15
+ export { deriveFromRootKey as deriveFromSiteKey } from "./keyring.env.js";
16
+ export { deriveSigningSecretFromRootKey as deriveSigningSecretFromSiteKey } from "./keyring.env.js";
17
+ export type { RootKeyEnvironmentPort as SiteKeyEnvironmentPort } from "./keyring.env.js";
18
+ export type { RootKeyRejection as SiteKeyRejection } from "./keyring.env.js";
19
+ export type { ParsedRootKeyHex as ParsedSiteKeyHex } from "./keyring.env.js";
20
+ export type { RootKeyStatus as SiteKeyStatus } from "./keyring.env.js";
21
+ export type { RootKeyReveal as SiteKeyReveal } from "./keyring.env.js";
22
+ export type { InspectRootKeyMaterialOptions as InspectSiteKeyMaterialOptions } from "./keyring.env.js";
23
+ export type { GeneratedFileRootKey as GeneratedFileSiteKey } from "./keyring.env.js";
24
+ /** @deprecated use SiteKeyHandle */
25
+ export type { RootKeyHandle } from "./ports.js";
26
+ /** @deprecated use FixedSiteKeyKeyring */
27
+ export { FixedRootKeyKeyring } from "./keyring.env.js";
28
+ /** @deprecated use UnusableSiteKeyError */
29
+ export { UnusableRootKeyError } from "./keyring.env.js";
30
+ /** @deprecated use SiteKeyFileAlreadyExistsError */
31
+ export { RootKeyFileAlreadyExistsError } from "./keyring.env.js";
32
+ /** @deprecated use parseSiteKeyHex */
33
+ export { parseRootKeyHex } from "./keyring.env.js";
34
+ /** @deprecated use fingerprintSiteKeyHex */
35
+ export { fingerprintRootKeyHex } from "./keyring.env.js";
36
+ /** @deprecated use inspectSiteKeyMaterial */
37
+ export { inspectRootKeyMaterial } from "./keyring.env.js";
38
+ /** @deprecated use revealSiteKeyMaterial */
39
+ export { revealRootKeyMaterial } from "./keyring.env.js";
40
+ /** @deprecated use generateFileSiteKey */
41
+ export { generateFileRootKey } from "./keyring.env.js";
42
+ /** @deprecated use deriveFromSiteKey */
43
+ export { deriveFromRootKey } from "./keyring.env.js";
44
+ /** @deprecated use deriveSigningSecretFromSiteKey */
45
+ export { deriveSigningSecretFromRootKey } from "./keyring.env.js";
46
+ /** @deprecated use SiteKeyEnvironmentPort */
47
+ export type { RootKeyEnvironmentPort } from "./keyring.env.js";
48
+ /** @deprecated use SiteKeyRejection */
49
+ export type { RootKeyRejection } from "./keyring.env.js";
50
+ /** @deprecated use ParsedSiteKeyHex */
51
+ export type { ParsedRootKeyHex } from "./keyring.env.js";
52
+ /** @deprecated use SiteKeyStatus */
53
+ export type { RootKeyStatus } from "./keyring.env.js";
54
+ /** @deprecated use SiteKeyReveal */
55
+ export type { RootKeyReveal } from "./keyring.env.js";
56
+ /** @deprecated use InspectSiteKeyMaterialOptions */
57
+ export type { InspectRootKeyMaterialOptions } from "./keyring.env.js";
58
+ /** @deprecated use GeneratedFileSiteKey */
59
+ export type { GeneratedFileRootKey } from "./keyring.env.js";
6
60
  export { formatAad } from "./aad.js";
7
61
  export { defaultPlatformMessages } from "../messages.js";
8
62
  export type { PlatformMessages } from "../messages.js";
63
+ export * from "./credential-token.js";
9
64
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,36 @@
1
1
  export { AesGcmSecretSealer } from "./secret-sealer.aesgcm.js";
2
- export { EnvOrFileKeyring, FixedRootKeyKeyring, UnusableRootKeyError, RootKeyFileAlreadyExistsError, parseRootKeyHex, fingerprintRootKeyHex, inspectRootKeyMaterial, revealRootKeyMaterial, generateFileRootKey, deriveFromRootKey, deriveSigningSecretFromRootKey, } from "./keyring.env.js";
2
+ export { EnvOrFileKeyring } from "./keyring.env.js";
3
+ export { FixedRootKeyKeyring as FixedSiteKeyKeyring } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
4
+ export { UnusableRootKeyError as UnusableSiteKeyError } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
5
+ export { RootKeyFileAlreadyExistsError as SiteKeyFileAlreadyExistsError } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
6
+ export { parseRootKeyHex as parseSiteKeyHex } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
7
+ export { fingerprintRootKeyHex as fingerprintSiteKeyHex } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
8
+ export { inspectRootKeyMaterial as inspectSiteKeyMaterial } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
9
+ export { revealRootKeyMaterial as revealSiteKeyMaterial } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
10
+ export { generateFileRootKey as generateFileSiteKey } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
11
+ export { deriveFromRootKey as deriveFromSiteKey } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
12
+ export { deriveSigningSecretFromRootKey as deriveSigningSecretFromSiteKey } from "./keyring.env.js"; // site-key-legacy: implementation binding; remove on/after 2026-11-01
13
+ /** @deprecated use FixedSiteKeyKeyring */
14
+ export { FixedRootKeyKeyring } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
15
+ /** @deprecated use UnusableSiteKeyError */
16
+ export { UnusableRootKeyError } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
17
+ /** @deprecated use SiteKeyFileAlreadyExistsError */
18
+ export { RootKeyFileAlreadyExistsError } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
19
+ /** @deprecated use parseSiteKeyHex */
20
+ export { parseRootKeyHex } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
21
+ /** @deprecated use fingerprintSiteKeyHex */
22
+ export { fingerprintRootKeyHex } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
23
+ /** @deprecated use inspectSiteKeyMaterial */
24
+ export { inspectRootKeyMaterial } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
25
+ /** @deprecated use revealSiteKeyMaterial */
26
+ export { revealRootKeyMaterial } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
27
+ /** @deprecated use generateFileSiteKey */
28
+ export { generateFileRootKey } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
29
+ /** @deprecated use deriveFromSiteKey */
30
+ export { deriveFromRootKey } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
31
+ /** @deprecated use deriveSigningSecretFromSiteKey */
32
+ export { deriveSigningSecretFromRootKey } from "./keyring.env.js"; // site-key-legacy: deprecated alias; remove on/after 2026-11-01
3
33
  export { formatAad } from "./aad.js";
4
34
  export { defaultPlatformMessages } from "../messages.js";
35
+ export * from "./credential-token.js";
5
36
  //# sourceMappingURL=index.js.map
@@ -2,8 +2,14 @@ import type { KeyringPort, RootKeyHandle, SecretSealerPort } from "./ports.js";
2
2
  import type { SealedSecret } from "./types.js";
3
3
  export declare class AesGcmSecretSealer implements SecretSealerPort {
4
4
  private readonly keyring;
5
+ private readonly randomBytesFn;
6
+ /** Inject randomness for wire fixtures; production uses Node's CSPRNG for every fresh IV. */
5
7
  constructor({ keyring }: {
6
8
  keyring: KeyringPort;
9
+ }, optional?: {
10
+ randomBytesFn?: (input: {
11
+ byteLength: number;
12
+ }) => Uint8Array;
7
13
  });
8
14
  /** Encrypts under the requested generation with a fresh IV; AAD must be reconstructed on open. */
9
15
  seal(input: {
@@ -14,13 +14,20 @@ export class AesGcmSecretSealer {
14
14
  // Root keys stay behind handles, and the purpose separates this HKDF domain from signing/tokens.
15
15
  // Exercise real GCM with an in-memory keyring: a plaintext fake would miss authentication failures.
16
16
  keyring;
17
- constructor({ keyring }) { this.keyring = keyring; }
17
+ randomBytesFn;
18
+ /** Inject randomness for wire fixtures; production uses Node's CSPRNG for every fresh IV. */
19
+ constructor({ keyring }, optional = {}) {
20
+ this.keyring = keyring;
21
+ this.randomBytesFn = optional.randomBytesFn ?? (({ byteLength }) => randomBytes(byteLength));
22
+ }
18
23
  /** Encrypts under the requested generation with a fresh IV; AAD must be reconstructed on open. */
19
24
  async seal(input) {
20
25
  if (typeof input.aad !== "string")
21
26
  throw new TypeError("AesGcmSecretSealer: aad must be a string");
22
27
  const aesKey = await this.deriveAesKey(input.key);
23
- const iv = randomBytes(IV_LENGTH_BYTES);
28
+ const iv = Buffer.from(this.randomBytesFn({ byteLength: IV_LENGTH_BYTES }));
29
+ if (iv.length !== IV_LENGTH_BYTES)
30
+ throw new TypeError("AesGcmSecretSealer: IV must contain exactly 12 bytes");
24
31
  // GCM must never reuse an IV under the same key. AAD is authenticated but not stored in the
25
32
  // envelope, so callers must reconstruct the byte-identical context when opening the record.
26
33
  const cipher = createCipheriv(ALG, aesKey, iv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/platform",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Generic OS/platform primitives: process exec and lifecycle, filesystem containment, HTTP readiness polling, toolchain discovery, blob storage, AWS signing, and an SSRF-safe asset cache.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -25,7 +25,9 @@
25
25
  "./fs/durable-json": "node",
26
26
  "./fs": "node",
27
27
  "./net": "node",
28
- "./fs/file-lock": "node"
28
+ "./net/endpoint-policy": "universal",
29
+ "./fs/file-lock": "node",
30
+ "./secrets/credential-token": "universal"
29
31
  }
30
32
  },
31
33
  "type": "module",
@@ -93,10 +95,20 @@
93
95
  "import": "./dist/net/index.js",
94
96
  "default": "./dist/net/index.js"
95
97
  },
98
+ "./net/endpoint-policy": {
99
+ "types": "./dist/net/endpoint-policy.d.ts",
100
+ "import": "./dist/net/endpoint-policy.js",
101
+ "default": "./dist/net/endpoint-policy.js"
102
+ },
96
103
  "./fs/file-lock": {
97
104
  "types": "./dist/fs/file-lock.d.ts",
98
105
  "import": "./dist/fs/file-lock.js",
99
106
  "default": "./dist/fs/file-lock.js"
107
+ },
108
+ "./secrets/credential-token": {
109
+ "types": "./dist/secrets/credential-token.d.ts",
110
+ "import": "./dist/secrets/credential-token.js",
111
+ "default": "./dist/secrets/credential-token.js"
100
112
  }
101
113
  },
102
114
  "files": [