@jini-ai/core 0.3.0 → 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.
- package/CHANGELOG.md +131 -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/composition.d.ts +41 -0
- package/dist/composition.js +39 -0
- 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 +9 -3
- package/dist/index.js +6 -1
- 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 +323 -0
- package/dist/registration-kit.js +396 -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 +60 -14
- package/dist/tool-registry.js +24 -9
- package/dist/tool-tokens.js +1 -1
- package/package.json +51 -7
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered domain-owned allowlists for safe model-facing failures. Unknown throws are unchanged.
|
|
3
|
+
* Rule evaluation is O(r); container wrapping O(n), with no I/O or reporting dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { type ToolHandler, type ToolRegistration } from "./tool-registry.js";
|
|
6
|
+
export interface ModelFacingErrorRule {
|
|
7
|
+
readonly error: abstract new (...args: never[]) => Error;
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly message?: string;
|
|
10
|
+
readonly guidance?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function forbiddenRule(required: {
|
|
13
|
+
domainPrefix: string;
|
|
14
|
+
error: ModelFacingErrorRule["error"];
|
|
15
|
+
}): ModelFacingErrorRule;
|
|
16
|
+
export declare function reclassifyToolError(required: {
|
|
17
|
+
err: unknown;
|
|
18
|
+
rules: readonly ModelFacingErrorRule[];
|
|
19
|
+
}): unknown;
|
|
20
|
+
export declare function withModelFacingErrors(required: {
|
|
21
|
+
handlers: Readonly<Record<string, ToolHandler>>;
|
|
22
|
+
rules: readonly ModelFacingErrorRule[];
|
|
23
|
+
}): Record<string, ToolHandler>;
|
|
24
|
+
export declare function withModelFacingRegistrationErrors(required: {
|
|
25
|
+
registrations: readonly ToolRegistration[];
|
|
26
|
+
rules: readonly ModelFacingErrorRule[];
|
|
27
|
+
}): ToolRegistration[];
|
|
28
|
+
export interface CallerSafeErrorRule {
|
|
29
|
+
readonly error: abstract new (...args: never[]) => Error;
|
|
30
|
+
readonly message?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function callerSafeErrorMessage(required: {
|
|
33
|
+
err: unknown;
|
|
34
|
+
rules: readonly CallerSafeErrorRule[];
|
|
35
|
+
fallback: string;
|
|
36
|
+
}): string;
|
|
37
|
+
//# sourceMappingURL=model-facing-tool-errors.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered domain-owned allowlists for safe model-facing failures. Unknown throws are unchanged.
|
|
3
|
+
* Rule evaluation is O(r); container wrapping O(n), with no I/O or reporting dependencies.
|
|
4
|
+
*/
|
|
5
|
+
// Tool transports redact unknown throws as internal failures. Leaving known domain refusals there
|
|
6
|
+
// made validation/not-found/permission errors look like identical crashes, preventing recovery.
|
|
7
|
+
// Domains explicitly own which classes/messages are safe to expose, including anonymous callers:
|
|
8
|
+
// stacks, internal paths, SQL, credentials, and another tenant's data must remain unlisted/redacted.
|
|
9
|
+
import { ToolInputError } from "./tool-registry.js";
|
|
10
|
+
export function forbiddenRule(required) {
|
|
11
|
+
return { error: required.error, code: `${required.domainPrefix}_FORBIDDEN` };
|
|
12
|
+
}
|
|
13
|
+
// First match wins: list subclasses before superclasses. Unmatched values must retain identity so
|
|
14
|
+
// the transport's redaction remains the default; already-classified ToolInputError also stays intact
|
|
15
|
+
// to avoid doubling a prefix attached by schema/recovery wrapping.
|
|
16
|
+
export function reclassifyToolError(required) {
|
|
17
|
+
const { err, rules } = required;
|
|
18
|
+
if (err instanceof ToolInputError)
|
|
19
|
+
return err;
|
|
20
|
+
for (const rule of rules) {
|
|
21
|
+
if (err instanceof rule.error) {
|
|
22
|
+
const message = `${rule.code}: ${rule.message ?? err.message}`;
|
|
23
|
+
return new ToolInputError({ message: rule.guidance ? `${message}. ${rule.guidance}` : message });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
// Wrap the whole map once: a correct per-handler converter previously reached only one handler
|
|
29
|
+
// while sibling operations still leaked into the internal-error redactor. Central wrapping has no
|
|
30
|
+
// omitted call-site arm, and successful results pass through untouched.
|
|
31
|
+
export function withModelFacingErrors(required) {
|
|
32
|
+
const { handlers, rules } = required;
|
|
33
|
+
return Object.fromEntries(Object.entries(handlers).map(([toolId, handler]) => [
|
|
34
|
+
toolId,
|
|
35
|
+
async (ctx, optional = {}) => {
|
|
36
|
+
try {
|
|
37
|
+
return await handler(ctx, optional);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
throw reclassifyToolError({ err, rules });
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
// Registration-array builders need the same blanket coverage as handler maps. Preserve descriptor,
|
|
46
|
+
// policy, ordering, and successful results; only the rejection path is reclassified.
|
|
47
|
+
export function withModelFacingRegistrationErrors(required) {
|
|
48
|
+
const { registrations, rules } = required;
|
|
49
|
+
return registrations.map((registration) => ({
|
|
50
|
+
...registration,
|
|
51
|
+
handler: (async (ctx, optional = {}) => {
|
|
52
|
+
try {
|
|
53
|
+
return await registration.handler(ctx, optional);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw reclassifyToolError({ err, rules });
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
// Unknown classes, raw driver errors, and non-Error throws get fixed fallback text, never raw error
|
|
62
|
+
// content. The fallback itself must not be derived from err; first matching rule still wins.
|
|
63
|
+
export function callerSafeErrorMessage(required) {
|
|
64
|
+
const { err, rules, fallback } = required;
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
if (err instanceof rule.error)
|
|
67
|
+
return rule.message ?? err.message;
|
|
68
|
+
}
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=model-facing-tool-errors.js.map
|
package/dist/naming.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Bounded asynchronous collision search; naming and copy wording are required host policy. */
|
|
2
|
+
export declare const MAX_SUFFIX_ATTEMPTS = 1000;
|
|
3
|
+
export interface NamingPolicy {
|
|
4
|
+
isTaken: (required: {
|
|
5
|
+
candidate: string;
|
|
6
|
+
}) => Promise<boolean>;
|
|
7
|
+
withSuffix: (required: {
|
|
8
|
+
base: string;
|
|
9
|
+
suffix: number;
|
|
10
|
+
}) => string;
|
|
11
|
+
exhaustionMessage: (required: {
|
|
12
|
+
base: string;
|
|
13
|
+
maxAttempts: number;
|
|
14
|
+
}) => string;
|
|
15
|
+
}
|
|
16
|
+
export interface AvailableNameOptions {
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
onExhausted?: (required: {
|
|
19
|
+
base: string;
|
|
20
|
+
maxAttempts: number;
|
|
21
|
+
}) => never;
|
|
22
|
+
}
|
|
23
|
+
/** O(k) sequential predicate calls, O(1) local space, k <= maxAttempts. */
|
|
24
|
+
export declare function deriveAvailableName(required: NamingPolicy & {
|
|
25
|
+
base: string;
|
|
26
|
+
}, optional?: AvailableNameOptions): Promise<string>;
|
|
27
|
+
/** O(k) plus at most one base lookup. A trailing integer is a counter only if its base exists. */
|
|
28
|
+
export declare function deriveDuplicateName(required: NamingPolicy & {
|
|
29
|
+
sourceName: string;
|
|
30
|
+
}, optional?: AvailableNameOptions): Promise<string>;
|
|
31
|
+
//# sourceMappingURL=naming.d.ts.map
|
package/dist/naming.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Bounded asynchronous collision search; naming and copy wording are required host policy. */
|
|
2
|
+
export const MAX_SUFFIX_ATTEMPTS = 1000;
|
|
3
|
+
/** O(k) sequential predicate calls, O(1) local space, k <= maxAttempts. */
|
|
4
|
+
export async function deriveAvailableName(required, optional = {}) {
|
|
5
|
+
// isTaken may also reject reserved/invalid candidates. withSuffix owns formatting and length caps;
|
|
6
|
+
// shorten the base to make room rather than truncating the counter and repeating a taken candidate.
|
|
7
|
+
const maxAttempts = optional.maxAttempts ?? MAX_SUFFIX_ATTEMPTS;
|
|
8
|
+
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
|
|
9
|
+
throw new RangeError('maxAttempts must be a positive safe integer');
|
|
10
|
+
for (let suffix = 1; suffix <= maxAttempts; suffix += 1) {
|
|
11
|
+
const candidate = suffix === 1 ? required.base : required.withSuffix({ base: required.base, suffix });
|
|
12
|
+
if (!(await required.isTaken({ candidate })))
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
if (optional.onExhausted)
|
|
16
|
+
optional.onExhausted({ base: required.base, maxAttempts });
|
|
17
|
+
throw new Error(required.exhaustionMessage({ base: required.base, maxAttempts }));
|
|
18
|
+
}
|
|
19
|
+
const TRAILING_NUMBER_PATTERN = /^(.*\S) \d+$/;
|
|
20
|
+
// Require a non-space base: a bare number has nothing meaningful to strip. Existing-base evidence
|
|
21
|
+
// distinguishes a copy counter ("Landing 2") from a real title ("Blog 2024" with no "Blog" row).
|
|
22
|
+
/** O(k) plus at most one base lookup. A trailing integer is a counter only if its base exists. */
|
|
23
|
+
export async function deriveDuplicateName(required, optional = {}) {
|
|
24
|
+
const strippedBase = TRAILING_NUMBER_PATTERN.exec(required.sourceName)?.[1];
|
|
25
|
+
const base = strippedBase !== undefined && await required.isTaken({ candidate: strippedBase })
|
|
26
|
+
? strippedBase : required.sourceName;
|
|
27
|
+
return deriveAvailableName({
|
|
28
|
+
// A copy must never reuse the source's name, even if the external predicate overlooks its row.
|
|
29
|
+
...required, base,
|
|
30
|
+
isTaken: ({ candidate }) => candidate === required.sourceName ? Promise.resolve(true) : required.isTaken({ candidate }),
|
|
31
|
+
}, optional);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=naming.js.map
|
|
@@ -9,12 +9,13 @@
|
|
|
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
|
+
import type { Logger } from "./primitives/index.js";
|
|
18
19
|
export interface ParsedHostHeader {
|
|
19
20
|
hostname: string;
|
|
20
21
|
host: string;
|
|
@@ -36,44 +37,88 @@ export interface OriginValidationEnvConfig {
|
|
|
36
37
|
/** Env var carrying the host the daemon itself is bound to (default `'127.0.0.1'`). */
|
|
37
38
|
bindHostEnvVar: string;
|
|
38
39
|
}
|
|
40
|
+
/** Host-supplied environment snapshot, shared by origin validation helpers. */
|
|
41
|
+
export interface OriginValidationArgs {
|
|
42
|
+
config: OriginValidationEnvConfig;
|
|
43
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
44
|
+
}
|
|
45
|
+
/** Optional diagnostic sink; validation never reads an ambient logger. */
|
|
46
|
+
export type OriginValidationLoggerPort = Pick<Logger, "warn">;
|
|
39
47
|
/**
|
|
40
48
|
* Parses {@link OriginValidationEnvConfig.allowedOriginsEnvVar} into normalized origins.
|
|
41
49
|
*
|
|
42
50
|
* Deliberately never throws — this is on the hot path of {@link isLocalSameOrigin}, called fresh
|
|
43
51
|
* 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
|
|
52
|
+
* aborting the whole parse. A request-time throw used to turn one config typo into
|
|
46
53
|
* either a 500 on every request, or — on a route with no enclosing catch — an unhandled
|
|
47
|
-
* rejection
|
|
54
|
+
* rejection: hand-mounted attachment routes and provider catch-all routes had no common
|
|
55
|
+
* enclosing catch. Dropping the malformed entry degrades to one fewer trusted origin,
|
|
56
|
+
* never every request breaking. Call {@link assertValidAllowedOrigins} once at host startup instead, to fail loudly
|
|
48
57
|
* before any request-serving traffic exists.
|
|
49
58
|
*/
|
|
50
|
-
export declare function configuredAllowedOrigins(config:
|
|
59
|
+
export declare function configuredAllowedOrigins({ config, env }: OriginValidationArgs, { logger }?: {
|
|
60
|
+
logger?: OriginValidationLoggerPort | undefined;
|
|
61
|
+
}): string[];
|
|
51
62
|
/**
|
|
52
63
|
* Boot-time companion to {@link configuredAllowedOrigins}: throws, naming every malformed entry,
|
|
53
64
|
* instead of silently dropping them. Call once, early in host startup, before the HTTP server
|
|
54
|
-
* accepts connections.
|
|
65
|
+
* accepts connections. A misconfiguration must fail boot loudly rather than quietly serving with
|
|
66
|
+
* a smaller-than-intended allow-list, or throwing on whichever request first reaches the guard.
|
|
55
67
|
*/
|
|
56
|
-
export declare function assertValidAllowedOrigins(config
|
|
68
|
+
export declare function assertValidAllowedOrigins({ config, env }: OriginValidationArgs): void;
|
|
57
69
|
/** The `host` (hostname:port) component of each configured allowed origin. */
|
|
58
|
-
export declare function configuredAllowedHosts(origins
|
|
70
|
+
export declare function configuredAllowedHosts({ origins }: {
|
|
71
|
+
origins: string[];
|
|
72
|
+
}): string[];
|
|
59
73
|
/** The set of browser-facing ports considered "local": the daemon's own port plus an optional configured web port. */
|
|
60
|
-
export declare function allowedBrowserPorts(config
|
|
74
|
+
export declare function allowedBrowserPorts({ config, port, env }: OriginValidationArgs & {
|
|
75
|
+
port: number | string | null | undefined;
|
|
76
|
+
}): number[];
|
|
61
77
|
/** Parses a raw `Host` header value into hostname/host/port, or `null` when unparseable. */
|
|
62
|
-
export declare function parseHostHeader(value
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
export declare function parseHostHeader({ value }: {
|
|
79
|
+
value: unknown;
|
|
80
|
+
}): ParsedHostHeader | null;
|
|
81
|
+
/** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
|
|
82
|
+
* Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
|
|
83
|
+
* for SSRF address classification; private addresses are allowed here for local development. */
|
|
84
|
+
export declare function isPrivateIpv4({ hostname }: {
|
|
85
|
+
hostname: unknown;
|
|
86
|
+
}): boolean;
|
|
65
87
|
/** Whether `hostname` is a literal IPv4 address or bracketed IPv6 literal (as opposed to a DNS name). */
|
|
66
|
-
export declare function isIpLiteralHostname(hostname
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
export declare function isIpLiteralHostname({ hostname }: {
|
|
89
|
+
hostname: unknown;
|
|
90
|
+
}): boolean;
|
|
91
|
+
/** Whether `hostname` is loopback, unspecified, or a private-LAN address.
|
|
92
|
+
* Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
|
|
93
|
+
export declare function isLoopbackOrPrivateLanHost({ hostname }: {
|
|
94
|
+
hostname: unknown;
|
|
95
|
+
}): boolean;
|
|
69
96
|
/** Whether a request's `Host` header names a locally-served or explicitly allow-listed host. */
|
|
70
|
-
export declare function isAllowedBrowserHost(
|
|
97
|
+
export declare function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowedOrigins }: {
|
|
98
|
+
config: OriginValidationEnvConfig;
|
|
99
|
+
hostHeader: unknown;
|
|
100
|
+
ports: number[];
|
|
101
|
+
bindHost: string;
|
|
102
|
+
extraAllowedOrigins: string[];
|
|
103
|
+
}): boolean;
|
|
71
104
|
/** Whether an `Origin` header value names a locally-served or explicitly allow-listed origin. */
|
|
72
|
-
export declare function isAllowedBrowserOrigin(
|
|
105
|
+
export declare function isAllowedBrowserOrigin({ origin, hostHeader, ports, bindHost, extraAllowedOrigins }: {
|
|
106
|
+
config: OriginValidationEnvConfig;
|
|
107
|
+
origin: unknown;
|
|
108
|
+
hostHeader: unknown;
|
|
109
|
+
ports: number[];
|
|
110
|
+
bindHost: string;
|
|
111
|
+
extraAllowedOrigins: string[];
|
|
112
|
+
}): boolean;
|
|
73
113
|
/**
|
|
74
114
|
* The top-level same-origin gate: is `req` a request the daemon should treat
|
|
75
115
|
* as coming from one of its own locally-served (or explicitly allow-listed)
|
|
76
116
|
* pages.
|
|
77
117
|
*/
|
|
78
|
-
export declare function isLocalSameOrigin(config
|
|
118
|
+
export declare function isLocalSameOrigin({ config, req, port, env }: OriginValidationArgs & {
|
|
119
|
+
req: RequestWithOriginHeaders;
|
|
120
|
+
port: number | string | null | undefined;
|
|
121
|
+
}, { logger }?: {
|
|
122
|
+
logger?: OriginValidationLoggerPort | undefined;
|
|
123
|
+
}): boolean;
|
|
79
124
|
//# sourceMappingURL=origin-validation.d.ts.map
|
|
@@ -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 });
|