@irogane/kaji 0.2.0-beta.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -551
- package/dist/index.d.mts +253 -0
- package/dist/index.mjs +440 -0
- package/package.json +28 -166
- package/contracts/README.md +0 -14
- package/contracts/beta-core-v1.json +0 -52
- package/contracts/cli/init-cases-v1.json +0 -27
- package/contracts/errors/error-codes.json +0 -48
- package/contracts/errors/integration-recovery-v1.json +0 -127
- package/contracts/errors/provider-normalization.json +0 -111
- package/contracts/events/conformance-invalid.json +0 -20
- package/contracts/events/conformance.json +0 -511
- package/contracts/events/new-kaji-event-v1.schema.json +0 -1021
- package/contracts/events/stored-kaji-event-v1.schema.json +0 -1025
- package/contracts/feature-tiers-v1.json +0 -488
- package/contracts/integrations/abi-index-v1.json +0 -8
- package/contracts/integrations/conformance-invalid.json +0 -443
- package/contracts/integrations/conformance-valid.json +0 -121
- package/contracts/integrations/copy-provenance-v1.schema.json +0 -61
- package/contracts/integrations/echo-tool-abi-v1.json +0 -37
- package/contracts/integrations/github-api-conformance-v1.json +0 -644
- package/contracts/integrations/github-tool-abi-typescript-v1.json +0 -369
- package/contracts/integrations/github-tool-abi-v1.json +0 -146
- package/contracts/integrations/gmail-api-conformance-v1.json +0 -750
- package/contracts/integrations/gmail-tool-abi-v1.json +0 -62
- package/contracts/integrations/index.schema.json +0 -37
- package/contracts/integrations/manifest.schema.json +0 -119
- package/contracts/parity/expected-normalized.json +0 -4906
- package/contracts/parity/scenarios.json +0 -100
- package/contracts/parity/scenarios.schema.json +0 -214
- package/contracts/providers/cost-conformance.json +0 -112
- package/contracts/release/github-proof-v1.schema.json +0 -138
- package/contracts/release/gmail-proof-v1.schema.json +0 -138
- package/contracts/release/kaji-ts-consumer-handoff-v1.schema.json +0 -1289
- package/contracts/release/publisher-identity-receipt-v1.schema.json +0 -319
- package/contracts/release/typescript-onboarding-evidence-v1.schema.json +0 -740
- package/contracts/tools/conformance-invalid.json +0 -271
- package/contracts/tools/conformance-valid.json +0 -78
- package/contracts/tools/tool-schema-v1.schema.json +0 -18
- package/dist/anthropic.cjs +0 -1231
- package/dist/anthropic.cjs.map +0 -1
- package/dist/anthropic.d.cts +0 -26
- package/dist/anthropic.d.ts +0 -26
- package/dist/anthropic.js +0 -270
- package/dist/anthropic.js.map +0 -1
- package/dist/auth.cjs +0 -1507
- package/dist/auth.cjs.map +0 -1
- package/dist/auth.d.cts +0 -129
- package/dist/auth.d.ts +0 -129
- package/dist/auth.js +0 -1039
- package/dist/auth.js.map +0 -1
- package/dist/base-B9FRMcP8.d.cts +0 -140
- package/dist/base-nHQd1VtS.d.ts +0 -140
- package/dist/chunk-AAM33KAO.js +0 -4367
- package/dist/chunk-AAM33KAO.js.map +0 -1
- package/dist/chunk-KAJ6BM64.js +0 -153
- package/dist/chunk-KAJ6BM64.js.map +0 -1
- package/dist/chunk-KCAXIOZS.js +0 -308
- package/dist/chunk-KCAXIOZS.js.map +0 -1
- package/dist/chunk-LSJ4AVO2.js +0 -243
- package/dist/chunk-LSJ4AVO2.js.map +0 -1
- package/dist/chunk-TM7ZGOJX.js +0 -716
- package/dist/chunk-TM7ZGOJX.js.map +0 -1
- package/dist/cli/bin.d.ts +0 -2
- package/dist/cli/bin.js +0 -13
- package/dist/cli/bin.js.map +0 -1
- package/dist/cli/chunk-2RCWPRVY.js +0 -6277
- package/dist/cli/chunk-2RCWPRVY.js.map +0 -1
- package/dist/cli/chunk-SEBX54TR.js +0 -681
- package/dist/cli/chunk-SEBX54TR.js.map +0 -1
- package/dist/cli/index.d.ts +0 -232
- package/dist/cli/index.js +0 -11
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/init-worker.d.ts +0 -2
- package/dist/cli/init-worker.js +0 -18
- package/dist/cli/init-worker.js.map +0 -1
- package/dist/cli/integration-copy-worker.js +0 -48
- package/dist/cli/integration-copy-worker.js.map +0 -1
- package/dist/cli/package-entry-cjs.cjs +0 -21
- package/dist/cli/package-entry-cjs.cjs.map +0 -1
- package/dist/cli/package-entry-cjs.d.cts +0 -2
- package/dist/cli/package-entry.d.ts +0 -2
- package/dist/cli/package-entry.js +0 -13
- package/dist/cli/package-entry.js.map +0 -1
- package/dist/context-BaFHrQHv.d.cts +0 -21
- package/dist/context-BaFHrQHv.d.ts +0 -21
- package/dist/context-C-YPY-GS.d.cts +0 -1538
- package/dist/context-C-YPY-GS.d.ts +0 -1538
- package/dist/index.cjs +0 -12346
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1560
- package/dist/index.d.ts +0 -1560
- package/dist/index.js +0 -7852
- package/dist/index.js.map +0 -1
- package/dist/integrations/github.cjs +0 -2092
- package/dist/integrations/github.cjs.map +0 -1
- package/dist/integrations/github.d.cts +0 -21
- package/dist/integrations/github.d.ts +0 -21
- package/dist/integrations/github.js +0 -2088
- package/dist/integrations/github.js.map +0 -1
- package/dist/integrations.cjs +0 -3370
- package/dist/integrations.cjs.map +0 -1
- package/dist/integrations.d.cts +0 -202
- package/dist/integrations.d.ts +0 -202
- package/dist/integrations.js +0 -2650
- package/dist/integrations.js.map +0 -1
- package/dist/observability-Cj--OkME.d.cts +0 -96
- package/dist/observability-Cj--OkME.d.ts +0 -96
- package/dist/openai.cjs +0 -1235
- package/dist/openai.cjs.map +0 -1
- package/dist/openai.d.cts +0 -32
- package/dist/openai.d.ts +0 -32
- package/dist/openai.js +0 -272
- package/dist/openai.js.map +0 -1
- package/dist/testing.cjs +0 -554
- package/dist/testing.cjs.map +0 -1
- package/dist/testing.d.cts +0 -43
- package/dist/testing.d.ts +0 -43
- package/dist/testing.js +0 -118
- package/dist/testing.js.map +0 -1
- package/registry/echo/index.ts +0 -53
- package/registry/echo/manifest.json +0 -53
- package/registry/github/LICENSE +0 -105
- package/registry/github/client.ts +0 -1727
- package/registry/github/index.ts +0 -263
- package/registry/github/manifest.json +0 -227
- package/registry/github/owner-fixtures.json +0 -10
- package/registry/github/tests/github.test.ts +0 -32
- package/registry/gmail/LICENSE +0 -105
- package/registry/gmail/client.ts +0 -548
- package/registry/gmail/index.ts +0 -165
- package/registry/gmail/manifest.json +0 -102
- package/registry/gmail/owner-fixtures.json +0 -10
- package/registry/gmail/tests/gmail.test.ts +0 -32
- package/registry/index.json +0 -21
- package/registry/index.schema.json +0 -37
- package/registry/schema.json +0 -119
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
//#region src/execution-context.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The information a capability's `execute` function needs to run safely.
|
|
4
|
+
*
|
|
5
|
+
* This stays intentionally small: no store, registry, session, or engine
|
|
6
|
+
* reference. Identity is exactly `principalId`. Cancellation uses the
|
|
7
|
+
* platform's native `AbortSignal` instead of a Kaji-specific token.
|
|
8
|
+
*/
|
|
9
|
+
type ExecutionContext = {
|
|
10
|
+
readonly principalId: string;
|
|
11
|
+
readonly idempotencyKey: string;
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/schema.d.ts
|
|
16
|
+
/**
|
|
17
|
+
* Kaji stays validator-neutral. `input` needs only a `parse` method that
|
|
18
|
+
* returns validated input or throws, per docs/api.md: "input needs only a
|
|
19
|
+
* parser that either returns validated input or throws." Zod, Valibot (via
|
|
20
|
+
* `.parse`), and other libraries satisfy this shape without an adapter.
|
|
21
|
+
*/
|
|
22
|
+
type InputParser<Output> = {
|
|
23
|
+
parse(input: unknown): Output;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/capability.d.ts
|
|
27
|
+
/**
|
|
28
|
+
* A request to authorize or require approval for one capability execution.
|
|
29
|
+
* Kept inline rather than named because it is not part of the store contract
|
|
30
|
+
* and does not need to be constructed by callers.
|
|
31
|
+
*/
|
|
32
|
+
type PrincipalRequest<Input> = {
|
|
33
|
+
readonly principalId: string;
|
|
34
|
+
readonly input: Input;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Declares one application action: its validated input contract,
|
|
38
|
+
* authorization/approval rules, and the function Kaji will eventually
|
|
39
|
+
* execute. A capability does not execute itself; `feat/execute` owns that.
|
|
40
|
+
*
|
|
41
|
+
* `Input` is the parser's returned/validated type, matching what
|
|
42
|
+
* `authorize`, `approval`, and `execute` receive.
|
|
43
|
+
*/
|
|
44
|
+
type CapabilityDefinition<Input, Result> = {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly input: InputParser<Input>;
|
|
47
|
+
readonly authorize: (request: PrincipalRequest<Input>) => boolean | Promise<boolean>;
|
|
48
|
+
readonly approval?: (request: PrincipalRequest<Input>) => boolean;
|
|
49
|
+
readonly execute: (input: Input, context: ExecutionContext) => Result | Promise<Result>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The declared capability, as frozen by docs/api.md.
|
|
53
|
+
*
|
|
54
|
+
* Only `name` is public today. `Input` and `Result` are retained as type
|
|
55
|
+
* parameters (unused at this field) because a future `Kaji.execute()` needs
|
|
56
|
+
* `Capability<Input, Result>` to recover both types; removing them now would
|
|
57
|
+
* force `feat/execute` to redesign this type. The unused-type-parameter
|
|
58
|
+
* warning this produces is suppressed in .oxlintrc.json for this file.
|
|
59
|
+
*/
|
|
60
|
+
type Capability<Input, Result> = {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Defines one application action that Kaji can later execute safely.
|
|
65
|
+
*
|
|
66
|
+
* `capability()` is the single canonical constructor. It validates its own
|
|
67
|
+
* declaration (name, hooks) at construction time; it never invokes
|
|
68
|
+
* `authorize`, `approval`, or `execute` itself.
|
|
69
|
+
*/
|
|
70
|
+
declare function capability<Input, Result>(definition: CapabilityDefinition<Input, Result>): Capability<Input, Result>;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/approval.d.ts
|
|
73
|
+
/**
|
|
74
|
+
* What Kaji tells an application's `approve` handler about one request
|
|
75
|
+
* requiring approval, per docs/api.md's inline `createKaji()` contract.
|
|
76
|
+
* Not exported: docs/api.md keeps this shape inline in `KajiOptions`
|
|
77
|
+
* rather than naming it, so there is no caller need to import it directly.
|
|
78
|
+
*/
|
|
79
|
+
type ApprovalRequest = {
|
|
80
|
+
readonly capability: string;
|
|
81
|
+
readonly principalId: string;
|
|
82
|
+
readonly input: unknown;
|
|
83
|
+
readonly idempotencyKey: string;
|
|
84
|
+
};
|
|
85
|
+
/** What an application's `approve` handler returns for one request. */
|
|
86
|
+
type ApprovalDecision = {
|
|
87
|
+
readonly approved: boolean;
|
|
88
|
+
readonly evidence?: unknown;
|
|
89
|
+
};
|
|
90
|
+
type ApproveHandler = (request: ApprovalRequest) => ApprovalDecision | Promise<ApprovalDecision>;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/execution-request.d.ts
|
|
93
|
+
/**
|
|
94
|
+
* One caller's request to run a capability, as frozen by docs/api.md.
|
|
95
|
+
* `principalId` and `idempotencyKey` are supplied by the caller — Kaji
|
|
96
|
+
* never infers either from process state (docs/invariants.md
|
|
97
|
+
* "Principal is explicit").
|
|
98
|
+
*/
|
|
99
|
+
type ExecutionRequest = {
|
|
100
|
+
readonly input: unknown;
|
|
101
|
+
readonly principalId: string;
|
|
102
|
+
readonly idempotencyKey: string;
|
|
103
|
+
readonly signal?: AbortSignal;
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/execution-result.d.ts
|
|
107
|
+
/**
|
|
108
|
+
* The minimal facts Kaji knows about one governed execution.
|
|
109
|
+
*
|
|
110
|
+
* This is deliberately not a general event or audit record: it carries only
|
|
111
|
+
* what a caller needs to identify the execution and detect conflicting
|
|
112
|
+
* idempotency key reuse. See docs/invariants.md "Evidence is minimal".
|
|
113
|
+
*/
|
|
114
|
+
type ExecutionEvidence = {
|
|
115
|
+
readonly executionId: string;
|
|
116
|
+
readonly capability: string;
|
|
117
|
+
readonly principalId: string;
|
|
118
|
+
readonly idempotencyKey: string;
|
|
119
|
+
readonly inputFingerprint: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* The explicit outcome of one governed execution, as frozen by docs/api.md.
|
|
123
|
+
*
|
|
124
|
+
* `unknown` is distinct from `failed`: it means the capability's side effect
|
|
125
|
+
* may have committed but Kaji cannot prove the final result, so callers
|
|
126
|
+
* must not treat it as an ordinary retryable failure.
|
|
127
|
+
*/
|
|
128
|
+
type ExecutionResult<Result> = {
|
|
129
|
+
readonly status: "succeeded";
|
|
130
|
+
readonly result: Result;
|
|
131
|
+
readonly evidence: ExecutionEvidence;
|
|
132
|
+
} | {
|
|
133
|
+
readonly status: "denied" | "rejected" | "failed" | "cancelled";
|
|
134
|
+
readonly error: unknown;
|
|
135
|
+
readonly evidence: ExecutionEvidence;
|
|
136
|
+
} | {
|
|
137
|
+
readonly status: "unknown";
|
|
138
|
+
readonly error: unknown;
|
|
139
|
+
readonly evidence: ExecutionEvidence;
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/execution-store.d.ts
|
|
143
|
+
/**
|
|
144
|
+
* The material identity of one intended operation: which capability, which
|
|
145
|
+
* principal, which caller-supplied idempotency key, and a fingerprint of
|
|
146
|
+
* the validated input. The store uses this tuple to detect duplicate
|
|
147
|
+
* claims and conflicting reuse; it never sees the raw input or the
|
|
148
|
+
* capability's callbacks.
|
|
149
|
+
*/
|
|
150
|
+
type ExecutionClaim = {
|
|
151
|
+
readonly capability: string;
|
|
152
|
+
readonly principalId: string;
|
|
153
|
+
readonly idempotencyKey: string;
|
|
154
|
+
readonly inputFingerprint: string;
|
|
155
|
+
};
|
|
156
|
+
/** The terminal or in-flight record a store persists for one execution. */
|
|
157
|
+
type StoredExecution = ExecutionResult<unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* The outcome of attempting to claim one execution.
|
|
160
|
+
*
|
|
161
|
+
* `"existing"` covers both an execution still running and one already
|
|
162
|
+
* settled: its `outcome` promise resolves once the claim is recorded,
|
|
163
|
+
* so a caller that must not duplicate a side effect can simply await it
|
|
164
|
+
* instead of the store exposing separate running/completed/unknown states.
|
|
165
|
+
*/
|
|
166
|
+
type ClaimResult = {
|
|
167
|
+
readonly status: "claimed";
|
|
168
|
+
readonly executionId: string;
|
|
169
|
+
} | {
|
|
170
|
+
readonly status: "existing";
|
|
171
|
+
readonly outcome: Promise<StoredExecution>;
|
|
172
|
+
} | {
|
|
173
|
+
readonly status: "conflict";
|
|
174
|
+
readonly executionId: string;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Persists execution identity, idempotency claims, and minimal outcome
|
|
178
|
+
* evidence. A store does not execute capabilities, enforce authorization
|
|
179
|
+
* or approval, or decide retries — it only answers whether an execution
|
|
180
|
+
* may begin and records what happened once it is known.
|
|
181
|
+
*
|
|
182
|
+
* `claim()` must be atomic enough that concurrent claims for the same
|
|
183
|
+
* identity produce exactly one `"claimed"` result; see docs/invariants.md
|
|
184
|
+
* "Store semantics are atomic where required".
|
|
185
|
+
*/
|
|
186
|
+
type ExecutionStore = {
|
|
187
|
+
claim(claim: ExecutionClaim): Promise<ClaimResult>;
|
|
188
|
+
record(execution: StoredExecution): Promise<void>;
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/kaji.d.ts
|
|
192
|
+
/** Stable application configuration for the executor, per docs/api.md. */
|
|
193
|
+
type KajiOptions = {
|
|
194
|
+
readonly store: ExecutionStore;
|
|
195
|
+
readonly approve?: ApproveHandler;
|
|
196
|
+
readonly timeoutMs?: number;
|
|
197
|
+
};
|
|
198
|
+
/** The Kaji executor, as frozen by docs/api.md. */
|
|
199
|
+
type Kaji = {
|
|
200
|
+
execute<Input, Result>(capability: Capability<Input, Result>, request: ExecutionRequest): Promise<ExecutionResult<Result>>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Creates the Kaji executor: the one canonical entry point for running a
|
|
204
|
+
* capability. `kaji.execute()` validates the request, claims its
|
|
205
|
+
* idempotency key, authorizes and approves it, calls the capability at
|
|
206
|
+
* most once, and records the explicit outcome.
|
|
207
|
+
*/
|
|
208
|
+
declare function createKaji(options: KajiOptions): Kaji;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/errors.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Marks a capability's `execute` failure as definitively known rather than
|
|
213
|
+
* ambiguous: application code is asserting that no side effect committed,
|
|
214
|
+
* so Kaji may settle `failed` instead of its safe `unknown` default. See
|
|
215
|
+
* docs/invariants.md "Ambiguous outcomes are preserved" and "Known failure
|
|
216
|
+
* is explicit".
|
|
217
|
+
*
|
|
218
|
+
* `knownFailure()` is the only way to opt out of `unknown`. Kaji never
|
|
219
|
+
* infers this from an error's class, message, or status code — only
|
|
220
|
+
* application code that actually knows the remote side effect did not
|
|
221
|
+
* commit may make this claim.
|
|
222
|
+
*/
|
|
223
|
+
declare class KnownFailure extends Error {
|
|
224
|
+
constructor(cause: unknown);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Wraps `cause` so a capability's `execute` function can throw it to
|
|
228
|
+
* report an ordinary, provably non-ambiguous failure (docs/api.md).
|
|
229
|
+
*
|
|
230
|
+
* Usage:
|
|
231
|
+
* ```ts
|
|
232
|
+
* execute: async (input, context) => {
|
|
233
|
+
* try {
|
|
234
|
+
* return await provider.charge(input);
|
|
235
|
+
* } catch (cause) {
|
|
236
|
+
* if (isDefinitelyDeclined(cause)) throw knownFailure(cause);
|
|
237
|
+
* throw cause; // stays unknown: the side effect may have committed
|
|
238
|
+
* }
|
|
239
|
+
* },
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
declare function knownFailure(cause: unknown): KnownFailure;
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/memory-store.d.ts
|
|
245
|
+
/**
|
|
246
|
+
* A process-local reference `ExecutionStore`. It has no external
|
|
247
|
+
* persistence, no cross-process visibility, and no durability guarantee —
|
|
248
|
+
* it exists for development, tests, and examples, but honors the same
|
|
249
|
+
* claim/record contract a durable store must honor.
|
|
250
|
+
*/
|
|
251
|
+
declare function memoryStore(): ExecutionStore;
|
|
252
|
+
//#endregion
|
|
253
|
+
export { type Capability, type ClaimResult, type ExecutionClaim, type ExecutionContext, type ExecutionEvidence, type ExecutionRequest, type ExecutionResult, type ExecutionStore, type StoredExecution, capability, createKaji, knownFailure, memoryStore };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
//#region src/capability.ts
|
|
2
|
+
/**
|
|
3
|
+
* Defines one application action that Kaji can later execute safely.
|
|
4
|
+
*
|
|
5
|
+
* `capability()` is the single canonical constructor. It validates its own
|
|
6
|
+
* declaration (name, hooks) at construction time; it never invokes
|
|
7
|
+
* `authorize`, `approval`, or `execute` itself.
|
|
8
|
+
*/
|
|
9
|
+
function capability(definition) {
|
|
10
|
+
const name = definition.name;
|
|
11
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error("capability() requires a non-empty name.");
|
|
12
|
+
if (typeof definition.authorize !== "function") throw new Error("capability() requires an authorize function.");
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
name,
|
|
15
|
+
input: definition.input,
|
|
16
|
+
authorize: definition.authorize,
|
|
17
|
+
approval: definition.approval,
|
|
18
|
+
execute: definition.execute
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recovers the full declaration from a `Capability`. The public
|
|
23
|
+
* `Capability<Input, Result>` type only advertises `name`, but the object
|
|
24
|
+
* `capability()` returns always carries `input`/`authorize`/`approval`/
|
|
25
|
+
* `execute` — the executor is their one legitimate reader.
|
|
26
|
+
*/
|
|
27
|
+
function capabilityDefinition(capability$1) {
|
|
28
|
+
return capability$1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/approval.ts
|
|
33
|
+
/**
|
|
34
|
+
* Resolves whether a request that requires approval may proceed.
|
|
35
|
+
*
|
|
36
|
+
* Approval is fail-closed (docs/invariants.md "Approval is fail-closed"):
|
|
37
|
+
* a missing handler, an invalid decision shape, a rejected decision, or a
|
|
38
|
+
* thrown/rejected handler all prevent execution. Only an explicit
|
|
39
|
+
* `{ approved: true }` allows the caller to continue.
|
|
40
|
+
*/
|
|
41
|
+
async function resolveApproval(approve, request) {
|
|
42
|
+
if (approve === void 0) return {
|
|
43
|
+
approved: false,
|
|
44
|
+
error: /* @__PURE__ */ new Error("Approval is required but no approve handler was configured.")
|
|
45
|
+
};
|
|
46
|
+
let decision;
|
|
47
|
+
try {
|
|
48
|
+
decision = await approve(request);
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
return {
|
|
51
|
+
approved: false,
|
|
52
|
+
error: cause
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (typeof decision !== "object" || decision === null || typeof decision.approved !== "boolean") return {
|
|
56
|
+
approved: false,
|
|
57
|
+
error: /* @__PURE__ */ new Error("approve() returned an invalid decision; expected { approved: boolean }.")
|
|
58
|
+
};
|
|
59
|
+
if (!decision.approved) return {
|
|
60
|
+
approved: false,
|
|
61
|
+
error: /* @__PURE__ */ new Error("Approval was not granted.")
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
approved: true,
|
|
65
|
+
evidence: decision.evidence
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/effective-signal.ts
|
|
71
|
+
/**
|
|
72
|
+
* Combines the caller's signal (if any) with Kaji's optional `timeoutMs`
|
|
73
|
+
* into one effective `AbortSignal`, using only native `AbortSignal`/
|
|
74
|
+
* `AbortController` primitives (docs/invariants.md "Native primitives
|
|
75
|
+
* first"). `cleanup()` must be called once execution finishes so the
|
|
76
|
+
* timeout timer does not keep the process alive.
|
|
77
|
+
*/
|
|
78
|
+
function effectiveSignal(callerSignal, timeoutMs) {
|
|
79
|
+
if (timeoutMs === void 0) return {
|
|
80
|
+
signal: callerSignal ?? new AbortController().signal,
|
|
81
|
+
cleanup: () => {}
|
|
82
|
+
};
|
|
83
|
+
const timeoutController = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
85
|
+
return {
|
|
86
|
+
signal: callerSignal === void 0 ? timeoutController.signal : AbortSignal.any([callerSignal, timeoutController.signal]),
|
|
87
|
+
cleanup: () => clearTimeout(timer)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/fingerprint.ts
|
|
93
|
+
/**
|
|
94
|
+
* Thrown when a value falls outside the durable value domain — plain
|
|
95
|
+
* JSON-shaped data — and cannot be fingerprinted deterministically.
|
|
96
|
+
* Capability input is schema-validated application data, so this is the
|
|
97
|
+
* natural boundary: it excludes functions, symbols, and other values a
|
|
98
|
+
* schema parser would not normally produce.
|
|
99
|
+
*/
|
|
100
|
+
var UnfingerprintableValueError = class extends Error {
|
|
101
|
+
constructor(reason) {
|
|
102
|
+
super(`Cannot fingerprint input: ${reason}`);
|
|
103
|
+
this.name = "UnfingerprintableValueError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Produces a deterministic string for one capability's validated input.
|
|
108
|
+
*
|
|
109
|
+
* Object key order must not affect the result — two semantically identical
|
|
110
|
+
* inputs must fingerprint identically regardless of how their keys were
|
|
111
|
+
* constructed — so this canonicalizes objects by sorting keys before
|
|
112
|
+
* serializing, rather than relying on `JSON.stringify`'s insertion order.
|
|
113
|
+
*/
|
|
114
|
+
function fingerprintInput(capability$1, input) {
|
|
115
|
+
return `${capability$1}:${canonicalize(input)}`;
|
|
116
|
+
}
|
|
117
|
+
function canonicalize(value) {
|
|
118
|
+
if (value === null) return "null";
|
|
119
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
120
|
+
if (typeof value === "number") {
|
|
121
|
+
if (!Number.isFinite(value)) throw new UnfingerprintableValueError(`non-finite number ${value}`);
|
|
122
|
+
return JSON.stringify(value);
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === "boolean") return JSON.stringify(value);
|
|
125
|
+
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
|
|
126
|
+
if (typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
|
|
127
|
+
throw new UnfingerprintableValueError(`unsupported value of type ${typeof value}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/errors.ts
|
|
132
|
+
/**
|
|
133
|
+
* Thrown only when Kaji cannot establish the execution boundary at all —
|
|
134
|
+
* an invalid executor configuration, or a store failure before any
|
|
135
|
+
* execution record exists. Every other outcome (denied, rejected, failed,
|
|
136
|
+
* cancelled, unknown) is an ordinary `ExecutionResult`, not a thrown error;
|
|
137
|
+
* see docs/api.md "Results and outcomes".
|
|
138
|
+
*/
|
|
139
|
+
var KajiConfigurationError = class extends Error {
|
|
140
|
+
constructor(message, options) {
|
|
141
|
+
super(message, options);
|
|
142
|
+
this.name = "KajiConfigurationError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Marks a capability's `execute` failure as definitively known rather than
|
|
147
|
+
* ambiguous: application code is asserting that no side effect committed,
|
|
148
|
+
* so Kaji may settle `failed` instead of its safe `unknown` default. See
|
|
149
|
+
* docs/invariants.md "Ambiguous outcomes are preserved" and "Known failure
|
|
150
|
+
* is explicit".
|
|
151
|
+
*
|
|
152
|
+
* `knownFailure()` is the only way to opt out of `unknown`. Kaji never
|
|
153
|
+
* infers this from an error's class, message, or status code — only
|
|
154
|
+
* application code that actually knows the remote side effect did not
|
|
155
|
+
* commit may make this claim.
|
|
156
|
+
*/
|
|
157
|
+
var KnownFailure = class extends Error {
|
|
158
|
+
constructor(cause) {
|
|
159
|
+
super("Capability execution failed with a definitively known outcome.", { cause });
|
|
160
|
+
this.name = "KnownFailure";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Wraps `cause` so a capability's `execute` function can throw it to
|
|
165
|
+
* report an ordinary, provably non-ambiguous failure (docs/api.md).
|
|
166
|
+
*
|
|
167
|
+
* Usage:
|
|
168
|
+
* ```ts
|
|
169
|
+
* execute: async (input, context) => {
|
|
170
|
+
* try {
|
|
171
|
+
* return await provider.charge(input);
|
|
172
|
+
* } catch (cause) {
|
|
173
|
+
* if (isDefinitelyDeclined(cause)) throw knownFailure(cause);
|
|
174
|
+
* throw cause; // stays unknown: the side effect may have committed
|
|
175
|
+
* }
|
|
176
|
+
* },
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
function knownFailure(cause) {
|
|
180
|
+
return new KnownFailure(cause);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/schema.ts
|
|
185
|
+
/**
|
|
186
|
+
* Validates unknown input against a capability's parser.
|
|
187
|
+
*
|
|
188
|
+
* This is the one canonical validation primitive in Kaji. The future
|
|
189
|
+
* executor calls this exact function so there is never a second path that
|
|
190
|
+
* could let invalid input reach application code.
|
|
191
|
+
*/
|
|
192
|
+
function validateInput(parser, input) {
|
|
193
|
+
return parser.parse(input);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/execute.ts
|
|
198
|
+
/**
|
|
199
|
+
* Runs the one canonical Kaji execution path for one request, in the exact
|
|
200
|
+
* order docs/api.md freezes: validate, claim idempotency, authorize,
|
|
201
|
+
* obtain approval when needed, execute, and record the explicit outcome.
|
|
202
|
+
*
|
|
203
|
+
* Claiming before authorization is deliberate: claiming an idempotency key
|
|
204
|
+
* is bookkeeping, not the side effect docs/invariants.md's "Authorization
|
|
205
|
+
* precedes side effects" protects. It lets a denied or rejected request
|
|
206
|
+
* still occupy and settle its idempotency key, so a caller who retries a
|
|
207
|
+
* denied request with the same key observes the same denial instead of a
|
|
208
|
+
* fresh authorization check racing a duplicate claim.
|
|
209
|
+
*/
|
|
210
|
+
async function executeCapability(dependencies, capability$1, request) {
|
|
211
|
+
const definition = capabilityDefinition(capability$1);
|
|
212
|
+
const principalId = requireNonEmptyString(request.principalId, "principalId");
|
|
213
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
214
|
+
const { signal, cleanup } = effectiveSignal(request.signal, dependencies.timeoutMs);
|
|
215
|
+
try {
|
|
216
|
+
if (signal.aborted) return cancelled(capability$1.name, principalId, idempotencyKey);
|
|
217
|
+
const input = validateInput(definition.input, request.input);
|
|
218
|
+
const inputFingerprint = fingerprintInput(capability$1.name, input);
|
|
219
|
+
const claimResult = await claim(dependencies.store, {
|
|
220
|
+
capability: capability$1.name,
|
|
221
|
+
principalId,
|
|
222
|
+
idempotencyKey,
|
|
223
|
+
inputFingerprint
|
|
224
|
+
});
|
|
225
|
+
if (claimResult.status === "conflict") return conflict(capability$1.name, principalId, idempotencyKey, claimResult.executionId, inputFingerprint);
|
|
226
|
+
if (claimResult.status === "existing") return await claimResult.outcome;
|
|
227
|
+
const evidence = {
|
|
228
|
+
executionId: claimResult.executionId,
|
|
229
|
+
capability: capability$1.name,
|
|
230
|
+
principalId,
|
|
231
|
+
idempotencyKey,
|
|
232
|
+
inputFingerprint
|
|
233
|
+
};
|
|
234
|
+
if (signal.aborted) return await settle(dependencies.store, cancelled(capability$1.name, principalId, idempotencyKey, evidence));
|
|
235
|
+
const authorization = await guarded(() => definition.authorize({
|
|
236
|
+
principalId,
|
|
237
|
+
input
|
|
238
|
+
}));
|
|
239
|
+
if (!authorization.ok) return await settle(dependencies.store, denied(evidence, authorization.error));
|
|
240
|
+
if (authorization.value === false) return await settle(dependencies.store, denied(evidence, /* @__PURE__ */ new Error("Authorization denied the request.")));
|
|
241
|
+
if (signal.aborted) return await settle(dependencies.store, cancelled(capability$1.name, principalId, idempotencyKey, evidence));
|
|
242
|
+
if (definition.approval?.({
|
|
243
|
+
principalId,
|
|
244
|
+
input
|
|
245
|
+
}) === true) {
|
|
246
|
+
const decision = await resolveApproval(dependencies.approve, {
|
|
247
|
+
capability: capability$1.name,
|
|
248
|
+
principalId,
|
|
249
|
+
input,
|
|
250
|
+
idempotencyKey
|
|
251
|
+
});
|
|
252
|
+
if (!decision.approved) return await settle(dependencies.store, rejected(evidence, decision.error));
|
|
253
|
+
}
|
|
254
|
+
if (signal.aborted) return await settle(dependencies.store, cancelled(capability$1.name, principalId, idempotencyKey, evidence));
|
|
255
|
+
const context = {
|
|
256
|
+
principalId,
|
|
257
|
+
idempotencyKey,
|
|
258
|
+
signal
|
|
259
|
+
};
|
|
260
|
+
const outcome = await runCapability(definition.execute, input, context, evidence);
|
|
261
|
+
return await settle(dependencies.store, outcome);
|
|
262
|
+
} finally {
|
|
263
|
+
cleanup();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function runCapability(execute, input, context, evidence) {
|
|
267
|
+
try {
|
|
268
|
+
return {
|
|
269
|
+
status: "succeeded",
|
|
270
|
+
result: await execute(input, context),
|
|
271
|
+
evidence
|
|
272
|
+
};
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
if (cause instanceof KnownFailure) return {
|
|
275
|
+
status: "failed",
|
|
276
|
+
error: cause.cause,
|
|
277
|
+
evidence
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
status: "unknown",
|
|
281
|
+
error: cause,
|
|
282
|
+
evidence
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function claim(store, executionClaim) {
|
|
287
|
+
try {
|
|
288
|
+
return await store.claim(executionClaim);
|
|
289
|
+
} catch (cause) {
|
|
290
|
+
throw new KajiConfigurationError("Execution store failed to claim the operation.", { cause });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Persists a terminal outcome and returns it. A store failure here means
|
|
295
|
+
* Kaji cannot prove the outcome was durably recorded; docs/invariants.md
|
|
296
|
+
* requires that ambiguity to surface as `unknown` rather than the
|
|
297
|
+
* capability's real outcome, since a caller could otherwise be told an
|
|
298
|
+
* action succeeded that Kaji has no durable record of.
|
|
299
|
+
*/
|
|
300
|
+
async function settle(store, outcome) {
|
|
301
|
+
try {
|
|
302
|
+
await store.record(outcome);
|
|
303
|
+
return outcome;
|
|
304
|
+
} catch (cause) {
|
|
305
|
+
return {
|
|
306
|
+
status: "unknown",
|
|
307
|
+
error: cause,
|
|
308
|
+
evidence: outcome.evidence
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function guarded(fn) {
|
|
313
|
+
try {
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
value: await fn()
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
error
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function denied(evidence, error) {
|
|
326
|
+
return {
|
|
327
|
+
status: "denied",
|
|
328
|
+
error,
|
|
329
|
+
evidence
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function rejected(evidence, error) {
|
|
333
|
+
return {
|
|
334
|
+
status: "rejected",
|
|
335
|
+
error,
|
|
336
|
+
evidence
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function conflict(capability$1, principalId, idempotencyKey, executionId, inputFingerprint) {
|
|
340
|
+
return {
|
|
341
|
+
status: "failed",
|
|
342
|
+
error: /* @__PURE__ */ new Error(`Idempotency key "${idempotencyKey}" was already used for a different request.`),
|
|
343
|
+
evidence: {
|
|
344
|
+
executionId,
|
|
345
|
+
capability: capability$1,
|
|
346
|
+
principalId,
|
|
347
|
+
idempotencyKey,
|
|
348
|
+
inputFingerprint
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function cancelled(capability$1, principalId, idempotencyKey, evidence) {
|
|
353
|
+
return {
|
|
354
|
+
status: "cancelled",
|
|
355
|
+
error: /* @__PURE__ */ new Error("Execution was cancelled before it began."),
|
|
356
|
+
evidence: evidence ?? {
|
|
357
|
+
executionId: "",
|
|
358
|
+
capability: capability$1,
|
|
359
|
+
principalId,
|
|
360
|
+
idempotencyKey,
|
|
361
|
+
inputFingerprint: ""
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function requireNonEmptyString(value, field) {
|
|
366
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new KajiConfigurationError(`ExecutionRequest.${field} must be a non-empty string.`);
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/kaji.ts
|
|
372
|
+
/**
|
|
373
|
+
* Creates the Kaji executor: the one canonical entry point for running a
|
|
374
|
+
* capability. `kaji.execute()` validates the request, claims its
|
|
375
|
+
* idempotency key, authorizes and approves it, calls the capability at
|
|
376
|
+
* most once, and records the explicit outcome.
|
|
377
|
+
*/
|
|
378
|
+
function createKaji(options) {
|
|
379
|
+
return { execute(capability$1, request) {
|
|
380
|
+
return executeCapability(options, capability$1, request);
|
|
381
|
+
} };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/memory-store.ts
|
|
386
|
+
/**
|
|
387
|
+
* A process-local reference `ExecutionStore`. It has no external
|
|
388
|
+
* persistence, no cross-process visibility, and no durability guarantee —
|
|
389
|
+
* it exists for development, tests, and examples, but honors the same
|
|
390
|
+
* claim/record contract a durable store must honor.
|
|
391
|
+
*/
|
|
392
|
+
function memoryStore() {
|
|
393
|
+
const claims = /* @__PURE__ */ new Map();
|
|
394
|
+
return {
|
|
395
|
+
async claim(claim$1) {
|
|
396
|
+
const identity = identityKey(claim$1);
|
|
397
|
+
const existing = claims.get(identity);
|
|
398
|
+
if (existing !== void 0) {
|
|
399
|
+
if (existing.claim.inputFingerprint !== claim$1.inputFingerprint) return {
|
|
400
|
+
status: "conflict",
|
|
401
|
+
executionId: existing.executionId
|
|
402
|
+
};
|
|
403
|
+
return {
|
|
404
|
+
status: "existing",
|
|
405
|
+
outcome: existing.outcome
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
let settle$1;
|
|
409
|
+
const outcome = new Promise((resolve) => {
|
|
410
|
+
settle$1 = resolve;
|
|
411
|
+
});
|
|
412
|
+
const executionId = crypto.randomUUID();
|
|
413
|
+
claims.set(identity, {
|
|
414
|
+
claim: claim$1,
|
|
415
|
+
executionId,
|
|
416
|
+
outcome,
|
|
417
|
+
settle: settle$1,
|
|
418
|
+
settled: false
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
status: "claimed",
|
|
422
|
+
executionId
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
async record(execution) {
|
|
426
|
+
const identity = identityKey(execution.evidence);
|
|
427
|
+
const entry = claims.get(identity);
|
|
428
|
+
if (entry === void 0 || entry.executionId !== execution.evidence.executionId) throw new Error(`record() called for unclaimed executionId "${execution.evidence.executionId}".`);
|
|
429
|
+
if (entry.settled) throw new Error(`record() called twice for executionId "${entry.executionId}".`);
|
|
430
|
+
entry.settled = true;
|
|
431
|
+
entry.settle(execution);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function identityKey(claim$1) {
|
|
436
|
+
return `${claim$1.capability}:${claim$1.principalId}:${claim$1.idempotencyKey}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
export { capability, createKaji, knownFailure, memoryStore };
|