@jini-ai/core 0.3.1 → 0.4.0

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 (97) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/LICENSE +201 -0
  3. package/README.md +154 -14
  4. package/dist/agent-tools.d.ts +29 -0
  5. package/dist/agent-tools.js +2 -0
  6. package/dist/api-token-auth.d.ts +12 -5
  7. package/dist/api-token-auth.js +7 -7
  8. package/dist/bindings.d.ts +16 -5
  9. package/dist/bindings.js +7 -6
  10. package/dist/composition.d.ts +41 -0
  11. package/dist/composition.js +39 -0
  12. package/dist/contribution-registry.d.ts +16 -0
  13. package/dist/contribution-registry.js +12 -0
  14. package/dist/daemon.d.ts +12 -7
  15. package/dist/daemon.js +21 -9
  16. package/dist/gated-mutations/actor-identity.d.ts +20 -0
  17. package/dist/gated-mutations/actor-identity.js +31 -0
  18. package/dist/gated-mutations/gateway.d.ts +115 -0
  19. package/dist/gated-mutations/gateway.js +123 -0
  20. package/dist/gated-mutations/ports.d.ts +27 -0
  21. package/dist/gated-mutations/ports.js +2 -0
  22. package/dist/gated-mutations/token.d.ts +95 -0
  23. package/dist/gated-mutations/token.js +87 -0
  24. package/dist/gated-mutations.d.ts +5 -0
  25. package/dist/gated-mutations.js +5 -0
  26. package/dist/index.d.ts +8 -2
  27. package/dist/index.js +5 -0
  28. package/dist/model-facing-tool-errors.d.ts +37 -0
  29. package/dist/model-facing-tool-errors.js +71 -0
  30. package/dist/naming.d.ts +31 -0
  31. package/dist/naming.js +33 -0
  32. package/dist/origin-validation.d.ts +64 -19
  33. package/dist/origin-validation.js +43 -33
  34. package/dist/pack-lifecycle.d.ts +19 -12
  35. package/dist/pack-lifecycle.js +14 -14
  36. package/dist/pack.d.ts +41 -13
  37. package/dist/pack.js +2 -2
  38. package/dist/primitives/http.d.ts +57 -0
  39. package/dist/primitives/http.js +2 -0
  40. package/dist/primitives/ids.d.ts +8 -0
  41. package/dist/primitives/ids.js +5 -0
  42. package/dist/primitives/index.d.ts +8 -0
  43. package/dist/primitives/index.js +8 -0
  44. package/dist/primitives/json.d.ts +10 -0
  45. package/dist/primitives/json.js +2 -0
  46. package/dist/primitives/logger.d.ts +26 -0
  47. package/dist/primitives/logger.js +17 -0
  48. package/dist/primitives/paths.d.ts +16 -0
  49. package/dist/primitives/paths.js +38 -0
  50. package/dist/primitives/result.d.ts +9 -0
  51. package/dist/primitives/result.js +2 -0
  52. package/dist/primitives/time.d.ts +17 -0
  53. package/dist/primitives/time.js +13 -0
  54. package/dist/redact.d.ts +43 -8
  55. package/dist/redact.js +121 -50
  56. package/dist/registration-kit.d.ts +323 -0
  57. package/dist/registration-kit.js +396 -0
  58. package/dist/text/index.d.ts +3 -0
  59. package/dist/text/index.js +2 -0
  60. package/dist/text/sanitize-untrusted-text.d.ts +22 -0
  61. package/dist/text/sanitize-untrusted-text.js +108 -0
  62. package/dist/token.d.ts +29 -2
  63. package/dist/token.js +22 -2
  64. package/dist/tool-registry.d.ts +32 -16
  65. package/dist/tool-registry.js +11 -11
  66. package/dist/tool-tokens.js +1 -1
  67. package/package.json +55 -11
  68. package/dist/api-token-auth.d.ts.map +0 -1
  69. package/dist/api-token-auth.js.map +0 -1
  70. package/dist/bindings.d.ts.map +0 -1
  71. package/dist/bindings.js.map +0 -1
  72. package/dist/compose.typecheck.d.ts +0 -2
  73. package/dist/compose.typecheck.d.ts.map +0 -1
  74. package/dist/compose.typecheck.js +0 -29
  75. package/dist/compose.typecheck.js.map +0 -1
  76. package/dist/daemon.d.ts.map +0 -1
  77. package/dist/daemon.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/internal.d.ts.map +0 -1
  81. package/dist/internal.js.map +0 -1
  82. package/dist/origin-validation.d.ts.map +0 -1
  83. package/dist/origin-validation.js.map +0 -1
  84. package/dist/pack-lifecycle.d.ts.map +0 -1
  85. package/dist/pack-lifecycle.js.map +0 -1
  86. package/dist/pack.d.ts.map +0 -1
  87. package/dist/pack.js.map +0 -1
  88. package/dist/principal.d.ts.map +0 -1
  89. package/dist/principal.js.map +0 -1
  90. package/dist/redact.d.ts.map +0 -1
  91. package/dist/redact.js.map +0 -1
  92. package/dist/token.d.ts.map +0 -1
  93. package/dist/token.js.map +0 -1
  94. package/dist/tool-registry.d.ts.map +0 -1
  95. package/dist/tool-registry.js.map +0 -1
  96. package/dist/tool-tokens.d.ts.map +0 -1
  97. package/dist/tool-tokens.js.map +0 -1
@@ -9,12 +9,14 @@
9
9
  *
10
10
  * Generalized from an upstream flat daemon module: the origin hardcoded its
11
11
  * host product's env-var names, now fields on
12
- * {@link OriginValidationEnvConfig} — see `source-map.md` for the exact
12
+ * {@link OriginValidationEnvConfig} — see `archived provenance ledger` for the exact
13
13
  * mapping. One function, a bypass for a specific product's own
14
14
  * browser-extension-driven ingest route, was deliberately **not** ported —
15
15
  * it names a specific product route and product feature with no generic
16
- * equivalent; see `source-map.md`.
16
+ * equivalent; see `archived provenance ledger`.
17
17
  */
18
+ /** Splits and trims the configured allow-list into entries. Both lenient request parsing and
19
+ * strict boot validation share this splitter so their definition of one entry cannot drift. */
18
20
  function splitAllowedOriginsEnv(config, env) {
19
21
  const raw = env[config.allowedOriginsEnvVar] || '';
20
22
  if (!raw.trim())
@@ -24,6 +26,7 @@ function splitAllowedOriginsEnv(config, env) {
24
26
  .map((origin) => origin.trim())
25
27
  .filter(Boolean);
26
28
  }
29
+ /** Normalizes one entry to a protocol://host origin; invalid or non-HTTP(S) entries are rejected. */
27
30
  function parseAllowedOrigin(entry) {
28
31
  let parsed;
29
32
  try {
@@ -41,18 +44,21 @@ function parseAllowedOrigin(entry) {
41
44
  *
42
45
  * Deliberately never throws — this is on the hot path of {@link isLocalSameOrigin}, called fresh
43
46
  * on every same-origin decision a host makes. A malformed entry is dropped and logged instead of
44
- * aborting the whole parse (see the twin `@jini-ai/http-kit` copy of this module for the fuller
45
- * incident history this mirrors: a request-time throw here used to turn one config typo into
47
+ * aborting the whole parse. A request-time throw used to turn one config typo into
46
48
  * either a 500 on every request, or — on a route with no enclosing catch — an unhandled
47
- * rejection). Call {@link assertValidAllowedOrigins} once at host startup instead, to fail loudly
49
+ * rejection: hand-mounted attachment routes and provider catch-all routes had no common
50
+ * enclosing catch. Dropping the malformed entry degrades to one fewer trusted origin,
51
+ * never every request breaking. Call {@link assertValidAllowedOrigins} once at host startup instead, to fail loudly
48
52
  * before any request-serving traffic exists.
49
53
  */
50
- export function configuredAllowedOrigins(config, env = process.env) {
54
+ export function configuredAllowedOrigins({ config, env }, { logger } = {}) {
51
55
  const origins = [];
52
56
  for (const entry of splitAllowedOriginsEnv(config, env)) {
53
57
  const origin = parseAllowedOrigin(entry);
54
58
  if (origin === undefined) {
55
- console.warn(`[@jini-ai/core] ignoring malformed ${config.allowedOriginsEnvVar} entry (must be an http:// or https:// origin): ${entry}`);
59
+ logger?.warn({
60
+ message: `[@jini-ai/core] ignoring malformed ${config.allowedOriginsEnvVar} entry (must be an http:// or https:// origin): ${entry}`,
61
+ });
56
62
  continue;
57
63
  }
58
64
  origins.push(origin);
@@ -62,9 +68,10 @@ export function configuredAllowedOrigins(config, env = process.env) {
62
68
  /**
63
69
  * Boot-time companion to {@link configuredAllowedOrigins}: throws, naming every malformed entry,
64
70
  * instead of silently dropping them. Call once, early in host startup, before the HTTP server
65
- * accepts connections.
71
+ * accepts connections. A misconfiguration must fail boot loudly rather than quietly serving with
72
+ * a smaller-than-intended allow-list, or throwing on whichever request first reaches the guard.
66
73
  */
67
- export function assertValidAllowedOrigins(config, env = process.env) {
74
+ export function assertValidAllowedOrigins({ config, env }) {
68
75
  const invalid = splitAllowedOriginsEnv(config, env).filter((entry) => parseAllowedOrigin(entry) === undefined);
69
76
  if (invalid.length === 0)
70
77
  return;
@@ -72,11 +79,11 @@ export function assertValidAllowedOrigins(config, env = process.env) {
72
79
  `(must be http:// or https:// origins): ${invalid.join(', ')}`);
73
80
  }
74
81
  /** The `host` (hostname:port) component of each configured allowed origin. */
75
- export function configuredAllowedHosts(origins) {
82
+ export function configuredAllowedHosts({ origins }) {
76
83
  return origins.map((origin) => new URL(origin).host);
77
84
  }
78
85
  /** The set of browser-facing ports considered "local": the daemon's own port plus an optional configured web port. */
79
- export function allowedBrowserPorts(config, port, env = process.env) {
86
+ export function allowedBrowserPorts({ config, port, env }) {
80
87
  const ports = [];
81
88
  const primary = Number(port);
82
89
  if (primary)
@@ -94,7 +101,7 @@ function headerValue(value) {
94
101
  return value == null ? undefined : String(value);
95
102
  }
96
103
  /** Parses a raw `Host` header value into hostname/host/port, or `null` when unparseable. */
97
- export function parseHostHeader(value) {
104
+ export function parseHostHeader({ value }) {
98
105
  const raw = String(headerValue(value) || '').trim();
99
106
  if (!raw)
100
107
  return null;
@@ -106,8 +113,10 @@ export function parseHostHeader(value) {
106
113
  return null;
107
114
  }
108
115
  }
109
- /** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16). */
110
- export function isPrivateIpv4(hostname) {
116
+ /** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
117
+ * Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
118
+ * for SSRF address classification; private addresses are allowed here for local development. */
119
+ export function isPrivateIpv4({ hostname }) {
111
120
  const parts = String(hostname || '').split('.');
112
121
  if (parts.length !== 4)
113
122
  return false;
@@ -120,7 +129,7 @@ export function isPrivateIpv4(hostname) {
120
129
  return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
121
130
  }
122
131
  /** Whether `hostname` is a literal IPv4 address or bracketed IPv6 literal (as opposed to a DNS name). */
123
- export function isIpLiteralHostname(hostname) {
132
+ export function isIpLiteralHostname({ hostname }) {
124
133
  const host = String(hostname || '').trim();
125
134
  if (!host)
126
135
  return false;
@@ -133,8 +142,9 @@ export function isIpLiteralHostname(hostname) {
133
142
  return false;
134
143
  return parts.map(Number).every((n) => Number.isInteger(n) && n >= 0 && n <= 255);
135
144
  }
136
- /** Whether `hostname` is loopback, unspecified, or a private-LAN address. */
137
- export function isLoopbackOrPrivateLanHost(hostname) {
145
+ /** Whether `hostname` is loopback, unspecified, or a private-LAN address.
146
+ * Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
147
+ export function isLoopbackOrPrivateLanHost({ hostname }) {
138
148
  const host = String(hostname || '').toLowerCase();
139
149
  return (host === 'localhost' ||
140
150
  host === '127.0.0.1' ||
@@ -142,26 +152,26 @@ export function isLoopbackOrPrivateLanHost(hostname) {
142
152
  host === '[::1]' ||
143
153
  host === '0.0.0.0' ||
144
154
  host === '::' ||
145
- isPrivateIpv4(host));
155
+ isPrivateIpv4({ hostname: host }));
146
156
  }
147
157
  /** Whether a request's `Host` header names a locally-served or explicitly allow-listed host. */
148
- export function isAllowedBrowserHost(config, hostHeader, ports, bindHost, extraAllowedOrigins) {
149
- const requestHost = parseHostHeader(hostHeader);
158
+ export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowedOrigins }) {
159
+ const requestHost = parseHostHeader({ value: hostHeader });
150
160
  if (!requestHost)
151
161
  return false;
152
162
  const loopbackHosts = ['127.0.0.1', 'localhost', '[::1]'];
153
163
  const explicitHosts = new Set([
154
164
  ...ports.flatMap((p) => [...loopbackHosts.map((h) => `${h}:${p}`), `${bindHost}:${p}`]),
155
- ...configuredAllowedHosts(extraAllowedOrigins),
165
+ ...configuredAllowedHosts({ origins: extraAllowedOrigins }),
156
166
  ]);
157
167
  if (explicitHosts.has(requestHost.host))
158
168
  return true;
159
169
  if (!ports.map(String).includes(requestHost.port))
160
170
  return false;
161
- return isLoopbackOrPrivateLanHost(requestHost.hostname);
171
+ return isLoopbackOrPrivateLanHost({ hostname: requestHost.hostname });
162
172
  }
163
173
  /** Whether an `Origin` header value names a locally-served or explicitly allow-listed origin. */
164
- export function isAllowedBrowserOrigin(config, origin, hostHeader, ports, bindHost, extraAllowedOrigins) {
174
+ export function isAllowedBrowserOrigin({ origin, hostHeader, ports, bindHost, extraAllowedOrigins }) {
165
175
  if (extraAllowedOrigins.includes(String(origin)))
166
176
  return true;
167
177
  let parsedOrigin;
@@ -173,7 +183,7 @@ export function isAllowedBrowserOrigin(config, origin, hostHeader, ports, bindHo
173
183
  }
174
184
  if (parsedOrigin.protocol !== 'http:' && parsedOrigin.protocol !== 'https:')
175
185
  return false;
176
- const requestHost = parseHostHeader(hostHeader);
186
+ const requestHost = parseHostHeader({ value: hostHeader });
177
187
  if (!requestHost)
178
188
  return false;
179
189
  const schemes = ['http', 'https'];
@@ -189,21 +199,21 @@ export function isAllowedBrowserOrigin(config, origin, hostHeader, ports, bindHo
189
199
  return false;
190
200
  if (parsedOrigin.hostname !== requestHost.hostname)
191
201
  return false;
192
- return isLoopbackOrPrivateLanHost(parsedOrigin.hostname);
202
+ return isLoopbackOrPrivateLanHost({ hostname: parsedOrigin.hostname });
193
203
  }
194
204
  /**
195
205
  * The top-level same-origin gate: is `req` a request the daemon should treat
196
206
  * as coming from one of its own locally-served (or explicitly allow-listed)
197
207
  * pages.
198
208
  */
199
- export function isLocalSameOrigin(config, req, port, env = process.env) {
209
+ export function isLocalSameOrigin({ config, req, port, env }, { logger } = {}) {
200
210
  const host = String(headerValue(req.headers?.host) || '');
201
211
  const origin = headerValue(req.headers?.origin);
202
- const ports = allowedBrowserPorts(config, port, env);
212
+ const ports = allowedBrowserPorts({ config, port, env });
203
213
  const bindHost = env[config.bindHostEnvVar] || '127.0.0.1';
204
- const extraAllowedOrigins = configuredAllowedOrigins(config, env);
205
- const ipOnlyExtraOrigins = extraAllowedOrigins.filter((o) => isIpLiteralHostname(new URL(o).hostname));
206
- const localHostAllowed = isAllowedBrowserHost(config, host, ports, bindHost, ipOnlyExtraOrigins);
214
+ 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 });
207
217
  if (origin == null || origin === '') {
208
218
  if (localHostAllowed)
209
219
  return true;
@@ -217,7 +227,7 @@ export function isLocalSameOrigin(config, req, port, env = process.env) {
217
227
  // consult the broader allow-list once that signal is present.
218
228
  const fetchSite = headerValue(req.headers?.['sec-fetch-site']);
219
229
  if (fetchSite === 'same-origin') {
220
- return isAllowedBrowserHost(config, host, ports, bindHost, extraAllowedOrigins);
230
+ return isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins });
221
231
  }
222
232
  return false;
223
233
  }
@@ -230,8 +240,8 @@ export function isLocalSameOrigin(config, req, port, env = process.env) {
230
240
  // allow-listed entry is the documented escape hatch for these deployments.
231
241
  if (extraAllowedOrigins.includes(origin))
232
242
  return true;
233
- if (!isAllowedBrowserHost(config, host, ports, bindHost, extraAllowedOrigins))
243
+ if (!isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins }))
234
244
  return false;
235
- return isAllowedBrowserOrigin(config, origin, host, ports, bindHost, extraAllowedOrigins);
245
+ return isAllowedBrowserOrigin({ config, origin, hostHeader: host, ports, bindHost, extraAllowedOrigins });
236
246
  }
237
247
  //# sourceMappingURL=origin-validation.js.map
@@ -15,17 +15,17 @@ import type { Pack } from './pack.js';
15
15
  import type { ToolRegistration, ToolRegistry } from './tool-registry.js';
16
16
  type AnyPack = Pack<any, any, string>;
17
17
  /**
18
- * Registers `pack.tools(services)` for every pack in `packs` that declares one, in pack order,
18
+ * Registers `pack.tools({ services })` for every pack in `packs` that declares one, in pack order,
19
19
  * into the single shared `registry`.
20
20
  *
21
21
  * Order matters and is deliberately the caller's to choose: `ToolRegistry.register` throws on a
22
- * duplicate descriptor id, so whichever pack registers second is the one named in the error. A
23
- * composition root that wants a host's own tools to win that message registers the host's packs
24
- * last.
22
+ * duplicate descriptor id, so whichever pack registers second triggers the error. The registry
23
+ * message names only the tool ID, not either pack. A composition root that wants a host's own
24
+ * contribution to trigger the duplicate failure registers the host's packs last.
25
25
  *
26
- * @param registry - The shared registry every composed pack contributes into.
27
- * @param packs - The composed packs, in composition order.
28
- * @param daemon - The `createDaemon` result holding each pack's already-built services.
26
+ * @param required.registry - The shared registry every composed pack contributes into.
27
+ * @param required.packs - The composed packs, in composition order.
28
+ * @param required.daemon - The `createDaemon` result holding each pack's already-built services.
29
29
  * @returns Every registration that was added, in the order it was added — so a caller can seed a
30
30
  * catalog, log an inventory, or assert on the composition without re-deriving it.
31
31
  * @throws Whatever `ToolRegistry.register` throws (notably a duplicate descriptor id), at the
@@ -33,14 +33,18 @@ type AnyPack = Pack<any, any, string>;
33
33
  * registered tool set is not a state anything downstream can reason about.
34
34
  * @complexity O(t) in total contributed tools.
35
35
  */
36
- export declare function registerPackTools<const Packs extends readonly AnyPack[]>(registry: ToolRegistry, packs: Packs, daemon: Daemon<Packs>): readonly ToolRegistration[];
36
+ export declare function registerPackTools<const Packs extends readonly AnyPack[]>({ registry, packs, daemon }: {
37
+ registry: ToolRegistry;
38
+ packs: Packs;
39
+ daemon: Daemon<Packs>;
40
+ }): readonly ToolRegistration[];
37
41
  /** One pack's teardown failure, kept with the pack's name so a caller can report which one failed. */
38
42
  export interface PackDisposalFailure {
39
43
  readonly pack: string;
40
44
  readonly error: unknown;
41
45
  }
42
46
  /**
43
- * Runs `pack.dispose(services)` for every pack that declares one, in **reverse** composition order
47
+ * Runs `pack.dispose({ services })` for every pack that declares one, in **reverse** composition order
44
48
  * (a pack composed later may depend on an earlier pack's resources, so it must release first).
45
49
  *
46
50
  * Best-effort by design: one pack throwing must never prevent the rest from disposing, or a single
@@ -48,11 +52,14 @@ export interface PackDisposalFailure {
48
52
  * and returned rather than thrown, leaving the composition root free to decide whether a teardown
49
53
  * failure is worth surfacing to its own caller.
50
54
  *
51
- * @param packs - The composed packs, in composition order (this function reverses them itself).
52
- * @param daemon - The `createDaemon` result holding each pack's already-built services.
55
+ * @param required.packs - The composed packs, in composition order (this function reverses them itself).
56
+ * @param required.daemon - The `createDaemon` result holding each pack's already-built services.
53
57
  * @returns The failures, in the order they occurred. Empty when every pack disposed cleanly.
54
58
  * @complexity O(p) in pack count, plus each pack's own teardown cost.
55
59
  */
56
- export declare function disposePacks<const Packs extends readonly AnyPack[]>(packs: Packs, daemon: Daemon<Packs>): Promise<readonly PackDisposalFailure[]>;
60
+ export declare function disposePacks<const Packs extends readonly AnyPack[]>({ packs, daemon }: {
61
+ packs: Packs;
62
+ daemon: Daemon<Packs>;
63
+ }): Promise<readonly PackDisposalFailure[]>;
57
64
  export {};
58
65
  //# sourceMappingURL=pack-lifecycle.d.ts.map
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Registers `pack.tools(services)` for every pack in `packs` that declares one, in pack order,
2
+ * Registers `pack.tools({ services })` for every pack in `packs` that declares one, in pack order,
3
3
  * into the single shared `registry`.
4
4
  *
5
5
  * Order matters and is deliberately the caller's to choose: `ToolRegistry.register` throws on a
6
- * duplicate descriptor id, so whichever pack registers second is the one named in the error. A
7
- * composition root that wants a host's own tools to win that message registers the host's packs
8
- * last.
6
+ * duplicate descriptor id, so whichever pack registers second triggers the error. The registry
7
+ * message names only the tool ID, not either pack. A composition root that wants a host's own
8
+ * contribution to trigger the duplicate failure registers the host's packs last.
9
9
  *
10
- * @param registry - The shared registry every composed pack contributes into.
11
- * @param packs - The composed packs, in composition order.
12
- * @param daemon - The `createDaemon` result holding each pack's already-built services.
10
+ * @param required.registry - The shared registry every composed pack contributes into.
11
+ * @param required.packs - The composed packs, in composition order.
12
+ * @param required.daemon - The `createDaemon` result holding each pack's already-built services.
13
13
  * @returns Every registration that was added, in the order it was added — so a caller can seed a
14
14
  * catalog, log an inventory, or assert on the composition without re-deriving it.
15
15
  * @throws Whatever `ToolRegistry.register` throws (notably a duplicate descriptor id), at the
@@ -17,11 +17,11 @@
17
17
  * registered tool set is not a state anything downstream can reason about.
18
18
  * @complexity O(t) in total contributed tools.
19
19
  */
20
- export function registerPackTools(registry, packs, daemon) {
20
+ export function registerPackTools({ registry, packs, daemon }) {
21
21
  const services = daemon.services;
22
22
  const registered = [];
23
23
  for (const pack of packs) {
24
- for (const registration of pack.tools?.(services[pack.name]) ?? []) {
24
+ for (const registration of pack.tools?.({ services: services[pack.name] }) ?? []) {
25
25
  registry.register(registration);
26
26
  registered.push(registration);
27
27
  }
@@ -29,7 +29,7 @@ export function registerPackTools(registry, packs, daemon) {
29
29
  return registered;
30
30
  }
31
31
  /**
32
- * Runs `pack.dispose(services)` for every pack that declares one, in **reverse** composition order
32
+ * Runs `pack.dispose({ services })` for every pack that declares one, in **reverse** composition order
33
33
  * (a pack composed later may depend on an earlier pack's resources, so it must release first).
34
34
  *
35
35
  * Best-effort by design: one pack throwing must never prevent the rest from disposing, or a single
@@ -37,12 +37,12 @@ export function registerPackTools(registry, packs, daemon) {
37
37
  * and returned rather than thrown, leaving the composition root free to decide whether a teardown
38
38
  * failure is worth surfacing to its own caller.
39
39
  *
40
- * @param packs - The composed packs, in composition order (this function reverses them itself).
41
- * @param daemon - The `createDaemon` result holding each pack's already-built services.
40
+ * @param required.packs - The composed packs, in composition order (this function reverses them itself).
41
+ * @param required.daemon - The `createDaemon` result holding each pack's already-built services.
42
42
  * @returns The failures, in the order they occurred. Empty when every pack disposed cleanly.
43
43
  * @complexity O(p) in pack count, plus each pack's own teardown cost.
44
44
  */
45
- export async function disposePacks(packs, daemon) {
45
+ export async function disposePacks({ packs, daemon }) {
46
46
  const services = daemon.services;
47
47
  const failures = [];
48
48
  for (let index = packs.length - 1; index >= 0; index -= 1) {
@@ -50,7 +50,7 @@ export async function disposePacks(packs, daemon) {
50
50
  if (!pack.dispose)
51
51
  continue;
52
52
  try {
53
- await pack.dispose(services[pack.name]);
53
+ await pack.dispose({ services: services[pack.name] });
54
54
  }
55
55
  catch (error) {
56
56
  failures.push({ pack: pack.name, error });
package/dist/pack.d.ts CHANGED
@@ -26,8 +26,12 @@ import type { AnyToken, ManyToken, Token } from './token.js';
26
26
  * silently falling through to a global container.
27
27
  */
28
28
  export interface PackContainer {
29
- get<T>(t: Token<T, string>): T;
30
- getMany<T>(t: ManyToken<T, string>): T[];
29
+ get<T>(required: {
30
+ token: Token<T, string>;
31
+ }): T;
32
+ getMany<T>(required: {
33
+ token: ManyToken<T, string>;
34
+ }): T[];
31
35
  }
32
36
  export interface Pack<Deps extends readonly AnyToken<unknown, string>[] = readonly AnyToken<unknown, string>[], Services = unknown, Name extends string = string> {
33
37
  readonly name: Name;
@@ -35,30 +39,54 @@ export interface Pack<Deps extends readonly AnyToken<unknown, string>[] = readon
35
39
  readonly services: (c: PackContainer) => Services;
36
40
  /**
37
41
  * `{descriptor, handler, policy}` triples this pack contributes to the composition's shared
38
- * `ToolRegistry`. Called exactly once per composed pack, before any transport is mounted, so a
39
- * pack's routes can assume its own tools are already registered.
42
+ * `ToolRegistry`. Composition roots must call `registerPackTools` exactly once per composed pack,
43
+ * before mounting any transport, so a pack's routes can assume its tools are already registered.
44
+ * This interface and `createDaemon` do not enforce that orchestration; repeated registration
45
+ * invokes the contribution again and duplicate tool IDs are rejected by the registry.
40
46
  *
41
47
  * Atomic with `http`/`cli` by construction: a pack that is not composed contributes neither its
42
48
  * tools nor its routes. See this module's own doc for the failure mode that makes this
43
49
  * load-bearing rather than a convenience.
44
50
  */
45
- readonly tools?: (services: Services) => readonly ToolRegistration[];
46
- readonly http?: (app: unknown, services: Services) => void;
47
- readonly cli?: (reg: unknown, services: Services) => void;
51
+ readonly tools?: (required: {
52
+ services: Services;
53
+ }) => readonly ToolRegistration[];
54
+ readonly http?: (required: {
55
+ app: unknown;
56
+ services: Services;
57
+ }) => void;
58
+ readonly cli?: (required: {
59
+ reg: unknown;
60
+ services: Services;
61
+ }) => void;
48
62
  /**
49
63
  * Releases whatever this pack's `services` acquired (a pty manager, a database handle, an OAuth
50
64
  * callback listener). Composition roots call these in reverse composition order, best-effort:
51
65
  * one pack's failure to dispose must never prevent another's from running.
52
66
  */
53
- readonly dispose?: (services: Services) => Promise<void> | void;
67
+ readonly dispose?: (required: {
68
+ services: Services;
69
+ }) => Promise<void> | void;
70
+ }
71
+ export interface PackContributions<Services> {
72
+ tools?: (required: {
73
+ services: Services;
74
+ }) => readonly ToolRegistration[];
75
+ http?: (required: {
76
+ app: unknown;
77
+ services: Services;
78
+ }) => void;
79
+ cli?: (required: {
80
+ reg: unknown;
81
+ services: Services;
82
+ }) => void;
83
+ dispose?: (required: {
84
+ services: Services;
85
+ }) => Promise<void> | void;
54
86
  }
55
87
  export declare function definePack<const Name extends string, const Deps extends readonly AnyToken<unknown, string>[], Services>(def: {
56
88
  name: Name;
57
89
  deps: Deps;
58
90
  services: (c: PackContainer) => Services;
59
- tools?: (services: Services) => readonly ToolRegistration[];
60
- http?: (app: unknown, services: Services) => void;
61
- cli?: (reg: unknown, services: Services) => void;
62
- dispose?: (services: Services) => Promise<void> | void;
63
- }): Pack<Deps, Services, Name>;
91
+ }, optional?: PackContributions<Services>): Pack<Deps, Services, Name>;
64
92
  //# sourceMappingURL=pack.d.ts.map
package/dist/pack.js CHANGED
@@ -1,4 +1,4 @@
1
- export function definePack(def) {
2
- return def;
1
+ export function definePack(def, optional = {}) {
2
+ return { ...def, ...optional };
3
3
  }
4
4
  //# sourceMappingURL=pack.js.map
@@ -0,0 +1,57 @@
1
+ /** One shared contract keeps binary, mail and webhook consumers behind the same egress guard.
2
+ * A guarded request requires a socket idle budget; total elapsed time is opt-in. */
3
+ export type HttpRequest = {
4
+ /** HEAD supports certificate/header diagnostics without downloading the page body. */
5
+ method: "POST" | "GET" | "HEAD" | "PUT" | "PATCH" | "DELETE";
6
+ url: string;
7
+ headers: Readonly<Record<string, string>>;
8
+ /** Preserve the signed bytes: re-serialization between signing and sending changes the signature. */
9
+ body?: string;
10
+ /** Whole-operation deadline, including DNS, redirects and body reads. Off by default.
11
+ * Retry/backoff belongs to the caller; its deadline can cover the whole retry operation. */
12
+ totalDeadlineMs?: number;
13
+ /** Bound the whole operation, including DNS and body reads, beyond the per-attempt timeout. */
14
+ signal?: AbortSignal;
15
+ /** Caller byte ceilings are bounded by the client policy, never a way to widen its cap. */
16
+ maxResponseBytes?: number;
17
+ } & ({
18
+ idleTimeoutMs: number; /** Legacy alias for the socket idle budget. */
19
+ timeoutMs?: number;
20
+ } | {
21
+ timeoutMs: number;
22
+ idleTimeoutMs?: number;
23
+ });
24
+ export interface HttpResponse {
25
+ status: number;
26
+ headers: Readonly<Record<string, string>>;
27
+ /** Bounded by the egress cap: a producer must not expose an unlimited text body through this port. */
28
+ bodyText: string;
29
+ /** Keep individual lines: Expires contains commas, so comma-splitting loses cookie boundaries. */
30
+ setCookies?: readonly string[];
31
+ /** UTF-8 decoding replaces invalid sequences and corrupts binary payloads. Expose the decompressed
32
+ * transport bytes here so binary consumers need not bypass DNS checks, peer pinning or redirects.
33
+ * Optional for older adapters/doubles; absence does not imply an empty payload. */
34
+ bodyBytes?: Uint8Array;
35
+ /** Coarse OR of text/byte clipping. A clipped image is corrupt, not merely smaller. Lossy UTF-8
36
+ * can expand invalid bytes into three-byte replacement characters and clip text even when all
37
+ * bytes fit, so binary consumers must use bodyBytesTruncated. Absence is an unknown verdict. */
38
+ bodyTruncated?: boolean;
39
+ /** Byte-specific completeness. A producer's unshaped truncation must count against both shapes
40
+ * rather than silently declaring bytes complete. Absent when no byte payload was supplied. */
41
+ bodyBytesTruncated?: boolean;
42
+ /** Provenance must name the guarded hop that supplied these bytes, not the requested URL or an
43
+ * unfollowed Location. Every followed hop is re-vetted. Older producers may omit this field,
44
+ * in which case consumers fall back to the request URL rather than inventing a blank source. */
45
+ finalUrl?: string;
46
+ }
47
+ export type RequestRedirect = "follow" | "error" | "manual";
48
+ /** Consumers receive the policy-enforcing client; the raw transport is a composition seam.
49
+ * Keeping construction at the host boundary prevents consumers from bypassing egress checks. */
50
+ export interface HttpClientPort {
51
+ send(required: {
52
+ request: HttpRequest;
53
+ }, optional?: {
54
+ redirect?: RequestRedirect;
55
+ }): Promise<HttpResponse>;
56
+ }
57
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1,8 @@
1
+ /** Unbranded identifier; UUID-producing adapters and opaque host IDs share this port. */
2
+ export type UUID = string;
3
+ export interface IdGenerator {
4
+ newId(): string;
5
+ }
6
+ /** Uses the host's cryptographic UUID generator; no insecure random fallback. */
7
+ export declare function createRandomUuidGenerator(): IdGenerator;
8
+ //# sourceMappingURL=ids.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** Uses the host's cryptographic UUID generator; no insecure random fallback. */
2
+ export function createRandomUuidGenerator() {
3
+ return { newId: () => globalThis.crypto.randomUUID() };
4
+ }
5
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,8 @@
1
+ export * from './time.js';
2
+ export * from './ids.js';
3
+ export * from './json.js';
4
+ export * from './http.js';
5
+ export * from './logger.js';
6
+ export * from './result.js';
7
+ export * from './paths.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ export * from './time.js';
2
+ export * from './ids.js';
3
+ export * from './json.js';
4
+ export * from './http.js';
5
+ export * from './logger.js';
6
+ export * from './result.js';
7
+ export * from './paths.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /** JSON wire values shared by contracts and domain packages. */
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
4
+ [key: string]: JsonValue;
5
+ };
6
+ export type JsonObject = {
7
+ [key: string]: JsonValue;
8
+ };
9
+ export type JsonArray = JsonValue[];
10
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1,26 @@
1
+ /** Host-independent logging contract: diagnostics travel as optional structured data. */
2
+ export interface Logger {
3
+ info(required: {
4
+ message: string;
5
+ }, optional?: {
6
+ meta?: Record<string, unknown>;
7
+ error?: unknown;
8
+ }): void;
9
+ warn(required: {
10
+ message: string;
11
+ }, optional?: {
12
+ meta?: Record<string, unknown>;
13
+ error?: unknown;
14
+ }): void;
15
+ error(required: {
16
+ message: string;
17
+ }, optional?: {
18
+ meta?: Record<string, unknown>;
19
+ error?: unknown;
20
+ }): void;
21
+ }
22
+ /** Minimal console adapter with a required host-owned prefix and structured metadata. */
23
+ export declare function createConsoleLogger({ prefix }: {
24
+ prefix: string;
25
+ }): Logger;
26
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1,17 @@
1
+ /** Minimal console adapter with a required host-owned prefix and structured metadata. */
2
+ export function createConsoleLogger({ prefix }) {
3
+ const write = (level, message, optional) => {
4
+ const args = [prefix ? `${prefix} ${message}` : message];
5
+ if (optional.meta !== undefined)
6
+ args.push(optional.meta);
7
+ if (optional.error !== undefined)
8
+ args.push(optional.error);
9
+ console[level](...args);
10
+ };
11
+ return {
12
+ info: ({ message }, optional = {}) => write('info', message, optional),
13
+ warn: ({ message }, optional = {}) => write('warn', message, optional),
14
+ error: ({ message }, optional = {}) => write('error', message, optional),
15
+ };
16
+ }
17
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Check lexical containment using whole directory segments, including equality.
3
+ * Hosts resolve cwd-relative paths and symlinks before calling: checking a textual
4
+ * prefix alone can permit traversal or a symlink escape. Keeping that effect outside
5
+ * this helper makes the kernel browser-safe. Windows hosts select case-insensitive
6
+ * comparison and backslash separators; drive roots and UNC share roots remain distinct.
7
+ * POSIX backslashes remain literal characters, so distinct directories cannot alias.
8
+ */
9
+ export declare function pathContains({ root, target }: {
10
+ root: string;
11
+ target: string;
12
+ }, { caseSensitive, separator }?: {
13
+ caseSensitive?: boolean;
14
+ separator?: '/' | '\\';
15
+ }): boolean;
16
+ //# sourceMappingURL=paths.d.ts.map