@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/agent-tools.d.ts +1 -1
  3. package/dist/approval.d.ts +15 -0
  4. package/dist/approval.js +87 -22
  5. package/dist/batch-tool-inputs.d.ts +147 -0
  6. package/dist/batch-tool-inputs.js +257 -0
  7. package/dist/feature-flags.d.ts +21 -0
  8. package/dist/feature-flags.js +17 -0
  9. package/dist/fetch/index.d.ts +2 -1
  10. package/dist/fetch/index.js +1 -0
  11. package/dist/fetch/loader-read.d.ts +16 -0
  12. package/dist/fetch/loader-read.js +22 -0
  13. package/dist/fetch/types.d.ts +12 -2
  14. package/dist/gated-mutations/gateway.js +32 -27
  15. package/dist/index.d.ts +9 -1
  16. package/dist/index.js +8 -1
  17. package/dist/object-store.d.ts +115 -0
  18. package/dist/object-store.js +70 -0
  19. package/dist/origin-validation.js +64 -57
  20. package/dist/primitives/http.d.ts +2 -2
  21. package/dist/primitives/index.d.ts +1 -0
  22. package/dist/primitives/index.js +1 -0
  23. package/dist/primitives/paths.js +17 -10
  24. package/dist/primitives/tasks.d.ts +10 -0
  25. package/dist/primitives/tasks.js +8 -0
  26. package/dist/provider-catalog.d.ts +76 -0
  27. package/dist/provider-catalog.js +6 -0
  28. package/dist/provider-registry.d.ts +144 -0
  29. package/dist/provider-registry.js +138 -0
  30. package/dist/records.d.ts +24 -0
  31. package/dist/records.js +25 -0
  32. package/dist/redact.d.ts +4 -0
  33. package/dist/redact.js +57 -34
  34. package/dist/registration-kit.d.ts +7 -4
  35. package/dist/registration-kit.js +81 -62
  36. package/dist/settings/agent-value-write-tools.d.ts +19 -0
  37. package/dist/settings/agent-value-write-tools.js +131 -70
  38. package/dist/settings/ensure-definitions.d.ts +4 -2
  39. package/dist/settings/ensure-definitions.js +79 -63
  40. package/dist/settings/express/cms-adapter.d.ts +2 -2
  41. package/dist/settings/express/cms-adapter.js +34 -22
  42. package/dist/settings/express/http-request.d.ts +33 -0
  43. package/dist/settings/express/http-request.js +49 -0
  44. package/dist/settings/express/http.d.ts +2 -2
  45. package/dist/settings/express/http.js +16 -39
  46. package/dist/settings/express/stream.d.ts +2 -2
  47. package/dist/settings/express/stream.js +49 -27
  48. package/dist/settings/repo.memory.js +7 -4
  49. package/dist/settings/settings.js +54 -60
  50. package/dist/settings/tool-registrations.d.ts +2 -3
  51. package/dist/settings/tool-registrations.js +64 -39
  52. package/dist/settings/write-service.d.ts +19 -0
  53. package/dist/settings/write-service.js +34 -0
  54. package/dist/target-scope.d.ts +52 -0
  55. package/dist/target-scope.js +82 -0
  56. package/dist/text/escape.js +2 -2
  57. package/dist/text/sanitize-untrusted-text.d.ts +5 -0
  58. package/dist/text/sanitize-untrusted-text.js +55 -35
  59. package/dist/tool-exposure.d.ts +79 -0
  60. package/dist/tool-exposure.js +163 -0
  61. package/dist/tool-input-errors.d.ts +23 -0
  62. package/dist/tool-input-errors.js +24 -0
  63. package/dist/tool-input-normalization.d.ts +37 -0
  64. package/dist/tool-input-normalization.js +188 -0
  65. package/dist/tool-metadata-types.d.ts +26 -0
  66. package/dist/tool-metadata-types.js +2 -0
  67. package/dist/tool-metadata.d.ts +2 -23
  68. package/dist/tool-registry.d.ts +38 -22
  69. package/dist/tool-registry.js +35 -20
  70. package/package.json +20 -2
  71. package/plugin-directory.js +72 -33
@@ -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
@@ -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: Pick<CachedLoaderOptions<T>, "key" | "fetch">, optional?: Omit<CachedLoaderOptions<T>, "key" | "fetch">) => CachedLoader<T>;
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
- const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.readPermission });
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
- const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.mutatePermission });
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
- const authResult = await authorizeForHooks({ deps, hooks, principalId, permission: hooks.mutatePermission });
86
- if (!authResult.allowed) {
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
- if (!latestRecord || latestRecord.status !== "redeemed")
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
- /** Normalizes one entry to a protocol://host origin; invalid or non-HTTP(S) entries are rejected. */
30
- function parseAllowedOrigin(entry) {
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(entry);
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.origin;
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 parts = String(hostname || '').split('.');
121
- if (parts.length !== 4)
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
- const parts = host.split('.');
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) => [...loopbackHosts.map((h) => `${h}:${p}`), `${bindHost}:${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
- if (extraAllowedOrigins.includes(String(origin)))
193
+ const originText = String(origin);
194
+ if (extraAllowedOrigins.includes(originText))
176
195
  return true;
177
- let parsedOrigin;
178
- try {
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
- const schemes = ['http', 'https'];
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
- const originPort = parsedOrigin.port || (parsedOrigin.protocol === 'https:' ? '443' : '80');
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
- if (localHostAllowed)
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 the signed bytes: re-serialization between signing and sending changes the signature. */
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;
@@ -7,4 +7,5 @@ export type { DecodedSseEvent, DecodedSseFrame, SseFramingOptions } from './sse.
7
7
  export * from './logger.js';
8
8
  export * from './result.js';
9
9
  export * from './paths.js';
10
+ export * from './tasks.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@ export { decodeSseFrames, decodeSseStream, parseSseRecord } from './sse.js';
6
6
  export * from './logger.js';
7
7
  export * from './result.js';
8
8
  export * from './paths.js';
9
+ export * from './tasks.js';
9
10
  //# sourceMappingURL=index.js.map