@jini-ai/core 0.4.7 → 0.4.9
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 +11 -0
- package/dist/agent-tools.d.ts +1 -1
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +87 -22
- package/dist/batch-tool-inputs.d.ts +147 -0
- package/dist/batch-tool-inputs.js +257 -0
- package/dist/feature-flags.d.ts +21 -0
- package/dist/feature-flags.js +17 -0
- package/dist/fetch/index.d.ts +2 -1
- package/dist/fetch/index.js +1 -0
- package/dist/fetch/loader-read.d.ts +16 -0
- package/dist/fetch/loader-read.js +22 -0
- package/dist/fetch/types.d.ts +12 -2
- package/dist/gated-mutations/gateway.js +32 -27
- package/dist/index.d.ts +9 -1
- package/dist/index.js +8 -1
- package/dist/object-store.d.ts +115 -0
- package/dist/object-store.js +70 -0
- package/dist/origin-validation.js +64 -57
- package/dist/primitives/http.d.ts +2 -2
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/paths.js +17 -10
- package/dist/primitives/tasks.d.ts +10 -0
- package/dist/primitives/tasks.js +8 -0
- package/dist/provider-catalog.d.ts +76 -0
- package/dist/provider-catalog.js +6 -0
- package/dist/provider-registry.d.ts +144 -0
- package/dist/provider-registry.js +138 -0
- package/dist/records.d.ts +24 -0
- package/dist/records.js +25 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.js +57 -34
- package/dist/registration-kit.d.ts +7 -4
- package/dist/registration-kit.js +81 -62
- package/dist/settings/agent-value-write-tools.d.ts +19 -0
- package/dist/settings/agent-value-write-tools.js +131 -70
- package/dist/settings/ensure-definitions.d.ts +4 -2
- package/dist/settings/ensure-definitions.js +79 -63
- package/dist/settings/express/cms-adapter.d.ts +2 -2
- package/dist/settings/express/cms-adapter.js +34 -22
- package/dist/settings/express/http-request.d.ts +33 -0
- package/dist/settings/express/http-request.js +49 -0
- package/dist/settings/express/http.d.ts +2 -2
- package/dist/settings/express/http.js +16 -39
- package/dist/settings/express/stream.d.ts +2 -2
- package/dist/settings/express/stream.js +49 -27
- package/dist/settings/repo.memory.js +7 -4
- package/dist/settings/settings.js +54 -60
- package/dist/settings/tool-registrations.d.ts +2 -3
- package/dist/settings/tool-registrations.js +64 -39
- package/dist/settings/write-service.d.ts +19 -0
- package/dist/settings/write-service.js +34 -0
- package/dist/target-scope.d.ts +52 -0
- package/dist/target-scope.js +82 -0
- package/dist/text/escape.js +2 -2
- package/dist/text/sanitize-untrusted-text.d.ts +5 -0
- package/dist/text/sanitize-untrusted-text.js +55 -35
- package/dist/tool-exposure.d.ts +79 -0
- package/dist/tool-exposure.js +163 -0
- package/dist/tool-input-errors.d.ts +23 -0
- package/dist/tool-input-errors.js +24 -0
- package/dist/tool-input-normalization.d.ts +37 -0
- package/dist/tool-input-normalization.js +188 -0
- package/dist/tool-metadata-types.d.ts +26 -0
- package/dist/tool-metadata-types.js +2 -0
- package/dist/tool-metadata.d.ts +2 -23
- package/dist/tool-registry.d.ts +38 -22
- package/dist/tool-registry.js +35 -20
- package/package.json +20 -2
- package/plugin-directory.js +72 -33
package/dist/fetch/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** Framework-neutral query contracts and cache; hosts supply transport callbacks and environment ports. */
|
|
2
2
|
export { DEFAULT_STALE_TIME, IDLE_RETENTION, FetchQueryCache, toQueryError } from './cache.js';
|
|
3
|
+
export { missingCachedLoaderFetchError, withCachedLoaderFetch } from './loader-read.js';
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CachedLoaderDefaults, CachedLoaderOptions, QueryKey } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The error an argument-less `load()` rejects with when its loader was created without a default
|
|
4
|
+
* `fetch`. Named, rather than left to a `TypeError: fetch is not a function` from deep inside a cache
|
|
5
|
+
* adapter, so the misuse points at the call site that needs to pass `{ key, fetch }`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function missingCachedLoaderFetchError(key: QueryKey): Error;
|
|
8
|
+
/**
|
|
9
|
+
* Runs `read` with a fully specified `{ key, fetch }`, or rejects with
|
|
10
|
+
* {@link missingCachedLoaderFetchError} when `required` (a loader's own defaults, for an argument-less
|
|
11
|
+
* `load()`) carries no fetch. It rejects BEFORE `read` runs, so a misuse never enters the shared cache:
|
|
12
|
+
* a failed fetch there would publish an error state to every observer of that key.
|
|
13
|
+
* Shared by every `FetchQueryAdapter` so each adapter's `load` stays a single straight-line call.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withCachedLoaderFetch<T>(required: CachedLoaderDefaults<T>, read: (resolved: Pick<CachedLoaderOptions<T>, 'key' | 'fetch'>) => Promise<T>): Promise<T>;
|
|
16
|
+
//# sourceMappingURL=loader-read.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error an argument-less `load()` rejects with when its loader was created without a default
|
|
3
|
+
* `fetch`. Named, rather than left to a `TypeError: fetch is not a function` from deep inside a cache
|
|
4
|
+
* adapter, so the misuse points at the call site that needs to pass `{ key, fetch }`.
|
|
5
|
+
*/
|
|
6
|
+
export function missingCachedLoaderFetchError(key) {
|
|
7
|
+
return new Error(`Cached loader for key ${JSON.stringify(key)} has no default fetch: call load({ key, fetch }) with a fetch of its own`);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs `read` with a fully specified `{ key, fetch }`, or rejects with
|
|
11
|
+
* {@link missingCachedLoaderFetchError} when `required` (a loader's own defaults, for an argument-less
|
|
12
|
+
* `load()`) carries no fetch. It rejects BEFORE `read` runs, so a misuse never enters the shared cache:
|
|
13
|
+
* a failed fetch there would publish an error state to every observer of that key.
|
|
14
|
+
* Shared by every `FetchQueryAdapter` so each adapter's `load` stays a single straight-line call.
|
|
15
|
+
*/
|
|
16
|
+
export function withCachedLoaderFetch(required, read) {
|
|
17
|
+
const { key, fetch } = required;
|
|
18
|
+
if (fetch === undefined)
|
|
19
|
+
return Promise.reject(missingCachedLoaderFetchError(key));
|
|
20
|
+
return read({ key, fetch });
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=loader-read.js.map
|
package/dist/fetch/types.d.ts
CHANGED
|
@@ -67,13 +67,23 @@ export interface CachedLoaderOptions<T> {
|
|
|
67
67
|
fetch: () => Promise<T>;
|
|
68
68
|
staleTime?: number;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* What a loader hook is created with: the key its `peek`/`replace` (and an argument-less `load`)
|
|
72
|
+
* address, plus an optional default `fetch`. The default is optional because a loader whose every
|
|
73
|
+
* `load` names its own `{ key, fetch }` (per-id detail lookups, per-path file reads) would otherwise
|
|
74
|
+
* have to invent a fetch that can never run. An argument-less `load()` on a loader created without
|
|
75
|
+
* one rejects with {@link missingCachedLoaderFetchError} instead of touching the cache.
|
|
76
|
+
*/
|
|
77
|
+
export type CachedLoaderDefaults<T> = Pick<CachedLoaderOptions<T>, "key"> & {
|
|
78
|
+
fetch?: CachedLoaderOptions<T>["fetch"] | undefined;
|
|
79
|
+
};
|
|
70
80
|
/**
|
|
71
81
|
* A plain promise loader serves components that own their loading UI rather than consuming
|
|
72
82
|
* a hook result. It shares the provider cache, including one in-flight request per key.
|
|
73
83
|
*/
|
|
74
84
|
export interface CachedLoader<T> {
|
|
75
85
|
peek: () => T | undefined;
|
|
76
|
-
load: (required?: Pick<CachedLoaderOptions<T>, "key" | "fetch">) => Promise<T>;
|
|
86
|
+
load: (required?: Pick<CachedLoaderOptions<T>, "key" | "fetch">, optional?: Pick<CachedLoaderOptions<T>, "staleTime">) => Promise<T>;
|
|
77
87
|
replace: (required: {
|
|
78
88
|
value: T;
|
|
79
89
|
}) => void;
|
|
@@ -88,6 +98,6 @@ export interface FetchQueryAdapter {
|
|
|
88
98
|
useInvalidate: () => (required: {
|
|
89
99
|
key: QueryKey;
|
|
90
100
|
}) => void;
|
|
91
|
-
useCachedLoader: <T>(required:
|
|
101
|
+
useCachedLoader: <T>(required: CachedLoaderDefaults<T>, optional?: Omit<CachedLoaderOptions<T>, "key" | "fetch">) => CachedLoader<T>;
|
|
92
102
|
}
|
|
93
103
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -40,13 +40,36 @@ export async function authorizeForHooks(required) {
|
|
|
40
40
|
}
|
|
41
41
|
return deps.authorizeInstance({ principalId, permission });
|
|
42
42
|
}
|
|
43
|
+
/** Fresh authorization gate shared by plan/confirm/execute; refuses with NOT_AUTHORIZED. */
|
|
44
|
+
async function requireAuthorization(required) {
|
|
45
|
+
const { principalId, permission } = required;
|
|
46
|
+
const authResult = await authorizeForHooks(required);
|
|
47
|
+
if (!authResult.allowed) {
|
|
48
|
+
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${permission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The refusal for a token that cannot be redeemed: already redeemed, otherwise expired. */
|
|
52
|
+
function unredeemableTokenError(record) {
|
|
53
|
+
if (record?.status === "redeemed") {
|
|
54
|
+
return new TokenAlreadyRedeemedError({ message: `confirmation token has already been redeemed` });
|
|
55
|
+
}
|
|
56
|
+
return new TokenExpiredError({ message: `confirmation token has expired` });
|
|
57
|
+
}
|
|
58
|
+
/** Look up the presented token and refuse it unless it is still minted and unexpired. */
|
|
59
|
+
async function loadRedeemableToken(required) {
|
|
60
|
+
const { deps, confirmationToken } = required;
|
|
61
|
+
const record = await deps.tokens.findByToken({ token: confirmationToken });
|
|
62
|
+
if (!record) {
|
|
63
|
+
throw new TokenExpiredError({ message: `confirmation token was not found` });
|
|
64
|
+
}
|
|
65
|
+
if (!isRedeemable({ record, now: nowIso({ clock: deps.clock }) }))
|
|
66
|
+
throw unredeemableTokenError(record);
|
|
67
|
+
return record;
|
|
68
|
+
}
|
|
43
69
|
// Planning is read-only: check read permission and derive details, never invoke executeMutation.
|
|
44
70
|
export async function plan(required, _optional = {}) {
|
|
45
71
|
const { deps, principalId, hooks } = required;
|
|
46
|
-
|
|
47
|
-
if (!authResult.allowed) {
|
|
48
|
-
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.readPermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
49
|
-
}
|
|
72
|
+
await requireAuthorization({ deps, hooks, principalId, permission: hooks.readPermission });
|
|
50
73
|
const { planHash, details } = await hooks.computePlan({});
|
|
51
74
|
return { domain: hooks.domain, planId: deps.idGen.newId(), planHash, details };
|
|
52
75
|
}
|
|
@@ -57,10 +80,7 @@ export async function confirm(required, _optional = {}) {
|
|
|
57
80
|
if (principalKind === "agent") {
|
|
58
81
|
throw new ForbiddenError({ message: `agent principals may not confirm a gated mutation`, reasonCode: "AGENT_CANNOT_CONFIRM" });
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
if (!authResult.allowed) {
|
|
62
|
-
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.mutatePermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
63
|
-
}
|
|
83
|
+
await requireAuthorization({ deps, hooks, principalId, permission: hooks.mutatePermission });
|
|
64
84
|
const token = mintToken({
|
|
65
85
|
planId,
|
|
66
86
|
planHash,
|
|
@@ -82,20 +102,8 @@ export async function confirm(required, _optional = {}) {
|
|
|
82
102
|
export async function execute(required, _optional = {}) {
|
|
83
103
|
// Reevaluate authorization here; confirm-time permission is not authority to execute later.
|
|
84
104
|
const { deps, principalId, principalKind, hooks, confirmationToken } = required;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw new ForbiddenError({ message: `principal '${principalId}' is not authorized for '${hooks.mutatePermission}' (${authResult.reason})`, reasonCode: "NOT_AUTHORIZED" });
|
|
88
|
-
}
|
|
89
|
-
const record = await deps.tokens.findByToken({ token: confirmationToken });
|
|
90
|
-
if (!record) {
|
|
91
|
-
throw new TokenExpiredError({ message: `confirmation token was not found` });
|
|
92
|
-
}
|
|
93
|
-
if (!isRedeemable({ record, now: nowIso({ clock: deps.clock }) })) {
|
|
94
|
-
if (record.status === "redeemed") {
|
|
95
|
-
throw new TokenAlreadyRedeemedError({ message: `confirmation token has already been redeemed` });
|
|
96
|
-
}
|
|
97
|
-
throw new TokenExpiredError({ message: `confirmation token has expired` });
|
|
98
|
-
}
|
|
105
|
+
await requireAuthorization({ deps, hooks, principalId, permission: hooks.mutatePermission });
|
|
106
|
+
const record = await loadRedeemableToken({ deps, confirmationToken });
|
|
99
107
|
const resolvedIdentity = await hooks.resolveActorClassIdentity({ principalId, principalKind });
|
|
100
108
|
if (resolvedIdentity !== record.confirmerPrincipalId) {
|
|
101
109
|
throw new ForbiddenError({ message: `principal '${principalId}' (kind '${principalKind}') is not the confirmer of this token`, reasonCode: "ACTOR_CLASS_MISMATCH" });
|
|
@@ -113,11 +121,8 @@ export async function execute(required, _optional = {}) {
|
|
|
113
121
|
// Concurrent executions can pass every earlier check; atomic redemption makes the loser fail
|
|
114
122
|
// before its domain mutation, preventing a second application of the same approval.
|
|
115
123
|
const { redeemed, record: latestRecord } = await deps.tokens.tryRedeem({ token: confirmationToken, now: nowIso({ clock: deps.clock }) });
|
|
116
|
-
if (!redeemed)
|
|
117
|
-
|
|
118
|
-
throw new TokenExpiredError({ message: `confirmation token has expired` });
|
|
119
|
-
throw new TokenAlreadyRedeemedError({ message: `confirmation token has already been redeemed` });
|
|
120
|
-
}
|
|
124
|
+
if (!redeemed)
|
|
125
|
+
throw unredeemableTokenError(latestRecord);
|
|
121
126
|
return hooks.executeMutation({ planHash, details });
|
|
122
127
|
}
|
|
123
128
|
//# sourceMappingURL=gateway.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -12,15 +12,23 @@ export * from './origin-validation.js';
|
|
|
12
12
|
export * from './principal.js';
|
|
13
13
|
export * from './surface-exchanges.js';
|
|
14
14
|
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
15
|
-
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
|
|
15
|
+
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, toolDiscoveryMetadata, ToolInputError } from './tool-registry.js';
|
|
16
16
|
export * from './tool-tokens.js';
|
|
17
|
+
export * from './tool-exposure.js';
|
|
17
18
|
export * from './gated-mutations.js';
|
|
18
19
|
export * from './model-facing-tool-errors.js';
|
|
19
20
|
export * from './contribution-registry.js';
|
|
21
|
+
export * from './provider-registry.js';
|
|
22
|
+
export * from './object-store.js';
|
|
20
23
|
export * from './naming.js';
|
|
21
24
|
export type { AgentToolSideEffect, AgentToolActorClassRule, AgentToolDefinition } from './agent-tools.js';
|
|
22
25
|
export * from "./registration-kit.js";
|
|
23
26
|
export * from "./approval.js";
|
|
27
|
+
export * from './target-scope.js';
|
|
24
28
|
export * from "./native-regexp.js";
|
|
25
29
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, type LegacyToolAuthorizeFn, type AuthorizationPort as ToolPermissionAuthorizationPort, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "./tool-permissions.js";
|
|
30
|
+
export { evaluateFeatureFlag, type FeatureFlagDecision } from './feature-flags.js';
|
|
31
|
+
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError, type NormalizedToolInput, type ToolInputNormalizer } from './tool-input-normalization.js';
|
|
32
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS, type BatchableTool, type BatchEligibility, type BatchOptOut, type BatchResult, type BatchItemResult } from './batch-tool-inputs.js';
|
|
33
|
+
export type { MediaProviderCatalogPolicy, MediaProviderOption, ProviderPreset, ProviderPresetKind } from './provider-catalog.js';
|
|
26
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -9,14 +9,21 @@ export * from './api-token-auth.js';
|
|
|
9
9
|
export * from './origin-validation.js';
|
|
10
10
|
export * from './principal.js';
|
|
11
11
|
export * from './surface-exchanges.js';
|
|
12
|
-
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, ToolInputError } from './tool-registry.js';
|
|
12
|
+
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, toolDiscoveryMetadata, ToolInputError } from './tool-registry.js';
|
|
13
13
|
export * from './tool-tokens.js';
|
|
14
|
+
export * from './tool-exposure.js';
|
|
14
15
|
export * from './gated-mutations.js';
|
|
15
16
|
export * from './model-facing-tool-errors.js';
|
|
16
17
|
export * from './contribution-registry.js';
|
|
18
|
+
export * from './provider-registry.js';
|
|
19
|
+
export * from './object-store.js';
|
|
17
20
|
export * from './naming.js';
|
|
18
21
|
export * from "./registration-kit.js";
|
|
19
22
|
export * from "./approval.js";
|
|
23
|
+
export * from './target-scope.js';
|
|
20
24
|
export * from "./native-regexp.js";
|
|
21
25
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, } from "./tool-permissions.js";
|
|
26
|
+
export { evaluateFeatureFlag } from './feature-flags.js';
|
|
27
|
+
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError } from './tool-input-normalization.js';
|
|
28
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS } from './batch-tool-inputs.js';
|
|
22
29
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The ONE object-storage contract (2026-10-21): key → bytes, with an optional public-read
|
|
3
|
+
* capability. It absorbs four overlapping shapes (cms `BlobStorePort`, capability-providers
|
|
4
|
+
* `StorageProvider`, devops `ArtifactStorePort`, platform `BlobStorage`); those stay as thin views
|
|
5
|
+
* over this one until their consumers move. Providers are keyed in a `createProviderRegistry`
|
|
6
|
+
* (`contract: 'object_store'`), default `local`.
|
|
7
|
+
*
|
|
8
|
+
* Universal: types and pure helpers only, so every storage package (all of which already depend
|
|
9
|
+
* on core) can implement it. Filesystem and network adapters live in node/host packages.
|
|
10
|
+
*
|
|
11
|
+
* Data-plane calls REJECT on a storage fault (as every existing byte port did), and report absence
|
|
12
|
+
* as `null`/`false`, never as a rejection: a caller must never mistake "the store failed" for "the
|
|
13
|
+
* object is gone". Only `ObjectStoreProvider.create` returns a `ProviderResult`, so a bad or missing
|
|
14
|
+
* setting is a refusal the host can report, not a boot crash.
|
|
15
|
+
*/
|
|
16
|
+
import { type ProviderDefinition, type ProviderRegistry, type ProviderResult } from './provider-registry.js';
|
|
17
|
+
/** Registry contract name for object-store providers. */
|
|
18
|
+
export declare const OBJECT_STORE_CONTRACT = "object_store";
|
|
19
|
+
/** The built-in default provider id (local filesystem). */
|
|
20
|
+
export declare const DEFAULT_OBJECT_STORE_PROVIDER_ID = "local";
|
|
21
|
+
export interface ObjectStorePutOptions {
|
|
22
|
+
readonly contentType?: string;
|
|
23
|
+
/** Extra stored response headers (e.g. `content-disposition`); a provider may drop unsupported ones. */
|
|
24
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
25
|
+
/**
|
|
26
|
+
* Create-only write: lands `bytes` iff no object occupies `key`, atomically with respect to any
|
|
27
|
+
* other concurrent writer of the SAME key. No adapter may implement this as a separate existence
|
|
28
|
+
* check followed by a write — that pair is exactly the TOCTOU gap this flag exists to close.
|
|
29
|
+
*/
|
|
30
|
+
readonly ifAbsent?: boolean;
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
export interface ObjectStoreCallOptions {
|
|
34
|
+
readonly signal?: AbortSignal;
|
|
35
|
+
}
|
|
36
|
+
/** Anonymous read access, present only on a store whose objects are publicly reachable. */
|
|
37
|
+
export interface PublicObjectAccess {
|
|
38
|
+
/** The public URL a recipient fetches `key` at. */
|
|
39
|
+
url(required: {
|
|
40
|
+
readonly key: string;
|
|
41
|
+
}): string;
|
|
42
|
+
/** Anonymous read check: present with `bytes`, or a confirmed 404/410 when `present` is false. Errors are not absence. */
|
|
43
|
+
verify(required: {
|
|
44
|
+
readonly key: string;
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly present: boolean;
|
|
47
|
+
readonly bytes?: number;
|
|
48
|
+
}, optional?: ObjectStoreCallOptions): Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
export interface ObjectStore {
|
|
51
|
+
/** `written: false` only when `ifAbsent` was set and the key was already occupied. */
|
|
52
|
+
put(required: {
|
|
53
|
+
readonly key: string;
|
|
54
|
+
readonly bytes: Uint8Array;
|
|
55
|
+
}, optional?: ObjectStorePutOptions): Promise<{
|
|
56
|
+
readonly written: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
/** `null` only when the key is absent; every other fault rejects. */
|
|
59
|
+
get(required: {
|
|
60
|
+
readonly key: string;
|
|
61
|
+
}, optional?: ObjectStoreCallOptions): Promise<Uint8Array | null>;
|
|
62
|
+
/** Byte count without reading the payload; `null` only when the key is absent. */
|
|
63
|
+
head(required: {
|
|
64
|
+
readonly key: string;
|
|
65
|
+
}, optional?: ObjectStoreCallOptions): Promise<{
|
|
66
|
+
readonly size: number;
|
|
67
|
+
} | null>;
|
|
68
|
+
/** Idempotent — deleting an absent key is not an error. */
|
|
69
|
+
delete(required: {
|
|
70
|
+
readonly key: string;
|
|
71
|
+
}, optional?: ObjectStoreCallOptions): Promise<void>;
|
|
72
|
+
readonly publicAccess?: PublicObjectAccess;
|
|
73
|
+
}
|
|
74
|
+
/** What a host hands a provider: non-secret settings, a pre-resolved credential, and its own kit. */
|
|
75
|
+
export interface ObjectStoreCreateInput<H = unknown> {
|
|
76
|
+
readonly settings: Readonly<Record<string, string | undefined>>;
|
|
77
|
+
/** Pre-resolved by the host (`fields` non-secret, `secret` the one secret value); never looked up by the provider. */
|
|
78
|
+
readonly credential?: {
|
|
79
|
+
readonly fields: Readonly<Record<string, string | undefined>>;
|
|
80
|
+
readonly secret: string;
|
|
81
|
+
};
|
|
82
|
+
/** Host capabilities a plain-JS plugin cannot import (signing, guarded fetch, observability). */
|
|
83
|
+
readonly host: H;
|
|
84
|
+
}
|
|
85
|
+
export interface ObjectStoreProvider<H = unknown> {
|
|
86
|
+
create(required: ObjectStoreCreateInput<H>, optional?: Record<string, never>): ProviderResult<ObjectStore>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shape check for a provider value, usable as a registry `validate` or a plugin module `validate`.
|
|
90
|
+
* @returns The reason it is not a provider, or `undefined`.
|
|
91
|
+
* @complexity O(1).
|
|
92
|
+
*/
|
|
93
|
+
export declare function objectStoreProviderFault(required: {
|
|
94
|
+
readonly provider: unknown;
|
|
95
|
+
}): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* A keyed registry for object-store providers whose default is the local filesystem provider.
|
|
98
|
+
* The `local` provider itself is node-only, so the host registers it (`seed` or `register`).
|
|
99
|
+
* @complexity O(seed).
|
|
100
|
+
*/
|
|
101
|
+
export declare function createObjectStoreRegistry<H = unknown>(_required?: Record<string, never>, optional?: {
|
|
102
|
+
readonly seed?: readonly ProviderDefinition<ObjectStoreProvider<H>>[];
|
|
103
|
+
}): ProviderRegistry<ObjectStoreProvider<H>>;
|
|
104
|
+
/**
|
|
105
|
+
* An `ObjectStore` whose real store is resolved on first use — for hosts whose boot must hand out
|
|
106
|
+
* a store before the provider choice (a setting, an installed plugin) can be read. A successful
|
|
107
|
+
* load is kept; a failed load rejects that call and is retried by the next one, so a transient
|
|
108
|
+
* plugin or settings fault does not pin the store broken for the life of the process.
|
|
109
|
+
* `publicAccess` is not forwarded: a deferred store cannot know it synchronously.
|
|
110
|
+
* @complexity O(1) per call after the first successful load.
|
|
111
|
+
*/
|
|
112
|
+
export declare function deferObjectStore(required: {
|
|
113
|
+
readonly load: () => Promise<ObjectStore>;
|
|
114
|
+
}, _optional?: Record<string, never>): ObjectStore;
|
|
115
|
+
//# sourceMappingURL=object-store.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The ONE object-storage contract (2026-10-21): key → bytes, with an optional public-read
|
|
3
|
+
* capability. It absorbs four overlapping shapes (cms `BlobStorePort`, capability-providers
|
|
4
|
+
* `StorageProvider`, devops `ArtifactStorePort`, platform `BlobStorage`); those stay as thin views
|
|
5
|
+
* over this one until their consumers move. Providers are keyed in a `createProviderRegistry`
|
|
6
|
+
* (`contract: 'object_store'`), default `local`.
|
|
7
|
+
*
|
|
8
|
+
* Universal: types and pure helpers only, so every storage package (all of which already depend
|
|
9
|
+
* on core) can implement it. Filesystem and network adapters live in node/host packages.
|
|
10
|
+
*
|
|
11
|
+
* Data-plane calls REJECT on a storage fault (as every existing byte port did), and report absence
|
|
12
|
+
* as `null`/`false`, never as a rejection: a caller must never mistake "the store failed" for "the
|
|
13
|
+
* object is gone". Only `ObjectStoreProvider.create` returns a `ProviderResult`, so a bad or missing
|
|
14
|
+
* setting is a refusal the host can report, not a boot crash.
|
|
15
|
+
*/
|
|
16
|
+
import { createProviderRegistry } from './provider-registry.js';
|
|
17
|
+
/** Registry contract name for object-store providers. */
|
|
18
|
+
export const OBJECT_STORE_CONTRACT = 'object_store';
|
|
19
|
+
/** The built-in default provider id (local filesystem). */
|
|
20
|
+
export const DEFAULT_OBJECT_STORE_PROVIDER_ID = 'local';
|
|
21
|
+
/**
|
|
22
|
+
* Shape check for a provider value, usable as a registry `validate` or a plugin module `validate`.
|
|
23
|
+
* @returns The reason it is not a provider, or `undefined`.
|
|
24
|
+
* @complexity O(1).
|
|
25
|
+
*/
|
|
26
|
+
export function objectStoreProviderFault(required) {
|
|
27
|
+
const { provider } = required;
|
|
28
|
+
if (typeof provider !== 'object' || provider === null)
|
|
29
|
+
return 'an object-store provider must be an object';
|
|
30
|
+
return typeof provider.create === 'function' ? undefined : 'an object-store provider must define create()';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A keyed registry for object-store providers whose default is the local filesystem provider.
|
|
34
|
+
* The `local` provider itself is node-only, so the host registers it (`seed` or `register`).
|
|
35
|
+
* @complexity O(seed).
|
|
36
|
+
*/
|
|
37
|
+
export function createObjectStoreRegistry(_required = {}, optional = {}) {
|
|
38
|
+
return createProviderRegistry({ contract: OBJECT_STORE_CONTRACT }, {
|
|
39
|
+
defaultProviderId: DEFAULT_OBJECT_STORE_PROVIDER_ID,
|
|
40
|
+
validate: objectStoreProviderFault,
|
|
41
|
+
...(optional.seed === undefined ? {} : { seed: optional.seed }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An `ObjectStore` whose real store is resolved on first use — for hosts whose boot must hand out
|
|
46
|
+
* a store before the provider choice (a setting, an installed plugin) can be read. A successful
|
|
47
|
+
* load is kept; a failed load rejects that call and is retried by the next one, so a transient
|
|
48
|
+
* plugin or settings fault does not pin the store broken for the life of the process.
|
|
49
|
+
* `publicAccess` is not forwarded: a deferred store cannot know it synchronously.
|
|
50
|
+
* @complexity O(1) per call after the first successful load.
|
|
51
|
+
*/
|
|
52
|
+
export function deferObjectStore(required, _optional = {}) {
|
|
53
|
+
let loaded;
|
|
54
|
+
const current = () => {
|
|
55
|
+
if (!loaded) {
|
|
56
|
+
const pending = required.load();
|
|
57
|
+
loaded = pending;
|
|
58
|
+
pending.catch(() => { if (loaded === pending)
|
|
59
|
+
loaded = undefined; });
|
|
60
|
+
}
|
|
61
|
+
return loaded;
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
put: async (input, options) => (await current()).put(input, options),
|
|
65
|
+
get: async (input, options) => (await current()).get(input, options),
|
|
66
|
+
head: async (input, options) => (await current()).head(input, options),
|
|
67
|
+
delete: async (input, options) => (await current()).delete(input, options),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=object-store.js.map
|
|
@@ -26,18 +26,23 @@ function splitAllowedOriginsEnv(config, env) {
|
|
|
26
26
|
.map((origin) => origin.trim())
|
|
27
27
|
.filter(Boolean);
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** Parses `value` as a URL, or `undefined` when it is unparseable or not HTTP(S). Shared by
|
|
30
|
+
* allow-list parsing and the Origin check so both agree on what counts as a web origin. */
|
|
31
|
+
function parseHttpUrl(value) {
|
|
31
32
|
let parsed;
|
|
32
33
|
try {
|
|
33
|
-
parsed = new URL(
|
|
34
|
+
parsed = new URL(value);
|
|
34
35
|
}
|
|
35
36
|
catch {
|
|
36
37
|
return undefined;
|
|
37
38
|
}
|
|
38
39
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
39
40
|
return undefined;
|
|
40
|
-
return parsed
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
/** Normalizes one entry to a protocol://host origin; invalid or non-HTTP(S) entries are rejected. */
|
|
44
|
+
function parseAllowedOrigin(entry) {
|
|
45
|
+
return parseHttpUrl(entry)?.origin;
|
|
41
46
|
}
|
|
42
47
|
/**
|
|
43
48
|
* Parses {@link OriginValidationEnvConfig.allowedOriginsEnvVar} into normalized origins.
|
|
@@ -93,6 +98,8 @@ export function allowedBrowserPorts({ config, port, env }) {
|
|
|
93
98
|
ports.push(webPort);
|
|
94
99
|
return ports;
|
|
95
100
|
}
|
|
101
|
+
/** The loopback spellings a locally-served page's Host or Origin can carry. */
|
|
102
|
+
const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '[::1]'];
|
|
96
103
|
function headerValue(value) {
|
|
97
104
|
if (Array.isArray(value)) {
|
|
98
105
|
const first = value[0];
|
|
@@ -113,20 +120,28 @@ export function parseHostHeader({ value }) {
|
|
|
113
120
|
return null;
|
|
114
121
|
}
|
|
115
122
|
}
|
|
123
|
+
/** Parses an exact dotted-quad (four all-digit parts, each 0-255) into octets, else `undefined`.
|
|
124
|
+
* Shared by the private-range and IP-literal checks so both agree on what an IPv4 literal is.
|
|
125
|
+
* `/^\d+$/` admits only non-negative integers, so the octet range check needs only an upper bound. */
|
|
126
|
+
function parseDottedQuad(host) {
|
|
127
|
+
const parts = host.split('.');
|
|
128
|
+
if (parts.length !== 4)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (!parts.every((part) => /^\d+$/.test(part)))
|
|
131
|
+
return undefined;
|
|
132
|
+
const octets = parts.map(Number);
|
|
133
|
+
return octets.every((n) => n <= 255) ? octets : undefined;
|
|
134
|
+
}
|
|
135
|
+
/** RFC 1918 (10/8, 172.16/12, 192.168/16) or link-local (169.254/16), from the first two octets. */
|
|
136
|
+
function isPrivateIpv4Prefix([a, b]) {
|
|
137
|
+
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
|
|
138
|
+
}
|
|
116
139
|
/** Whether `hostname` is an RFC 1918 private IPv4 address (10/8, 172.16/12, 192.168/16) or link-local (169.254/16).
|
|
117
140
|
* Implements the dev-browser LAN allow-list, not SSRF blocking. Use @jini-ai/platform/net
|
|
118
141
|
* for SSRF address classification; private addresses are allowed here for local development. */
|
|
119
142
|
export function isPrivateIpv4({ hostname }) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
return false;
|
|
123
|
-
if (!parts.every((part) => /^\d+$/.test(part)))
|
|
124
|
-
return false;
|
|
125
|
-
const octets = parts.map((part) => Number(part));
|
|
126
|
-
if (!octets.every((n) => Number.isInteger(n) && n >= 0 && n <= 255))
|
|
127
|
-
return false;
|
|
128
|
-
const [a, b] = octets;
|
|
129
|
-
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254);
|
|
143
|
+
const octets = parseDottedQuad(String(hostname || ''));
|
|
144
|
+
return octets !== undefined && isPrivateIpv4Prefix(octets);
|
|
130
145
|
}
|
|
131
146
|
/** Whether `hostname` is a literal IPv4 address or bracketed IPv6 literal (as opposed to a DNS name). */
|
|
132
147
|
export function isIpLiteralHostname({ hostname }) {
|
|
@@ -135,12 +150,7 @@ export function isIpLiteralHostname({ hostname }) {
|
|
|
135
150
|
return false;
|
|
136
151
|
if (host.startsWith('[') && host.endsWith(']'))
|
|
137
152
|
return true;
|
|
138
|
-
|
|
139
|
-
if (parts.length !== 4)
|
|
140
|
-
return false;
|
|
141
|
-
if (!parts.every((part) => /^\d+$/.test(part)))
|
|
142
|
-
return false;
|
|
143
|
-
return parts.map(Number).every((n) => Number.isInteger(n) && n >= 0 && n <= 255);
|
|
153
|
+
return parseDottedQuad(host) !== undefined;
|
|
144
154
|
}
|
|
145
155
|
/** Whether `hostname` is loopback, unspecified, or a private-LAN address.
|
|
146
156
|
* Implements the dev-browser LAN allow-list, not SSRF blocking (@jini-ai/platform/net). */
|
|
@@ -159,9 +169,8 @@ export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowed
|
|
|
159
169
|
const requestHost = parseHostHeader({ value: hostHeader });
|
|
160
170
|
if (!requestHost)
|
|
161
171
|
return false;
|
|
162
|
-
const loopbackHosts = ['127.0.0.1', 'localhost', '[::1]'];
|
|
163
172
|
const explicitHosts = new Set([
|
|
164
|
-
...ports.flatMap((p) => [...
|
|
173
|
+
...ports.flatMap((p) => [...LOOPBACK_HOSTS.map((h) => `${h}:${p}`), `${bindHost}:${p}`]),
|
|
165
174
|
...configuredAllowedHosts({ origins: extraAllowedOrigins }),
|
|
166
175
|
]);
|
|
167
176
|
if (explicitHosts.has(requestHost.host))
|
|
@@ -170,37 +179,51 @@ export function isAllowedBrowserHost({ hostHeader, ports, bindHost, extraAllowed
|
|
|
170
179
|
return false;
|
|
171
180
|
return isLoopbackOrPrivateLanHost({ hostname: requestHost.hostname });
|
|
172
181
|
}
|
|
182
|
+
/** Every `scheme://host:port` origin a locally-served page can have: loopback spellings and the bind host, over both schemes. */
|
|
183
|
+
function explicitLocalOrigins(ports, bindHost) {
|
|
184
|
+
const hosts = [...LOOPBACK_HOSTS, bindHost];
|
|
185
|
+
return new Set(ports.flatMap((p) => ['http', 'https'].flatMap((s) => hosts.map((h) => `${s}://${h}:${p}`))));
|
|
186
|
+
}
|
|
187
|
+
/** The URL's port, or its scheme's default when the URL omits it (only http/https reach here). */
|
|
188
|
+
function effectivePort(url) {
|
|
189
|
+
return url.port || (url.protocol === 'https:' ? '443' : '80');
|
|
190
|
+
}
|
|
173
191
|
/** Whether an `Origin` header value names a locally-served or explicitly allow-listed origin. */
|
|
174
192
|
export function isAllowedBrowserOrigin({ origin, hostHeader, ports, bindHost, extraAllowedOrigins }) {
|
|
175
|
-
|
|
193
|
+
const originText = String(origin);
|
|
194
|
+
if (extraAllowedOrigins.includes(originText))
|
|
176
195
|
return true;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
parsedOrigin = new URL(String(origin));
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
if (parsedOrigin.protocol !== 'http:' && parsedOrigin.protocol !== 'https:')
|
|
196
|
+
const parsedOrigin = parseHttpUrl(originText);
|
|
197
|
+
if (!parsedOrigin)
|
|
185
198
|
return false;
|
|
186
199
|
const requestHost = parseHostHeader({ value: hostHeader });
|
|
187
200
|
if (!requestHost)
|
|
188
201
|
return false;
|
|
189
|
-
|
|
190
|
-
const loopbackHosts = ['127.0.0.1', 'localhost', '[::1]'];
|
|
191
|
-
const explicitOrigins = new Set(ports.flatMap((p) => [
|
|
192
|
-
...schemes.flatMap((s) => loopbackHosts.map((h) => `${s}://${h}:${p}`)),
|
|
193
|
-
...schemes.map((s) => `${s}://${bindHost}:${p}`),
|
|
194
|
-
]));
|
|
195
|
-
if (explicitOrigins.has(String(origin)))
|
|
202
|
+
if (explicitLocalOrigins(ports, bindHost).has(originText))
|
|
196
203
|
return true;
|
|
197
|
-
|
|
198
|
-
if (!ports.map(String).includes(originPort))
|
|
204
|
+
if (!ports.map(String).includes(effectivePort(parsedOrigin)))
|
|
199
205
|
return false;
|
|
200
206
|
if (parsedOrigin.hostname !== requestHost.hostname)
|
|
201
207
|
return false;
|
|
202
208
|
return isLoopbackOrPrivateLanHost({ hostname: parsedOrigin.hostname });
|
|
203
209
|
}
|
|
210
|
+
/** The decision for a request carrying no `Origin` header (as a same-origin GET subresource does). */
|
|
211
|
+
function isAllowedWithoutOrigin({ req, hostArgs, extraAllowedOrigins }) {
|
|
212
|
+
const ipOnlyExtraOrigins = extraAllowedOrigins.filter((o) => isIpLiteralHostname({ hostname: new URL(o).hostname }));
|
|
213
|
+
if (isAllowedBrowserHost({ ...hostArgs, extraAllowedOrigins: ipOnlyExtraOrigins }))
|
|
214
|
+
return true;
|
|
215
|
+
// Browsers (Firefox, Chrome) omit Origin on same-origin GET subresource
|
|
216
|
+
// requests per the Fetch spec, which makes hostname entries in the
|
|
217
|
+
// allow-list unreachable for legitimate same-origin GETs through a
|
|
218
|
+
// reverse proxy. Sec-Fetch-Site is set by the user agent and cannot be
|
|
219
|
+
// modified by JavaScript, so a value of "same-origin" attests that the
|
|
220
|
+
// request originated from the same origin as the target — a cross-site
|
|
221
|
+
// `<img>`/`<script>` exploit would carry "cross-site" instead. Only
|
|
222
|
+
// consult the broader allow-list once that signal is present.
|
|
223
|
+
if (headerValue(req.headers?.['sec-fetch-site']) !== 'same-origin')
|
|
224
|
+
return false;
|
|
225
|
+
return isAllowedBrowserHost({ ...hostArgs, extraAllowedOrigins });
|
|
226
|
+
}
|
|
204
227
|
/**
|
|
205
228
|
* The top-level same-origin gate: is `req` a request the daemon should treat
|
|
206
229
|
* as coming from one of its own locally-served (or explicitly allow-listed)
|
|
@@ -212,24 +235,8 @@ export function isLocalSameOrigin({ config, req, port, env }, { logger } = {}) {
|
|
|
212
235
|
const ports = allowedBrowserPorts({ config, port, env });
|
|
213
236
|
const bindHost = env[config.bindHostEnvVar] || '127.0.0.1';
|
|
214
237
|
const extraAllowedOrigins = configuredAllowedOrigins({ config, env }, { logger });
|
|
215
|
-
const ipOnlyExtraOrigins = extraAllowedOrigins.filter((o) => isIpLiteralHostname({ hostname: new URL(o).hostname }));
|
|
216
|
-
const localHostAllowed = isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins: ipOnlyExtraOrigins });
|
|
217
238
|
if (origin == null || origin === '') {
|
|
218
|
-
|
|
219
|
-
return true;
|
|
220
|
-
// Browsers (Firefox, Chrome) omit Origin on same-origin GET subresource
|
|
221
|
-
// requests per the Fetch spec, which makes hostname entries in the
|
|
222
|
-
// allow-list unreachable for legitimate same-origin GETs through a
|
|
223
|
-
// reverse proxy. Sec-Fetch-Site is set by the user agent and cannot be
|
|
224
|
-
// modified by JavaScript, so a value of "same-origin" attests that the
|
|
225
|
-
// request originated from the same origin as the target — a cross-site
|
|
226
|
-
// `<img>`/`<script>` exploit would carry "cross-site" instead. Only
|
|
227
|
-
// consult the broader allow-list once that signal is present.
|
|
228
|
-
const fetchSite = headerValue(req.headers?.['sec-fetch-site']);
|
|
229
|
-
if (fetchSite === 'same-origin') {
|
|
230
|
-
return isAllowedBrowserHost({ config, hostHeader: host, ports, bindHost, extraAllowedOrigins });
|
|
231
|
-
}
|
|
232
|
-
return false;
|
|
239
|
+
return isAllowedWithoutOrigin({ req, hostArgs: { config, hostHeader: host, ports, bindHost }, extraAllowedOrigins });
|
|
233
240
|
}
|
|
234
241
|
// Reverse-proxy deployments terminate the browser connection at the proxy
|
|
235
242
|
// and open a fresh upstream connection to the daemon. The Host header the
|
|
@@ -5,8 +5,8 @@ export type HttpRequest = {
|
|
|
5
5
|
method: "POST" | "GET" | "HEAD" | "PUT" | "PATCH" | "DELETE";
|
|
6
6
|
url: string;
|
|
7
7
|
headers: Readonly<Record<string, string>>;
|
|
8
|
-
/** Preserve
|
|
9
|
-
body?: string;
|
|
8
|
+
/** Preserve signed text and raw binary uploads without base64 expansion or UTF-8 corruption. */
|
|
9
|
+
body?: string | Uint8Array;
|
|
10
10
|
/** Whole-operation deadline, including DNS, redirects and body reads. Off by default.
|
|
11
11
|
* Retry/backoff belongs to the caller; its deadline can cover the whole retry operation. */
|
|
12
12
|
totalDeadlineMs?: number;
|
package/dist/primitives/index.js
CHANGED