@jini-ai/core 0.3.1 → 0.4.1
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 +136 -0
- package/LICENSE +201 -0
- package/README.md +154 -14
- package/dist/agent-tools.d.ts +29 -0
- package/dist/agent-tools.js +2 -0
- package/dist/api-token-auth.d.ts +12 -5
- package/dist/api-token-auth.js +7 -7
- package/dist/bindings.d.ts +16 -5
- package/dist/bindings.js +7 -6
- package/dist/{internal.d.ts → composition.d.ts} +9 -9
- package/dist/{internal.js → composition.js} +9 -9
- package/dist/contribution-registry.d.ts +16 -0
- package/dist/contribution-registry.js +12 -0
- package/dist/daemon.d.ts +12 -7
- package/dist/daemon.js +21 -9
- package/dist/gated-mutations/actor-identity.d.ts +20 -0
- package/dist/gated-mutations/actor-identity.js +31 -0
- package/dist/gated-mutations/gateway.d.ts +115 -0
- package/dist/gated-mutations/gateway.js +123 -0
- package/dist/gated-mutations/ports.d.ts +27 -0
- package/dist/gated-mutations/ports.js +2 -0
- package/dist/gated-mutations/token.d.ts +95 -0
- package/dist/gated-mutations/token.js +87 -0
- package/dist/gated-mutations.d.ts +5 -0
- package/dist/gated-mutations.js +5 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +37 -0
- package/dist/model-facing-tool-errors.js +71 -0
- package/dist/naming.d.ts +31 -0
- package/dist/naming.js +33 -0
- package/dist/origin-validation.d.ts +64 -19
- package/dist/origin-validation.js +43 -33
- package/dist/pack-lifecycle.d.ts +19 -12
- package/dist/pack-lifecycle.js +14 -14
- package/dist/pack.d.ts +41 -13
- package/dist/pack.js +2 -2
- package/dist/primitives/http.d.ts +57 -0
- package/dist/primitives/http.js +2 -0
- package/dist/primitives/ids.d.ts +8 -0
- package/dist/primitives/ids.js +5 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.js +8 -0
- package/dist/primitives/json.d.ts +10 -0
- package/dist/primitives/json.js +2 -0
- package/dist/primitives/logger.d.ts +26 -0
- package/dist/primitives/logger.js +17 -0
- package/dist/primitives/paths.d.ts +16 -0
- package/dist/primitives/paths.js +38 -0
- package/dist/primitives/result.d.ts +9 -0
- package/dist/primitives/result.js +2 -0
- package/dist/primitives/time.d.ts +17 -0
- package/dist/primitives/time.js +13 -0
- package/dist/redact.d.ts +43 -8
- package/dist/redact.js +121 -50
- package/dist/registration-kit.d.ts +357 -0
- package/dist/registration-kit.js +439 -0
- package/dist/text/index.d.ts +3 -0
- package/dist/text/index.js +2 -0
- package/dist/text/sanitize-untrusted-text.d.ts +22 -0
- package/dist/text/sanitize-untrusted-text.js +108 -0
- package/dist/token.d.ts +29 -2
- package/dist/token.js +22 -2
- package/dist/tool-registry.d.ts +32 -16
- package/dist/tool-registry.js +11 -11
- package/dist/tool-tokens.js +1 -1
- package/package.json +56 -11
- package/dist/api-token-auth.d.ts.map +0 -1
- package/dist/api-token-auth.js.map +0 -1
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js.map +0 -1
- package/dist/compose.typecheck.d.ts +0 -2
- package/dist/compose.typecheck.d.ts.map +0 -1
- package/dist/compose.typecheck.js +0 -29
- package/dist/compose.typecheck.js.map +0 -1
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/origin-validation.d.ts.map +0 -1
- package/dist/origin-validation.js.map +0 -1
- package/dist/pack-lifecycle.d.ts.map +0 -1
- package/dist/pack-lifecycle.js.map +0 -1
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -1
- package/dist/principal.d.ts.map +0 -1
- package/dist/principal.js.map +0 -1
- package/dist/redact.d.ts.map +0 -1
- package/dist/redact.js.map +0 -1
- package/dist/token.d.ts.map +0 -1
- package/dist/token.js.map +0 -1
- package/dist/tool-registry.d.ts.map +0 -1
- package/dist/tool-registry.js.map +0 -1
- package/dist/tool-tokens.d.ts.map +0 -1
- 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 `
|
|
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 `
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
package/dist/pack-lifecycle.d.ts
CHANGED
|
@@ -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
|
|
23
|
-
*
|
|
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
|
|
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
|
|
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
|
package/dist/pack-lifecycle.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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>(
|
|
30
|
-
|
|
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`.
|
|
39
|
-
* pack's routes can assume its
|
|
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?: (
|
|
46
|
-
|
|
47
|
-
|
|
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?: (
|
|
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
|
-
|
|
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
|
@@ -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,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,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,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
|