@kedge/server 0.0.0-stage → 0.10.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/src/types.ts ADDED
@@ -0,0 +1,841 @@
1
+ // What the HTTP layer needs from a runtime. @kedge/runtime-cf implements
2
+ // CartGateway with one Durable Object per cart; tests can use fakes.
3
+ import type {
4
+ AccessContext,
5
+ Clock,
6
+ CustomFields,
7
+ IdGenerator,
8
+ Json,
9
+ JsonRecord,
10
+ LineInput,
11
+ LinePatch,
12
+ OrderStore,
13
+ OrderView,
14
+ Overrides,
15
+ PricedCart,
16
+ StoreProof,
17
+ TaxContext,
18
+ } from "@kedge/core";
19
+
20
+ export interface CartView {
21
+ id: string;
22
+ status: "active" | "completing" | "completed";
23
+ currency_code: string;
24
+ email: string | null;
25
+ revision: number;
26
+ locale: string;
27
+ /** The cart's `order` custom fields, projected for the reader. */
28
+ custom: JsonRecord;
29
+ consents: Record<string, { revision_id: string }>;
30
+ /** "unavailable" when a plugin the cart needs can't price it now; totals are then null (§1.1). */
31
+ pricing: "ok" | "unavailable";
32
+ lines: {
33
+ id: string;
34
+ variant_id: string;
35
+ kind: string;
36
+ parent_line_id: string | null;
37
+ quantity_mode: "own" | "per_parent";
38
+ quantity_per_parent: number | null;
39
+ period_start: number | null;
40
+ period_end: number | null;
41
+ custom: JsonRecord;
42
+ sku: string;
43
+ title: string;
44
+ unit_amount: number | null;
45
+ quantity: number;
46
+ line_total: number | null;
47
+ charge_class: "revenue" | "deposit" | null;
48
+ breakdown: Json;
49
+ }[];
50
+ totals: PricedCart["totals"] | null;
51
+ /** What's still missing before complete can succeed (docs/05 §3). */
52
+ requirements: string[];
53
+ order_id: string | null;
54
+ }
55
+
56
+ /** Errors cross the Durable Object boundary as data, so codes and details survive. */
57
+ export type Result<T> =
58
+ | {
59
+ ok: true;
60
+ value: T;
61
+ /** A customer session the cart's own read renewed (§2.3): the Worker refreshes its cookie. */
62
+ session?: { expiresAt: number };
63
+ }
64
+ | { ok: false; error: { code: string; message: string; status?: number; details?: Record<string, unknown> } };
65
+
66
+ /** What a store write to the cart itself may change (§4, §5). */
67
+ export interface CartPatch {
68
+ email?: string | null;
69
+ locale?: string;
70
+ custom?: JsonRecord;
71
+ consents?: Record<string, { revision_id: string; typed_name: string } | null>;
72
+ }
73
+
74
+ /**
75
+ * Every call after create carries the cart token, so the cart can check it
76
+ * again (a second layer), and the request's store proof, which the cart
77
+ * re-proves inside the batch it sends anyway (p0.5 §6.2).
78
+ */
79
+ export interface CartGateway {
80
+ create(
81
+ cartId: string,
82
+ proof: StoreProof,
83
+ input: { currency: string; email: string | null; locale?: string },
84
+ ): Promise<Result<CartView>>;
85
+ get(cartId: string, token: string, proof: StoreProof): Promise<Result<CartView>>;
86
+ setLine(
87
+ cartId: string,
88
+ token: string,
89
+ proof: StoreProof,
90
+ variantId: string,
91
+ quantity: number,
92
+ mode: "add" | "set",
93
+ ): Promise<Result<CartView>>;
94
+ setEmail(cartId: string, token: string, proof: StoreProof, email: string | null): Promise<Result<CartView>>;
95
+ // P0.3: lines by id (§1.1) and the cart's own fields (§4, §5).
96
+ addLine(cartId: string, token: string, proof: StoreProof, input: LineInput): Promise<Result<CartView>>;
97
+ changeLine(
98
+ cartId: string,
99
+ token: string,
100
+ proof: StoreProof,
101
+ lineId: string,
102
+ patch: LinePatch,
103
+ ): Promise<Result<CartView>>;
104
+ removeLines(cartId: string, token: string, proof: StoreProof, lineIds: string[]): Promise<Result<CartView>>;
105
+ update(cartId: string, token: string, proof: StoreProof, patch: CartPatch): Promise<Result<CartView>>;
106
+ complete(
107
+ cartId: string,
108
+ token: string,
109
+ proof: StoreProof,
110
+ expectedTotal: number,
111
+ ): Promise<Result<{ order: OrderView }>>;
112
+ }
113
+
114
+ export interface ProductReader {
115
+ productByHandle(
116
+ handle: string,
117
+ currency: string,
118
+ ): Promise<{
119
+ id: string;
120
+ title: string;
121
+ handle: string;
122
+ variants: {
123
+ id: string;
124
+ sku: string;
125
+ title: string;
126
+ price: { amount: number; currency_code: string } | null;
127
+ available: number;
128
+ }[];
129
+ } | null>;
130
+ createProduct(
131
+ input: {
132
+ title: string;
133
+ handle: string;
134
+ variants: {
135
+ sku: string;
136
+ title: string;
137
+ prices: Record<string, number>;
138
+ stock: Record<string, number>;
139
+ allowedKinds?: string[];
140
+ inventory?: boolean;
141
+ }[];
142
+ },
143
+ who: CommandIdentity,
144
+ ): Promise<{ productId: string; variantIds: string[] }>;
145
+ setAllowedKinds(variantId: string, kinds: string[]): Promise<void>;
146
+ }
147
+
148
+ /** Staff-set store settings P0.3 adds (§3.2, §5.1). */
149
+ export interface SettingsStore {
150
+ tax(): Promise<TaxContext>;
151
+ setTax(input: TaxContext): Promise<TaxContext>;
152
+ publishRevision(
153
+ policy: string,
154
+ input: { locale: string; body: string },
155
+ who: CommandIdentity,
156
+ ): Promise<{
157
+ id: string;
158
+ policy: string;
159
+ version: number;
160
+ locale: string;
161
+ body: string;
162
+ digest: string;
163
+ publishedAt: number;
164
+ }>;
165
+ currentRevision(
166
+ policy: string,
167
+ locale: string,
168
+ ): Promise<{
169
+ id: string;
170
+ policy: string;
171
+ version: number;
172
+ locale: string;
173
+ body: string;
174
+ digest: string;
175
+ publishedAt: number;
176
+ } | null>;
177
+ requiredPolicies(): Promise<string[]>;
178
+ setRequiredPolicies(policies: string[]): Promise<string[]>;
179
+ }
180
+
181
+ /**
182
+ * The line features' rollout mode (§1.6). `legacy`: only P0.2's routes and
183
+ * RPC methods, for the first deployment of an upgrade. `on`: everything.
184
+ * `frozen`: the rollback once `on` has run, refusing new feature state but
185
+ * keeping every operation existing carts need.
186
+ */
187
+ export type LineFeatures = "legacy" | "on" | "frozen";
188
+
189
+ export interface Secrets {
190
+ cartTokenSecret: string;
191
+ orderTokenSecret: string;
192
+ }
193
+
194
+ // ---- P0.5: access (p0.5-auth-and-access.md §2, §3) --------------------------------
195
+
196
+ export type Pool = "staff" | "customer";
197
+
198
+ /** A valid session, as the access read found it. */
199
+ export interface AccessSession {
200
+ id: string;
201
+ pool: Pool;
202
+ principalId: string;
203
+ method: string;
204
+ authenticatedAt: number;
205
+ lastSeenAt: number;
206
+ expiresAt: number;
207
+ enrolUntil: number | null;
208
+ epoch: number;
209
+ }
210
+
211
+ /** A verified API key. */
212
+ export interface AccessKey {
213
+ id: string;
214
+ type: "secret" | "publishable";
215
+ storefrontId: string | null;
216
+ lastUsedAt: number | null;
217
+ /** A publishable key's storefront's allowed browser origins (§6.2). */
218
+ origins: readonly string[];
219
+ }
220
+
221
+ export interface ResolvedAccess {
222
+ ctx: AccessContext;
223
+ session?: AccessSession;
224
+ key?: AccessKey;
225
+ }
226
+
227
+ /** The per-request access read (§3.3); @kedge/db's D1AccessStore. */
228
+ export interface AccessStore {
229
+ session(pool: Pool, token: string, now: number): Promise<ResolvedAccess | null>;
230
+ maintenanceDue(session: AccessSession, now: number): boolean;
231
+ /** Staff activity or a customer renewal; the renewed expiry, if any. */
232
+ maintain(session: AccessSession, now: number): Promise<number | null>;
233
+ apiKey(presented: string, now: number): Promise<ResolvedAccess | null>;
234
+ /** A CLI session's principal (p0.10 §6.2), or null when the token names no unexpired one. */
235
+ cliSession(token: string, now: number): Promise<ResolvedAccess | null>;
236
+ keyTouchDue(key: { lastUsedAt: number | null }, now: number): boolean;
237
+ touchKey(keyId: string, now: number): Promise<void>;
238
+ /** Counts a failed key verification against the address; false once past the limit (§4.3). */
239
+ keyFailure(address: string | null, now: number): Promise<boolean>;
240
+ /**
241
+ * A publishable key's lookup id and its secret's digest under every pepper
242
+ * version this Worker holds, with no D1 read (§6.2); null if malformed.
243
+ */
244
+ keyProof(presented: string): Promise<{ lookup: string; digests: string[] } | null>;
245
+ /** Every origin a storefront with an unrevoked publishable key lists, for preflights (§6.2). */
246
+ storefrontOrigins(now: number): Promise<ReadonlySet<string>>;
247
+ csrf(sessionToken: string, token: string, now: number): Promise<"ok" | "expired" | "invalid">;
248
+ /** Consumes a session route's rate counters (§2.8); false once past a limit. */
249
+ limit(
250
+ policy: "session" | "session_read",
251
+ pool: Pool,
252
+ subjects: { session?: string; address?: string },
253
+ now: number,
254
+ ): Promise<boolean>;
255
+ }
256
+
257
+ export interface ClientInfo {
258
+ ip: string | null;
259
+ userAgent: string | null;
260
+ }
261
+
262
+ /** A session a ceremony made; its raw token goes to the client once. */
263
+ export interface IssuedSession {
264
+ id: string;
265
+ token: string;
266
+ pool: Pool;
267
+ principalId: string;
268
+ method: string;
269
+ expiresAt: number;
270
+ csrfToken: string;
271
+ }
272
+
273
+ export interface Caller {
274
+ sessionId: string;
275
+ principalId: string;
276
+ }
277
+
278
+ /** Kedge's credential ceremonies (§2); @kedge/db's D1AuthStore. */
279
+ export interface AuthCeremonies {
280
+ staffSignIn(
281
+ input: { email: string; password: string; deviceToken?: string | null },
282
+ client: ClientInfo,
283
+ now: number,
284
+ ): Promise<
285
+ | { kind: "session"; session: IssuedSession; deviceToken: string | null }
286
+ | { kind: "challenge"; challengeToken: string; expiresAt: number }
287
+ >;
288
+ staffTotpChallenge(
289
+ input: { challengeToken: string; code: string; deviceToken?: string | null },
290
+ client: ClientInfo,
291
+ now: number,
292
+ ): Promise<{ session: IssuedSession; deviceToken: string | null }>;
293
+ totpEnrolStart(caller: Caller, account: string, now: number): Promise<{ uri: string; expiresAt: number }>;
294
+ totpEnrolConfirm(caller: Caller, code: string, client: ClientInfo, now: number): Promise<void>;
295
+ staffChangePassword(caller: Caller, current: string, next: string, client: ClientInfo, now: number): Promise<void>;
296
+ signOut(pool: Pool, token: string, now: number, who: { principalId: string; ip: string | null }): Promise<void>;
297
+ signOutEverywhere(caller: Caller, pool: Pool, now: number, ip: string | null): Promise<void>;
298
+ acceptInvite(
299
+ input: { token: string; password: string; resumeNonce: string },
300
+ client: ClientInfo,
301
+ now: number,
302
+ ): Promise<IssuedSession>;
303
+ resumeInvite(
304
+ input: { token: string; password: string; resumeNonce: string },
305
+ client: ClientInfo,
306
+ now: number,
307
+ ): Promise<IssuedSession>;
308
+ requestCode(email: string, client: ClientInfo, now: number): Promise<void>;
309
+ verifyCode(email: string, code: string, client: ClientInfo, now: number): Promise<IssuedSession>;
310
+ refreshCsrf(sessionToken: string, now: number): Promise<{ csrfToken: string; expiresAt: number }>;
311
+ /** A signed-in customer's own record (`/store/customers/me`). */
312
+ customerProfile(customerId: string): Promise<{ id: string; email: string; createdAt: number } | null>;
313
+ /** A signed-in staff member's email, for the authenticator label. */
314
+ staffEmail(staffId: string): Promise<string | null>;
315
+ /** P0.9 (§4.4): a member's email and roles. */
316
+ staffProfile(
317
+ staffId: string,
318
+ ): Promise<{ email: string; roles: { id: string; name: string; strong: boolean }[] } | null>;
319
+ /** P0.9 (§4.2): whether the member has an enrolled TOTP factor. */
320
+ staffFactorEnrolled(staffId: string): Promise<boolean>;
321
+ }
322
+
323
+ /** What one admin request's batches record and re-prove (§7.1, §3.3). */
324
+ export interface AdminScope {
325
+ access: ResolvedAccess;
326
+ /** The route's permission(s), held unscoped, re-proved in every batch. */
327
+ required: readonly string[];
328
+ action: string;
329
+ ip: string | null;
330
+ userAgent: string | null;
331
+ /** What the batch scope's audit row says the request was for (§7.2): its target and its input, made audit-safe. */
332
+ entity?: { type: string; id: string | null };
333
+ changes?: unknown;
334
+ /** The request as a command (§8) when its route's store keeps no envelope of its own. */
335
+ command?: { key: string; operation: string; targets: readonly string[]; input: unknown };
336
+ }
337
+
338
+ /** A scope-recorded command's stored outcome (§8): what happened and the ids it touched. */
339
+ export interface ScopeOutcome {
340
+ outcome: string;
341
+ ids: readonly string[];
342
+ }
343
+
344
+ /** Runs a handler with every batch it sends audited and guarded (@kedge/db's adminScopeRunner). */
345
+ export interface AdminScopeRunner {
346
+ /** A command already recorded under its key is `replayed` and `fn` doesn't run. */
347
+ run<T>(scope: AdminScope, fn: () => Promise<T>): Promise<{ replayed: ScopeOutcome | null }>;
348
+ /** After a refused request: the outcome of a concurrent send with the same key that won. */
349
+ replayed(scope: AdminScope): Promise<ScopeOutcome | null>;
350
+ }
351
+
352
+ export interface GrantInput {
353
+ permission: string;
354
+ rowScope: "all" | "self" | "location";
355
+ }
356
+ export interface RoleRef {
357
+ id: string;
358
+ version: number;
359
+ }
360
+ export interface AdminRequest {
361
+ idempotencyKey?: string | null;
362
+ ip?: string | null;
363
+ userAgent?: string | null;
364
+ }
365
+ export interface IssuedInvite {
366
+ id: string;
367
+ token: string;
368
+ expiresAt: number;
369
+ }
370
+ /** What a credential reference made, kept for ever (p0.10 §6.2). */
371
+ export interface CredentialRefState {
372
+ ref: string;
373
+ kind: "secret" | "publishable" | "storefront";
374
+ state: "used" | "fenced" | "revoked";
375
+ storefrontId: string | null;
376
+ keyId: string | null;
377
+ }
378
+
379
+ export type CredentialRefCreate =
380
+ | { kind: "secret"; name: string; grants: readonly GrantInput[]; locationIds?: readonly string[] }
381
+ | { kind: "publishable"; name: string; storefrontId: string }
382
+ | { kind: "storefront"; name: string; allowedOrigins: readonly string[] };
383
+
384
+ export interface IssuedKey {
385
+ id: string;
386
+ key: string;
387
+ tokenPrefix: string;
388
+ }
389
+
390
+ /** Staff, roles, invites, storefronts and keys (§3, §4); @kedge/db's D1AdminStore. */
391
+ export interface AdminStore {
392
+ // P0.6b's webhook endpoints (p0.6b §2.1, §2.7, §2.9).
393
+ createWebhookEndpoint(
394
+ ctx: AccessContext,
395
+ input: { url: string; events: readonly string[]; grants: readonly string[]; description?: string | null },
396
+ req?: AdminRequest,
397
+ ): Promise<{ id: string; secret: string }>;
398
+ updateWebhookEndpoint(
399
+ ctx: AccessContext,
400
+ id: string,
401
+ input: { url?: string; events?: readonly string[]; grants?: readonly string[]; description?: string | null },
402
+ req?: AdminRequest,
403
+ ): Promise<{ id: string; generation: number }>;
404
+ setWebhookEndpointStatus(
405
+ ctx: AccessContext,
406
+ id: string,
407
+ action: "disable" | "enable" | "delete",
408
+ req?: AdminRequest,
409
+ ): Promise<void>;
410
+ rotateWebhookSecret(ctx: AccessContext, id: string, req?: AdminRequest): Promise<{ id: string; secret: string }>;
411
+ replayWebhookDelivery(ctx: AccessContext, deliveryId: string, req?: AdminRequest): Promise<{ id: string }>;
412
+ testWebhookEndpoint(ctx: AccessContext, id: string, req?: AdminRequest): Promise<{ id: string }>;
413
+ webhookEndpoints(
414
+ ctx: AccessContext,
415
+ q: { status: "active" | "disabled"; after: string | null; limit: number },
416
+ ): Promise<{ endpoints: unknown[]; next: string | null }>;
417
+ staffList(ctx: AccessContext): Promise<unknown[]>;
418
+ rolesList(ctx: AccessContext): Promise<unknown[]>;
419
+ keysList(ctx: AccessContext, opts?: { stalePepper?: boolean }): Promise<unknown[]>;
420
+ storefrontsList(ctx: AccessContext): Promise<unknown[]>;
421
+ createRole(
422
+ ctx: AccessContext,
423
+ input: { code: string; name: string; grants: readonly GrantInput[]; strongAuth?: boolean },
424
+ req?: AdminRequest,
425
+ ): Promise<{ id: string; version: number | null }>;
426
+ setRoleGrants(
427
+ ctx: AccessContext,
428
+ roleId: string,
429
+ input: { expectedVersion: number; grants: readonly GrantInput[] },
430
+ req?: AdminRequest,
431
+ ): Promise<{ id: string; version: number | null }>;
432
+ deleteRole(ctx: AccessContext, roleId: string, req?: AdminRequest): Promise<void>;
433
+ assignRoles(
434
+ ctx: AccessContext,
435
+ staffId: string,
436
+ input: { roleIds: readonly RoleRef[] },
437
+ req?: AdminRequest,
438
+ ): Promise<void>;
439
+ setLocations(
440
+ ctx: AccessContext,
441
+ principal: { staffId: string } | { apiKeyId: string },
442
+ locationIds: readonly string[],
443
+ req?: AdminRequest,
444
+ ): Promise<void>;
445
+ deactivateStaff(ctx: AccessContext, staffId: string, req?: AdminRequest): Promise<void>;
446
+ createInvite(
447
+ ctx: AccessContext,
448
+ input: { email: string; roleIds: readonly RoleRef[] },
449
+ req?: AdminRequest,
450
+ ): Promise<IssuedInvite>;
451
+ resetFactors(ctx: AccessContext, staffId: string, req?: AdminRequest): Promise<IssuedInvite>;
452
+ createStorefront(
453
+ ctx: AccessContext,
454
+ input: { name: string; allowedOrigins: readonly string[] },
455
+ req?: AdminRequest,
456
+ ): Promise<{ id: string }>;
457
+ setOrigins(
458
+ ctx: AccessContext,
459
+ storefrontId: string,
460
+ allowedOrigins: readonly string[],
461
+ req?: AdminRequest,
462
+ /** The list the caller read: the write is refused CONFLICT if it changed since. */
463
+ expected?: readonly string[],
464
+ ): Promise<void>;
465
+ createSecretKey(
466
+ ctx: AccessContext,
467
+ input: { name: string; grants: readonly GrantInput[]; locationIds?: readonly string[] },
468
+ req?: AdminRequest,
469
+ ): Promise<IssuedKey>;
470
+ createPublishableKey(
471
+ ctx: AccessContext,
472
+ input: { name: string; storefrontId: string },
473
+ req?: AdminRequest,
474
+ ): Promise<IssuedKey>;
475
+ revokeKey(
476
+ ctx: AccessContext,
477
+ keyId: string,
478
+ at?: number,
479
+ req?: AdminRequest,
480
+ storefrontId?: string,
481
+ ): Promise<{ id: string; revokedAt: number | null }>;
482
+ /** A key made under a caller-chosen ref: at most one, ever (p0.10 §6.2). */
483
+ createByRef(
484
+ ctx: AccessContext,
485
+ ref: string,
486
+ input: CredentialRefCreate,
487
+ req?: AdminRequest,
488
+ ): Promise<CredentialRefState & IssuedKey>;
489
+ /** Settles a ref for good: fenced when unused, its key revoked when used. */
490
+ voidRef(
491
+ ctx: AccessContext,
492
+ ref: string,
493
+ kind: CredentialRefState["kind"],
494
+ req?: AdminRequest,
495
+ ): Promise<CredentialRefState>;
496
+ rotateKey(
497
+ ctx: AccessContext,
498
+ keyId: string,
499
+ input: { overlapMs: number },
500
+ req?: AdminRequest,
501
+ storefrontId?: string,
502
+ ): Promise<IssuedKey & { previous: { id: string; revokedAt: number | null } }>;
503
+ auditLog(
504
+ ctx: AccessContext,
505
+ filter?: {
506
+ entityType?: string;
507
+ entityId?: string;
508
+ actor?: string;
509
+ before?: { createdAt: number; id: string };
510
+ limit?: number;
511
+ },
512
+ ): Promise<{ entries: unknown[]; next: { createdAt: number; id: string } | null }>;
513
+ }
514
+
515
+ export interface OrderFilter {
516
+ field: string;
517
+ op: "eq" | "in" | "gte" | "lt";
518
+ value: string | number | readonly (string | number)[];
519
+ }
520
+ export interface OrderPage {
521
+ data: Record<string, unknown>[];
522
+ next_cursor: string | null;
523
+ has_more: boolean;
524
+ }
525
+ /** Authorized order reads through the resource registry (§5.2); @kedge/db's D1OrderQuery. */
526
+ export interface OrderQuery {
527
+ get(ctx: AccessContext, id: string): Promise<Record<string, unknown> | null>;
528
+ list(
529
+ ctx: AccessContext,
530
+ req: {
531
+ filters?: readonly OrderFilter[];
532
+ sort?: { field: string; direction: "asc" | "desc" };
533
+ expand?: readonly string[];
534
+ cursor?: string | null;
535
+ limit?: number;
536
+ },
537
+ ): Promise<OrderPage>;
538
+ search(
539
+ ctx: AccessContext,
540
+ req: {
541
+ filters?: readonly OrderFilter[];
542
+ sort?: { field: string; direction: "asc" | "desc" };
543
+ search?: { fields: readonly string[]; term: string };
544
+ expand?: readonly string[];
545
+ cursor?: string | null;
546
+ limit?: number;
547
+ },
548
+ ): Promise<OrderPage>;
549
+ }
550
+
551
+ export interface KeyringStateView {
552
+ family: "pepper" | "totp" | "auth";
553
+ active: number;
554
+ retiring: number | null;
555
+ activatedAt: number;
556
+ held: readonly number[];
557
+ }
558
+ /** The keyring protocol's steps (§4.1, §9); @kedge/db's D1KeyringStore. */
559
+ export interface KeyringAdmin {
560
+ state(): Promise<KeyringStateView[]>;
561
+ activate(
562
+ family: KeyringStateView["family"],
563
+ version: number,
564
+ now: number,
565
+ who?: CommandIdentity,
566
+ ): Promise<KeyringStateView>;
567
+ complete(family: KeyringStateView["family"], now: number, who?: CommandIdentity): Promise<KeyringStateView>;
568
+ rollback(family: KeyringStateView["family"], now: number, who?: CommandIdentity): Promise<KeyringStateView>;
569
+ }
570
+
571
+ export interface AuthDeps {
572
+ access: AccessStore;
573
+ ceremonies: AuthCeremonies;
574
+ scoped: AdminScopeRunner;
575
+ /** `ADMIN_ORIGINS`: the admin's browser origins (§2.9, §9). The Worker's own origin is one too (p0.9 §4.1). */
576
+ adminOrigins: readonly string[];
577
+ /** P0.9 (§2.4): the admin build bundled with this Worker, `KEDGE_ADMIN_BUILD`; null when none is. */
578
+ adminBuild?: string | null;
579
+ /** P0.9 (§9): the deployment's resolved access contract, hashed once at boot; part of every authority fingerprint. */
580
+ accessContract?: string;
581
+ }
582
+
583
+ /** P0.4's capacity administration (p0.4-dated-capacity.md §6); @kedge/db's D1CapacityStore. */
584
+ export interface CapacityAdmin {
585
+ createPool(
586
+ who: CommandIdentity,
587
+ input: {
588
+ kind: string;
589
+ inventoryItemId?: string | null;
590
+ locationId?: string | null;
591
+ bucketMinutes: number;
592
+ maxSpan: number;
593
+ initialBase: number;
594
+ },
595
+ ): Promise<{ id: string }>;
596
+ changeBase(
597
+ who: CommandIdentity,
598
+ poolId: string,
599
+ input: { delta: number; origin: "physical" | "scheduled"; date?: number; reason: string },
600
+ ): Promise<{ id: string; fromBucket: number }>;
601
+ cancelBaseChange(changeId: string): Promise<void>;
602
+ adjust(
603
+ who: CommandIdentity,
604
+ poolId: string,
605
+ input: { start: number; end: number; delta: number; reason: string },
606
+ ): Promise<{ id: string; firstBucket: number; lastBucket: number }>;
607
+ cancelAdjustment(adjustmentId: string): Promise<void>;
608
+ setCeiling(
609
+ who: CommandIdentity,
610
+ poolId: string,
611
+ input: { slotOfWeek: number; ceiling: number | null },
612
+ ): Promise<{ poolId: string; slotOfWeek: number; ceiling: number | null }>;
613
+ setBuffer(input: { kind: string; poolKind: string; before: number; after: number }): Promise<void>;
614
+ hold(
615
+ who: CommandIdentity,
616
+ poolId: string,
617
+ input: { kind: "block" | "maintenance" | "import"; start: number; end: number; qty: number },
618
+ ): Promise<{ id: string; firstBucket: number; lastBucket: number }>;
619
+ releaseHold(holdId: string): Promise<void>;
620
+ mapScope(
621
+ who: CommandIdentity,
622
+ input: { mapping: string; key: string; poolId: string },
623
+ ): Promise<{ mapping: string; key: string; poolId: string }>;
624
+ unmapScope(input: { mapping: string; key: string; poolId: string }): Promise<void>;
625
+ markNotReopenable(orderId: string): Promise<void>;
626
+ convert(
627
+ who: CommandIdentity,
628
+ itemId: string,
629
+ input: { to: "count" | "dated" | "none"; bucketMinutes?: number; maxSpan?: number; reason: string },
630
+ ): Promise<{ itemId: string; tracking: string; pools: string[] }>;
631
+ replacePool(
632
+ who: CommandIdentity,
633
+ poolId: string,
634
+ input: { bucketMinutes: number; maxSpan: number; reason: string },
635
+ ): Promise<{ id: string }>;
636
+ pools(filter?: { inventoryItemId?: string; kind?: string }): Promise<unknown[]>;
637
+ poolAvailability(poolId: string, first: number, last: number): Promise<unknown>;
638
+ holdsOn(poolId: string, first: number, last: number): Promise<unknown[]>;
639
+ shortages(): Promise<unknown>;
640
+ negativeBases(): Promise<unknown[]>;
641
+ }
642
+
643
+ /** The relay's delivery tables (p0.6a §2.3; p0.6b §2.3): subscriber, release, resolution and webhook deliveries. */
644
+ export type DeliveryKind = "sub" | "rel" | "res" | "whk";
645
+
646
+ /**
647
+ * P0.6a's staff surface (p0.6a §2.8, §4.9): failed, blocked and orphaned
648
+ * deliveries, the relay's health, and the unresolved effects across the
649
+ * journal's shards. @kedge/runtime-cf implements it over D1 and the journal.
650
+ */
651
+ export interface DeliveryAdmin {
652
+ problems(q: {
653
+ state: "failed" | "blocked" | "orphaned";
654
+ plugin?: string;
655
+ /** The types whose webhook deliveries the reader may see (p0.6b §2.9); none listed without it. */
656
+ webhookTypes?: readonly string[];
657
+ cursor: string | null;
658
+ limit: number;
659
+ }): Promise<{ items: unknown[]; next: string | null }>;
660
+ /** A webhook delivery's event type, for the routes that name one (p0.6b §2.9). */
661
+ webhookEventType(seq: number): Promise<string | null>;
662
+ /** False when the delivery isn't `failed` (a second retry, or one racing a discard). */
663
+ /** `expected`: the row's `updated_at` the caller acted on, or null for any failed row. */
664
+ retry(kind: DeliveryKind, seq: number, expected?: number | null): Promise<boolean>;
665
+ discard(kind: DeliveryKind, seq: number, expected?: number | null): Promise<boolean>;
666
+ health(): Promise<unknown>;
667
+ effects(q: {
668
+ state: "unresolved" | "conflict" | "divergence";
669
+ cursor: string | null;
670
+ limit: number;
671
+ /** The webhook types the reader may see, for `conflict`: a webhook's key conflict is listed too (codex code r2). */
672
+ webhookTypes: readonly string[];
673
+ }): Promise<{ items: unknown[]; next: string | null }>;
674
+ /**
675
+ * Records a staff decision as a resolution delivery (§4.9), after checking
676
+ * it against the record as it is now and the operation this deployment
677
+ * declares; refused before anything is written when it lacks what its form needs.
678
+ */
679
+ resolve(input: {
680
+ key: string;
681
+ expectedRevision: number;
682
+ form: "observe" | "succeeded" | "failed_final";
683
+ result?: Record<string, unknown>;
684
+ code?: string;
685
+ staffId: string;
686
+ /** Required: the resolution is created under it, and its retry answers the same id (code r17). */
687
+ idempotencyKey: string | null;
688
+ }): Promise<{ resolutionId: string; replayed?: boolean }>;
689
+ resolution(id: string): Promise<unknown | null>;
690
+ }
691
+
692
+ /** P0.6b's delivery log (§2.9): bounded per readable type. @kedge/db's D1WebhookStore. */
693
+ export interface WebhookLog {
694
+ log(q: {
695
+ types: readonly string[];
696
+ endpoint?: string;
697
+ state?: "pending" | "finalizing" | "done" | "failed";
698
+ after: number;
699
+ limit: number;
700
+ }): Promise<{ items: Record<string, unknown>[]; next: number | null }>;
701
+ }
702
+
703
+ /** P0.6b's preview capture log (§4.4); null outside a preview. */
704
+ export interface PreviewCaptures {
705
+ list(q: { port?: string; after: number; limit: number }): Promise<Record<string, unknown>[] | null>;
706
+ }
707
+
708
+ /** P0.6b's business calendar (§3), over the store's zone, which the runtime holds. */
709
+ export interface CalendarAdmin {
710
+ get(now: number): Promise<unknown>;
711
+ set(input: { workingDays: number; cutoffMinutes: number | null }): Promise<void>;
712
+ putHoliday(day: string, name: string): Promise<void>;
713
+ deleteHoliday(day: string): Promise<void>;
714
+ }
715
+
716
+ /** A command's caller and its Idempotency-Key (p0.4 §6). */
717
+ export interface CommandIdentity {
718
+ principal: string;
719
+ key: string | null;
720
+ }
721
+
722
+ /** Store availability (dated-capacity.md §7); @kedge/db's D1AvailabilityStore. */
723
+ export interface AvailabilityReader {
724
+ available(input: {
725
+ variantId: string;
726
+ kind?: string;
727
+ periods: { start: number; end: number }[];
728
+ locationId?: string;
729
+ now: number;
730
+ }): Promise<{ kind: string; periods: { start: number; end: number; available: number | null }[] }>;
731
+ days(from: number, days: number): { start: number; end: number }[];
732
+ }
733
+
734
+ /** P0.7 (§3.4, §3.8): track commands and reads; @kedge/db's D1TrackStore. */
735
+ export interface TrackToken {
736
+ recovery_id: string;
737
+ version: number;
738
+ }
739
+ export interface TrackCommandInput {
740
+ plugin: string;
741
+ track: string;
742
+ aggregateId: string;
743
+ idempotencyKey: string | null;
744
+ ip?: string | null;
745
+ userAgent?: string | null;
746
+ }
747
+ export interface TrackAdmin {
748
+ read(ctx: AccessContext, plugin: string, track: string, aggregateId: string, after?: number): Promise<unknown>;
749
+ start(ctx: AccessContext, cmd: TrackCommandInput): Promise<unknown>;
750
+ transition(
751
+ ctx: AccessContext,
752
+ cmd: TrackCommandInput & { transition: string; expected: TrackToken; input?: unknown; comment?: string | null },
753
+ ): Promise<unknown>;
754
+ }
755
+
756
+ /** P0.7 (§4.4, §4.7): plugin entities; @kedge/db's D1EntityStore. */
757
+ export interface EntityAdmin {
758
+ list(
759
+ ctx: AccessContext,
760
+ plugin: string,
761
+ entity: string,
762
+ req: { query: unknown; cursor?: string | null; limit?: number; state?: "live" | "archived" },
763
+ ): Promise<unknown>;
764
+ get(ctx: AccessContext, plugin: string, entity: string, id: string): Promise<Record<string, unknown> | null>;
765
+ create(
766
+ ctx: AccessContext,
767
+ cmd: {
768
+ plugin: string;
769
+ entity: string;
770
+ values: unknown;
771
+ orderId?: string | null;
772
+ idempotencyKey: string | null;
773
+ ip?: string | null;
774
+ userAgent?: string | null;
775
+ },
776
+ ): Promise<unknown>;
777
+ write(
778
+ ctx: AccessContext,
779
+ cmd: {
780
+ plugin: string;
781
+ entity: string;
782
+ id: string;
783
+ version: number;
784
+ set?: unknown;
785
+ archive?: boolean;
786
+ idempotencyKey: string | null;
787
+ ip?: string | null;
788
+ userAgent?: string | null;
789
+ },
790
+ ): Promise<unknown>;
791
+ metadata(ctx: AccessContext): unknown;
792
+ }
793
+
794
+ /** P0.7 (§5.1): installed plugins and their lifecycle; @kedge/db's D1PluginStore. */
795
+ export interface PluginAdmin {
796
+ list(): Promise<unknown[]>;
797
+ set(plugin: string, to: "active" | "disabled"): Promise<unknown>;
798
+ }
799
+
800
+ export interface AppDeps {
801
+ secrets: Secrets;
802
+ /** P0.8 (§7): the store's ejected units, by id. */
803
+ overrides?: Overrides;
804
+ /** P0.5: who is asking (§3.3) and the credential ceremonies (§2). */
805
+ auth: AuthDeps;
806
+ /** P0.5: staff, roles, keys, storefronts, the audit log and authorized order reads; their routes exist only when given. */
807
+ admin?: AdminStore;
808
+ orderQuery?: OrderQuery;
809
+ keyring?: KeyringAdmin;
810
+ /** P0.4: capacity administration and store availability; their routes exist only when given. */
811
+ capacity?: CapacityAdmin;
812
+ availability?: AvailabilityReader;
813
+ /** P0.6a: deliveries and effects; their routes exist only when given. */
814
+ deliveries?: DeliveryAdmin;
815
+ /** P0.6b: the webhook delivery log and the business calendar; their routes exist only when given. */
816
+ webhooks?: WebhookLog;
817
+ calendar?: CalendarAdmin;
818
+ previewCaptures?: PreviewCaptures;
819
+ /** P0.7: the plugin lifecycle; its routes exist only when given. */
820
+ plugins?: PluginAdmin;
821
+ /** P0.7: tracks; their routes exist only when given. */
822
+ tracks?: TrackAdmin;
823
+ /** P0.9 (§5.1): plugin tracks on core resources, for the admin's metadata. */
824
+ adminMeta?: { tracksOn: (ctx: import("@kedge/core").AccessContext, on: "order" | "order_line") => unknown[] };
825
+ /** P0.7: plugin entities, with every declared entity's definition for its routes and schemas. */
826
+ entities?: {
827
+ store: EntityAdmin;
828
+ declared: readonly { plugin: string; entity: string; def: import("@kedge/core").EntityDefinition }[];
829
+ };
830
+ carts: CartGateway;
831
+ products: ProductReader;
832
+ settings: SettingsStore;
833
+ lineFeatures?: LineFeatures;
834
+ /** The merchant's custom fields, to project orders for each reader (§4.2). */
835
+ fields?: CustomFields;
836
+ /** `order_line` identity fields: setting one creates a new line identity, refused while frozen. */
837
+ identityKeys?: readonly string[];
838
+ orders: OrderStore;
839
+ ids: IdGenerator;
840
+ clock: Clock;
841
+ }