@jini-ai/core 0.4.1 → 0.4.3
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 +8 -0
- package/README.md +19 -1
- package/dist/agent-tools.d.ts +1 -0
- package/dist/approval.d.ts +134 -0
- package/dist/approval.js +141 -0
- package/dist/fetch/cache.d.ts +129 -0
- package/dist/fetch/cache.js +243 -0
- package/dist/fetch/index.d.ts +5 -0
- package/dist/fetch/index.js +3 -0
- package/dist/fetch/types.d.ts +93 -0
- package/dist/fetch/types.js +2 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/model-facing-tool-errors.d.ts +152 -0
- package/dist/model-facing-tool-errors.js +124 -0
- package/dist/native-regexp.d.ts +10 -0
- package/dist/native-regexp.js +2 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/sse.d.ts +60 -0
- package/dist/primitives/sse.js +122 -0
- package/dist/redact.js +4 -3
- package/dist/registration-kit.d.ts +13 -0
- package/dist/registration-kit.js +12 -0
- package/dist/settings/agent-tools.d.ts +10 -0
- package/dist/settings/agent-tools.js +204 -0
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +157 -0
- package/dist/settings/agent-writable-preferences.d.ts +67 -0
- package/dist/settings/agent-writable-preferences.js +132 -0
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +83 -0
- package/dist/settings/change-feed.js +61 -0
- package/dist/settings/definitions-dispatch.d.ts +56 -0
- package/dist/settings/definitions-dispatch.js +86 -0
- package/dist/settings/dictionaries/index.d.ts +48 -0
- package/dist/settings/dictionaries/index.js +69 -0
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
- package/dist/settings/ensure-definitions.d.ts +101 -0
- package/dist/settings/ensure-definitions.js +101 -0
- package/dist/settings/errors.d.ts +42 -0
- package/dist/settings/errors.js +46 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +4 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +40 -0
- package/dist/settings/index.js +37 -0
- package/dist/settings/ports.d.ts +118 -0
- package/dist/settings/ports.js +2 -0
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +54 -0
- package/dist/settings/purge-service.js +106 -0
- package/dist/settings/repo.memory.d.ts +119 -0
- package/dist/settings/repo.memory.js +199 -0
- package/dist/settings/settings.d.ts +139 -0
- package/dist/settings/settings.js +259 -0
- package/dist/settings/tool-registrations.d.ts +54 -0
- package/dist/settings/tool-registrations.js +250 -0
- package/dist/settings/types.d.ts +125 -0
- package/dist/settings/types.js +3 -0
- package/dist/settings/ui-tab-definitions.d.ts +41 -0
- package/dist/settings/ui-tab-definitions.js +148 -0
- package/dist/settings/write-service.d.ts +320 -0
- package/dist/settings/write-service.js +683 -0
- package/dist/surface-exchanges.d.ts +209 -0
- package/dist/surface-exchanges.js +55 -0
- package/dist/text/escape.d.ts +20 -0
- package/dist/text/escape.js +23 -0
- package/dist/text/index.d.ts +1 -0
- package/dist/text/index.js +1 -0
- package/dist/tool-metadata.d.ts +71 -0
- package/dist/tool-metadata.js +56 -0
- package/dist/tool-permissions.d.ts +76 -0
- package/dist/tool-permissions.js +57 -0
- package/dist/tool-registry.d.ts +19 -1
- package/dist/tool-registry.js +44 -0
- package/package.json +79 -4
- package/plugin-directory.d.ts +32 -0
- package/plugin-directory.js +140 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — pending release
|
|
4
|
+
|
|
5
|
+
- Add source-backed `./plugin-directory` publisher identity and live metadata lookup. Warn and omit invalid advisory keywords; scope legacy `unknown/<name>` IDs to each plugin family, with optional family selection for ambiguous lookups.
|
|
6
|
+
|
|
7
|
+
- Own generic settings at `./settings` and its optional Express adapter at `./settings/express`; keep both out of the root runtime closure. Move the existing permission helper into core, preserving the CMS command denial class through re-export.
|
|
8
|
+
|
|
9
|
+
- Own the shared surface-exchange interfaces and reserved parameter constants, formerly declared in daemon. Add the `SurfaceAskThenReport` port so UI can consume the existing daemon lifecycle without depending on its runtime.
|
|
10
|
+
|
|
3
11
|
## 0.4.1 — 2026-10-05
|
|
4
12
|
|
|
5
13
|
- New `readToolLimit({ input, max, fallback }, { key })`: an absent limit is `fallback`, one above `max` is capped at `max`, and a non-integer or one below 1 throws `ToolInputError` ("'limit' must be an integer between 1 and <max>").
|
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ set of tokens, and fails to typecheck if any pack's declared dependency is left
|
|
|
15
15
|
npm install @jini-ai/core
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The root has no runtime dependencies and never loads settings or Express. `express` is an optional
|
|
19
|
+
peer used only by `@jini-ai/core/settings/express`; install it in hosts mounting those routes.
|
|
19
20
|
|
|
20
21
|
## What you get
|
|
21
22
|
|
|
@@ -93,6 +94,7 @@ Tool handlers receive required execution context first and optional `{ emitSurfa
|
|
|
93
94
|
|
|
94
95
|
| subpath | what's behind it |
|
|
95
96
|
|---|---|
|
|
97
|
+
| `./fetch` | Framework-neutral query contracts and `FetchQueryCache`; inject transport callbacks, `Clock`, scheduler and connectivity/focus ports. No React, DOM or TanStack dependency. |
|
|
96
98
|
| `.` | The public surface above: tokens, packs, daemon composition, tool registry (descriptors only), principal, redact, auth primitives. |
|
|
97
99
|
| `./composition` | **Not part of the public contract.** Exposes `authorizeToolInvocation` (resolves a registry's tool and runs its authorization gate — a security-sensitive value export, but one that only ever hands back a handler alongside an `'allow'` decision) plus type-only `AnyPack`/`RequiredTokenIds`/`MissingTokenIds` re-derivation helpers. Its sole intended consumers are `@jini-ai/daemon`'s `ToolExecutor` and `@jini-ai/server`'s `createLocalNodeDaemon`. Node's resolver will happily let any consumer import it — the boundary is enforced by `scripts/check-engine-boundaries.ts`, not by the exports map — so external adopters should treat it as off-limits regardless. |
|
|
98
100
|
|
|
@@ -239,3 +241,19 @@ Node or reading a working directory. Equality counts as contained. Directory
|
|
|
239
241
|
prefix siblings and traversal escapes are rejected; a legal `..name` segment is
|
|
240
242
|
allowed. Windows hosts pass `{ caseSensitive: false, separator: "\\\\" }`; POSIX hosts
|
|
241
243
|
retain the default slash separator so backslashes in directory names remain literal.
|
|
244
|
+
|
|
245
|
+
## Generic settings
|
|
246
|
+
|
|
247
|
+
`@jini-ai/core/settings` owns scoped definitions, defaults, values, revisions, change feeds,
|
|
248
|
+
boot-time definition registration, dictionaries and agent tools. Hosts inject settings storage,
|
|
249
|
+
active-principal lookup, authorization, clocks and IDs; no CMS or database adapter is required.
|
|
250
|
+
The keys, scopes, permissions and tool IDs are unchanged by the relocation.
|
|
251
|
+
|
|
252
|
+
`@jini-ai/core/settings/express` exports `registerSettingsRoutes`, HTTP policy contracts and
|
|
253
|
+
`createCmsSettingsService` / `createCmsSettingsChangeFeed` (factory names retained for compatibility).
|
|
254
|
+
Hosts provide route paths, readiness, principal resolution, scheduler, service and feed ports.
|
|
255
|
+
Registration returns a disposer that closes active feeds. The universal settings entry never
|
|
256
|
+
loads this adapter; neither entry is re-exported by the kernel root.
|
|
257
|
+
|
|
258
|
+
The former `@jini-ai/cms/settings` and `@jini-ai/cms/settings/express` entries re-export these
|
|
259
|
+
owners for one release. New consumers should import the core subpaths directly.
|
package/dist/agent-tools.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type AgentToolSideEffect = "none" | "mutates-durable-state" | "deletes-du
|
|
|
16
16
|
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
17
17
|
/** Structural catalog contract. Domains may require inputSchema or tighten their own metadata. */
|
|
18
18
|
export interface AgentToolDefinition {
|
|
19
|
+
readonly metadata?: import('./tool-metadata.js').ToolMetadata;
|
|
19
20
|
name: string;
|
|
20
21
|
description: string;
|
|
21
22
|
sideEffects: AgentToolSideEffect;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type HumanConfirmer } from './registration-kit.js';
|
|
2
|
+
import type { ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolRegistration } from './tool-registry.js';
|
|
3
|
+
/** A host describes the action; core owns the decision lifecycle, never the card format. */
|
|
4
|
+
export interface ApprovalPlan<TDescription> {
|
|
5
|
+
readonly ask: boolean;
|
|
6
|
+
readonly description: TDescription;
|
|
7
|
+
/** Only reusable escalation grants receive a key. Irreversible actions omit it. */
|
|
8
|
+
readonly rememberKey?: string;
|
|
9
|
+
}
|
|
10
|
+
export type ApprovalAnswer = {
|
|
11
|
+
confirmed: true;
|
|
12
|
+
choice?: string;
|
|
13
|
+
} | {
|
|
14
|
+
confirmed: false;
|
|
15
|
+
reason: 'declined' | 'expired' | 'abandoned';
|
|
16
|
+
};
|
|
17
|
+
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
18
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect. */
|
|
19
|
+
export declare function resolveApproval({ signal, askHuman }: {
|
|
20
|
+
signal: AbortSignal;
|
|
21
|
+
askHuman: () => Promise<ApprovalAnswer>;
|
|
22
|
+
}, _optional?: {}): Promise<ApprovalAnswer>;
|
|
23
|
+
export interface RememberedApprovalPort {
|
|
24
|
+
has(required: {
|
|
25
|
+
ctx: ToolExecutionContext;
|
|
26
|
+
key: string;
|
|
27
|
+
}): Promise<boolean>;
|
|
28
|
+
grant(required: {
|
|
29
|
+
ctx: ToolExecutionContext;
|
|
30
|
+
key: string;
|
|
31
|
+
confirmer: HumanConfirmer;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface ApprovalSteps<TPrepared, TDescription> {
|
|
35
|
+
prepare(required: {
|
|
36
|
+
ctx: ToolExecutionContext;
|
|
37
|
+
}, optional: ToolExecutionOptions): Promise<TPrepared>;
|
|
38
|
+
describe(required: {
|
|
39
|
+
ctx: ToolExecutionContext;
|
|
40
|
+
prepared: TPrepared;
|
|
41
|
+
}, optional: ToolExecutionOptions): Promise<ApprovalPlan<TDescription>> | ApprovalPlan<TDescription>;
|
|
42
|
+
askHuman(required: {
|
|
43
|
+
ctx: ToolExecutionContext;
|
|
44
|
+
description: TDescription;
|
|
45
|
+
}, optional: ToolExecutionOptions): Promise<ApprovalAnswer>;
|
|
46
|
+
run(required: {
|
|
47
|
+
ctx: ToolExecutionContext;
|
|
48
|
+
prepared: TPrepared;
|
|
49
|
+
confirmer: HumanConfirmer;
|
|
50
|
+
choice?: string | undefined;
|
|
51
|
+
}, optional: ToolExecutionOptions): Promise<unknown>;
|
|
52
|
+
/** Abort may happen before preparation; the lifecycle forwards that missing value explicitly. */
|
|
53
|
+
notConfirmed(required: {
|
|
54
|
+
reason: 'declined' | 'expired' | 'abandoned';
|
|
55
|
+
prepared?: TPrepared | undefined;
|
|
56
|
+
}): unknown;
|
|
57
|
+
}
|
|
58
|
+
/** Extend the branded human-confirm owner with descriptions and optional remembered grants.
|
|
59
|
+
* Inputs and actor identity are detached before any asynchronous port runs. No input field can
|
|
60
|
+
* approve an action; only the host transport or the bound grant store can answer. Abort checks
|
|
61
|
+
* bracket preparation, storage and the human wait, so a completed run never performs an effect.
|
|
62
|
+
* @param steps Domain preparation/effects and the host's description/transport ports.
|
|
63
|
+
* @param optional.remembered A persisted escalation store; omission requires a fresh decision.
|
|
64
|
+
* @returns A handler accepted by the registration kit's human-only actor rule.
|
|
65
|
+
* @throws Port failures propagate; a failed store cannot silently authorize an action.
|
|
66
|
+
* @complexity O(input size) for the snapshot plus bounded port calls and the human wait.
|
|
67
|
+
* @example createApprovalHandler({ prepare, describe, askHuman, run, notConfirmed });
|
|
68
|
+
*/
|
|
69
|
+
export declare function createApprovalHandler<TPrepared, TDescription>(steps: ApprovalSteps<TPrepared, TDescription>, { remembered }?: {
|
|
70
|
+
remembered?: RememberedApprovalPort;
|
|
71
|
+
}): ToolHandler;
|
|
72
|
+
/** Page-local proposal transport for a browser that must return before its explicit reply arrives.
|
|
73
|
+
* It carries no principal or tool authorization: the host's execute port must re-check access.
|
|
74
|
+
* The private action is independent of the rendered snapshot; replies consume it before awaiting
|
|
75
|
+
* so double clicks/replayed replies cannot execute twice. Abort clears the proposal and listener.
|
|
76
|
+
* @returns An observable transport for an app dialog and its authenticated reply entry point.
|
|
77
|
+
* @complexity O(description size) to detach the proposal; O(listeners) to publish a change.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createApprovalProposalStore<TDescription>({ responseTool, messages }: {
|
|
80
|
+
responseTool: string;
|
|
81
|
+
messages: {
|
|
82
|
+
closed: string;
|
|
83
|
+
pending: string;
|
|
84
|
+
missing: string;
|
|
85
|
+
};
|
|
86
|
+
}, { createId }?: {
|
|
87
|
+
createId?: () => string;
|
|
88
|
+
}): {
|
|
89
|
+
getSnapshot: (_required?: {}, _optional?: {}) => {
|
|
90
|
+
pending: {
|
|
91
|
+
approvalId: string;
|
|
92
|
+
description: TDescription;
|
|
93
|
+
} | null;
|
|
94
|
+
error: string | null;
|
|
95
|
+
};
|
|
96
|
+
subscribe: ({ listener }: {
|
|
97
|
+
listener: () => void;
|
|
98
|
+
}, _optional?: {}) => () => void;
|
|
99
|
+
propose({ description, execute, signal }: {
|
|
100
|
+
description: TDescription;
|
|
101
|
+
execute: () => Promise<unknown>;
|
|
102
|
+
signal: AbortSignal;
|
|
103
|
+
}, _optional?: {}): {
|
|
104
|
+
status: "approval_required";
|
|
105
|
+
approvalId: string;
|
|
106
|
+
description: TDescription;
|
|
107
|
+
responseTool: string;
|
|
108
|
+
};
|
|
109
|
+
respond({ approvalId, approved }: {
|
|
110
|
+
approvalId: string;
|
|
111
|
+
approved: boolean;
|
|
112
|
+
}, _optional?: {}): Promise<unknown>;
|
|
113
|
+
};
|
|
114
|
+
/** Apply a host classification through the same handler owner used by domain plans.
|
|
115
|
+
* @returns The registration with one approval handler; authorization policy is preserved.
|
|
116
|
+
*/
|
|
117
|
+
export declare function applyApprovalPolicy<TDescription>({ registration, classify, asks, describe, askHuman, notConfirmed }: {
|
|
118
|
+
registration: ToolRegistration;
|
|
119
|
+
classify(required: {
|
|
120
|
+
input: unknown;
|
|
121
|
+
}): string;
|
|
122
|
+
asks(required: {
|
|
123
|
+
class: string;
|
|
124
|
+
}): boolean;
|
|
125
|
+
describe(required: {
|
|
126
|
+
ctx: ToolExecutionContext;
|
|
127
|
+
class: string;
|
|
128
|
+
}): Promise<TDescription>;
|
|
129
|
+
askHuman: ApprovalSteps<string, TDescription>['askHuman'];
|
|
130
|
+
notConfirmed: ApprovalSteps<string, TDescription>['notConfirmed'];
|
|
131
|
+
}, optional?: {
|
|
132
|
+
remembered?: RememberedApprovalPort;
|
|
133
|
+
}): ToolRegistration;
|
|
134
|
+
//# sourceMappingURL=approval.d.ts.map
|
package/dist/approval.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { humanConfirmedHandler } from './registration-kit.js';
|
|
2
|
+
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
3
|
+
* An answer never revives an aborted operation. Transport failures propagate without an effect. */
|
|
4
|
+
export async function resolveApproval({ signal, askHuman }, _optional = {}) {
|
|
5
|
+
if (signal.aborted)
|
|
6
|
+
return { confirmed: false, reason: 'abandoned' };
|
|
7
|
+
let abandon;
|
|
8
|
+
const aborted = new Promise(resolve => { abandon = () => resolve({ confirmed: false, reason: 'abandoned' }); });
|
|
9
|
+
signal.addEventListener('abort', abandon, { once: true });
|
|
10
|
+
try {
|
|
11
|
+
const answer = await Promise.race([askHuman(), aborted]);
|
|
12
|
+
return signal.aborted ? { confirmed: false, reason: 'abandoned' } : answer;
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
signal.removeEventListener('abort', abandon);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Extend the branded human-confirm owner with descriptions and optional remembered grants.
|
|
19
|
+
* Inputs and actor identity are detached before any asynchronous port runs. No input field can
|
|
20
|
+
* approve an action; only the host transport or the bound grant store can answer. Abort checks
|
|
21
|
+
* bracket preparation, storage and the human wait, so a completed run never performs an effect.
|
|
22
|
+
* @param steps Domain preparation/effects and the host's description/transport ports.
|
|
23
|
+
* @param optional.remembered A persisted escalation store; omission requires a fresh decision.
|
|
24
|
+
* @returns A handler accepted by the registration kit's human-only actor rule.
|
|
25
|
+
* @throws Port failures propagate; a failed store cannot silently authorize an action.
|
|
26
|
+
* @complexity O(input size) for the snapshot plus bounded port calls and the human wait.
|
|
27
|
+
* @example createApprovalHandler({ prepare, describe, askHuman, run, notConfirmed });
|
|
28
|
+
*/
|
|
29
|
+
export function createApprovalHandler(steps, { remembered } = {}) {
|
|
30
|
+
return humanConfirmedHandler({
|
|
31
|
+
prepare: async ({ ctx }, options = {}) => {
|
|
32
|
+
const snapshot = { ...ctx, input: structuredClone(ctx.input), principal: { ...ctx.principal }, run: { ...ctx.run } };
|
|
33
|
+
if (snapshot.signal.aborted)
|
|
34
|
+
return { ctx: snapshot };
|
|
35
|
+
const value = await steps.prepare({ ctx: snapshot }, options);
|
|
36
|
+
if (snapshot.signal.aborted)
|
|
37
|
+
return { ctx: snapshot, value };
|
|
38
|
+
const plan = await steps.describe({ ctx: snapshot, prepared: value }, options);
|
|
39
|
+
return { ctx: snapshot, value, plan };
|
|
40
|
+
},
|
|
41
|
+
askHuman: async ({ prepared }, options = {}) => {
|
|
42
|
+
const { ctx, plan, value } = prepared;
|
|
43
|
+
const abandoned = () => ({ confirmed: false, result: steps.notConfirmed({ reason: 'abandoned', prepared: value }) });
|
|
44
|
+
if (ctx.signal.aborted || !plan)
|
|
45
|
+
return abandoned();
|
|
46
|
+
if (!plan.ask)
|
|
47
|
+
return { confirmed: true };
|
|
48
|
+
if (plan.rememberKey !== undefined && remembered) {
|
|
49
|
+
const approved = await remembered.has({ ctx, key: plan.rememberKey });
|
|
50
|
+
if (ctx.signal.aborted)
|
|
51
|
+
return abandoned();
|
|
52
|
+
if (approved)
|
|
53
|
+
return { confirmed: true };
|
|
54
|
+
}
|
|
55
|
+
const answer = await resolveApproval({ signal: ctx.signal, askHuman: () => steps.askHuman({ ctx, description: plan.description }, options) });
|
|
56
|
+
if (ctx.signal.aborted)
|
|
57
|
+
return abandoned();
|
|
58
|
+
if (answer.confirmed !== true)
|
|
59
|
+
return { confirmed: false, result: steps.notConfirmed({ reason: answer.reason, prepared: value }) };
|
|
60
|
+
prepared.choice = answer.choice;
|
|
61
|
+
if (plan.rememberKey !== undefined && remembered) {
|
|
62
|
+
await remembered.grant({ ctx, key: plan.rememberKey, confirmer: { id: ctx.principal.id, kind: 'user' } });
|
|
63
|
+
}
|
|
64
|
+
if (ctx.signal.aborted)
|
|
65
|
+
return abandoned();
|
|
66
|
+
return { confirmed: true };
|
|
67
|
+
},
|
|
68
|
+
run: ({ prepared }, options = {}) => {
|
|
69
|
+
const { ctx, value, choice } = prepared;
|
|
70
|
+
if (ctx.signal.aborted)
|
|
71
|
+
return Promise.resolve(steps.notConfirmed({ reason: 'abandoned', prepared: value }));
|
|
72
|
+
return steps.run({ ctx, prepared: value, confirmer: { id: ctx.principal.id, kind: 'user' }, choice }, options);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Page-local proposal transport for a browser that must return before its explicit reply arrives.
|
|
77
|
+
* It carries no principal or tool authorization: the host's execute port must re-check access.
|
|
78
|
+
* The private action is independent of the rendered snapshot; replies consume it before awaiting
|
|
79
|
+
* so double clicks/replayed replies cannot execute twice. Abort clears the proposal and listener.
|
|
80
|
+
* @returns An observable transport for an app dialog and its authenticated reply entry point.
|
|
81
|
+
* @complexity O(description size) to detach the proposal; O(listeners) to publish a change.
|
|
82
|
+
*/
|
|
83
|
+
export function createApprovalProposalStore({ responseTool, messages }, { createId = () => crypto.randomUUID() } = {}) {
|
|
84
|
+
let snapshot = { pending: null, error: null };
|
|
85
|
+
let action = null;
|
|
86
|
+
let detach = null;
|
|
87
|
+
const listeners = new Set();
|
|
88
|
+
const publish = (next) => { snapshot = next; for (const listener of listeners)
|
|
89
|
+
listener(); };
|
|
90
|
+
const clear = () => {
|
|
91
|
+
detach?.();
|
|
92
|
+
detach = null;
|
|
93
|
+
action = null;
|
|
94
|
+
publish({ pending: null, error: null });
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
getSnapshot: (_required = {}, _optional = {}) => snapshot,
|
|
98
|
+
subscribe: ({ listener }, _optional = {}) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
99
|
+
propose({ description, execute, signal }, _optional = {}) {
|
|
100
|
+
if (signal.aborted)
|
|
101
|
+
throw new Error(messages.closed);
|
|
102
|
+
if (snapshot.pending)
|
|
103
|
+
throw new Error(messages.pending);
|
|
104
|
+
const frozenDescription = structuredClone(description);
|
|
105
|
+
const approvalId = createId();
|
|
106
|
+
action = { approvalId, execute, signal };
|
|
107
|
+
signal.addEventListener('abort', clear, { once: true });
|
|
108
|
+
detach = () => signal.removeEventListener('abort', clear);
|
|
109
|
+
publish({ pending: { approvalId, description: structuredClone(frozenDescription) }, error: null });
|
|
110
|
+
return { status: 'approval_required', approvalId, description: frozenDescription, responseTool };
|
|
111
|
+
},
|
|
112
|
+
async respond({ approvalId, approved }, _optional = {}) {
|
|
113
|
+
if (!action || action.approvalId !== approvalId)
|
|
114
|
+
throw new Error(messages.missing);
|
|
115
|
+
const prepared = action;
|
|
116
|
+
clear();
|
|
117
|
+
const answer = await resolveApproval({ signal: prepared.signal, askHuman: async () => approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' } });
|
|
118
|
+
if (!answer.confirmed)
|
|
119
|
+
return { status: 'declined' };
|
|
120
|
+
try {
|
|
121
|
+
return await prepared.execute();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
publish({ ...snapshot, error: error instanceof Error ? error.message : String(error) });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Apply a host classification through the same handler owner used by domain plans.
|
|
131
|
+
* @returns The registration with one approval handler; authorization policy is preserved.
|
|
132
|
+
*/
|
|
133
|
+
export function applyApprovalPolicy({ registration, classify, asks, describe, askHuman, notConfirmed }, optional = {}) {
|
|
134
|
+
return { ...registration, handler: createApprovalHandler({
|
|
135
|
+
prepare: async ({ ctx }) => classify({ input: ctx.input }),
|
|
136
|
+
describe: async ({ ctx, prepared }) => ({ ask: asks({ class: prepared }), description: await describe({ ctx, class: prepared }) }),
|
|
137
|
+
askHuman, notConfirmed,
|
|
138
|
+
run: ({ ctx }, options) => registration.handler(ctx, options),
|
|
139
|
+
}, optional) };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=approval.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Clock } from '../primitives/time.js';
|
|
2
|
+
import type { QueryKey } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ten seconds covers reading, closing and reopening a record or bouncing between screens
|
|
5
|
+
* without paying for another identical request. Five seconds was too short for that read
|
|
6
|
+
* cycle; thirty seconds exposed more other-operator staleness than the conservative initial
|
|
7
|
+
* shared default should allow. There is no default polling/focus refresh, so this bounds
|
|
8
|
+
* the extra exposure on a quick revisit. Write invalidation bypasses freshness; reads that
|
|
9
|
+
* must be fresh on every mount can choose staleTime: 0 without lowering every query default.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_STALE_TIME = 10000;
|
|
12
|
+
/** Minimum time an unobserved entry is kept; both adapters retain max(this, staleTime). */
|
|
13
|
+
export declare const IDLE_RETENTION: number;
|
|
14
|
+
export interface QueryCacheSchedulerPort {
|
|
15
|
+
schedule(required: {
|
|
16
|
+
callback: () => void;
|
|
17
|
+
delayMs: number;
|
|
18
|
+
}): unknown;
|
|
19
|
+
cancel(required: {
|
|
20
|
+
handle: unknown;
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
/** Host connectivity and focus signals; the cache itself has no DOM dependency. */
|
|
24
|
+
export interface FetchQueryEnvironmentPort {
|
|
25
|
+
isOnline(): boolean;
|
|
26
|
+
subscribeOnline(required: {
|
|
27
|
+
listener: (online: boolean) => void;
|
|
28
|
+
}): () => void;
|
|
29
|
+
subscribeFocus(required: {
|
|
30
|
+
listener: () => void;
|
|
31
|
+
}): () => void;
|
|
32
|
+
}
|
|
33
|
+
export interface FetchQueryCacheOptions {
|
|
34
|
+
clock?: Clock;
|
|
35
|
+
scheduler?: QueryCacheSchedulerPort;
|
|
36
|
+
environment?: FetchQueryEnvironmentPort;
|
|
37
|
+
}
|
|
38
|
+
export interface QueryState {
|
|
39
|
+
data: unknown;
|
|
40
|
+
error: Error | null;
|
|
41
|
+
status: 'pending' | 'success' | 'error';
|
|
42
|
+
isFetching: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface QueryObserver {
|
|
45
|
+
fetch: () => Promise<unknown>;
|
|
46
|
+
staleTime: number;
|
|
47
|
+
focus: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface CacheEntry {
|
|
50
|
+
key: QueryKey;
|
|
51
|
+
state: QueryState;
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
invalidated: boolean;
|
|
54
|
+
replacement: number;
|
|
55
|
+
pending: Promise<unknown> | undefined;
|
|
56
|
+
pendingRead: {
|
|
57
|
+
fetch: () => Promise<unknown>;
|
|
58
|
+
staleTime: number;
|
|
59
|
+
} | undefined;
|
|
60
|
+
refreshAfterPending: boolean;
|
|
61
|
+
listeners: Set<() => void>;
|
|
62
|
+
observers: Set<QueryObserver>;
|
|
63
|
+
retention: number;
|
|
64
|
+
eviction: unknown;
|
|
65
|
+
}
|
|
66
|
+
export declare function toQueryError(value: unknown): Error;
|
|
67
|
+
/** One provider-owned cache. Requests are shared by structural primitive keys.
|
|
68
|
+
* Failed reads retain earlier data; replacement writes supersede older requests.
|
|
69
|
+
* Idle entries live for at least five minutes or their declared freshness period. */
|
|
70
|
+
export declare class FetchQueryCache {
|
|
71
|
+
private readonly entries;
|
|
72
|
+
private disposed;
|
|
73
|
+
private readonly clock;
|
|
74
|
+
private readonly scheduler;
|
|
75
|
+
private readonly environment;
|
|
76
|
+
private online;
|
|
77
|
+
private readonly onlineWaiters;
|
|
78
|
+
private releaseEnvironment;
|
|
79
|
+
constructor(_required?: Record<string, never>, { clock, scheduler, environment }?: FetchQueryCacheOptions);
|
|
80
|
+
entry({ key }: {
|
|
81
|
+
key: QueryKey;
|
|
82
|
+
}, { staleTime }?: {
|
|
83
|
+
staleTime?: number;
|
|
84
|
+
}): CacheEntry;
|
|
85
|
+
fresh({ entry, staleTime }: {
|
|
86
|
+
entry: CacheEntry;
|
|
87
|
+
staleTime: number;
|
|
88
|
+
}): boolean;
|
|
89
|
+
subscribe({ entry, listener }: {
|
|
90
|
+
entry: CacheEntry;
|
|
91
|
+
listener: () => void;
|
|
92
|
+
}): () => void;
|
|
93
|
+
observe({ entry, observer }: {
|
|
94
|
+
entry: CacheEntry;
|
|
95
|
+
observer: QueryObserver;
|
|
96
|
+
}): () => void;
|
|
97
|
+
/** Cache hits respect freshness; explicit refreshes restart background reads.
|
|
98
|
+
* Initial reads still deduplicate because no earlier successful data exists to refresh. */
|
|
99
|
+
load<T>({ entry, fetch, staleTime }: {
|
|
100
|
+
entry: CacheEntry;
|
|
101
|
+
fetch: () => Promise<T>;
|
|
102
|
+
staleTime: number;
|
|
103
|
+
}, { force }?: {
|
|
104
|
+
force?: boolean;
|
|
105
|
+
}): Promise<T>;
|
|
106
|
+
replace({ entry, data }: {
|
|
107
|
+
entry: CacheEntry;
|
|
108
|
+
data: unknown;
|
|
109
|
+
}): void;
|
|
110
|
+
invalidate({ prefix }: {
|
|
111
|
+
prefix: QueryKey;
|
|
112
|
+
}): void;
|
|
113
|
+
onFocus(): void;
|
|
114
|
+
/** Execute a read/write once connectivity returns; failures are not retried.
|
|
115
|
+
* @param required.run Operation to invoke online.
|
|
116
|
+
* @returns The operation's original result or rejection. */
|
|
117
|
+
runOnline<T>({ run }: {
|
|
118
|
+
run: () => Promise<T>;
|
|
119
|
+
}): Promise<T>;
|
|
120
|
+
/** Stop timers on provider disposal; reactivate for React's effect replay. */
|
|
121
|
+
activate(): void;
|
|
122
|
+
dispose(): void;
|
|
123
|
+
private onOnline;
|
|
124
|
+
private refresh;
|
|
125
|
+
private publish;
|
|
126
|
+
private cancelEviction;
|
|
127
|
+
private scheduleEviction;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=cache.d.ts.map
|