@nifrajs/core 3.1.0 → 3.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.
Files changed (146) hide show
  1. package/README.md +21 -0
  2. package/dist/durable-adapters.d.ts.map +1 -1
  3. package/dist/durable-adapters.js +26 -3
  4. package/dist/durable-adapters.js.map +1 -1
  5. package/dist/durable-approval.d.ts +56 -0
  6. package/dist/durable-approval.d.ts.map +1 -0
  7. package/dist/durable-approval.js +233 -0
  8. package/dist/durable-approval.js.map +1 -0
  9. package/dist/durable-effect.d.ts +42 -0
  10. package/dist/durable-effect.d.ts.map +1 -0
  11. package/dist/durable-effect.js +234 -0
  12. package/dist/durable-effect.js.map +1 -0
  13. package/dist/durable-execution.d.ts +8 -346
  14. package/dist/durable-execution.d.ts.map +1 -1
  15. package/dist/durable-execution.js +7 -1150
  16. package/dist/durable-execution.js.map +1 -1
  17. package/dist/durable-saga.d.ts +94 -0
  18. package/dist/durable-saga.d.ts.map +1 -0
  19. package/dist/durable-saga.js +550 -0
  20. package/dist/durable-saga.js.map +1 -0
  21. package/dist/edge-kit.d.ts +2 -2
  22. package/dist/edge-kit.d.ts.map +1 -1
  23. package/dist/edge-kit.js +2 -2
  24. package/dist/edge-kit.js.map +1 -1
  25. package/dist/index.d.ts +23 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +18 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/internal/durable-approval-protocol.d.ts +1 -1
  30. package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
  31. package/dist/internal/durable-effect-protocol.d.ts +1 -1
  32. package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
  33. package/dist/internal/durable-saga-protocol.d.ts +1 -1
  34. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  35. package/dist/internal/durable-shared.d.ts +26 -0
  36. package/dist/internal/durable-shared.d.ts.map +1 -0
  37. package/dist/internal/durable-shared.js +131 -0
  38. package/dist/internal/durable-shared.js.map +1 -0
  39. package/dist/internal/durable-types.d.ts +159 -0
  40. package/dist/internal/durable-types.d.ts.map +1 -0
  41. package/dist/internal/durable-types.js +2 -0
  42. package/dist/internal/durable-types.js.map +1 -0
  43. package/dist/internal/route-compiler.d.ts +61 -0
  44. package/dist/internal/route-compiler.d.ts.map +1 -0
  45. package/dist/internal/route-compiler.js +121 -0
  46. package/dist/internal/route-compiler.js.map +1 -0
  47. package/dist/internal/route-execution.d.ts +6 -4
  48. package/dist/internal/route-execution.d.ts.map +1 -1
  49. package/dist/internal/route-execution.js +2 -17
  50. package/dist/internal/route-execution.js.map +1 -1
  51. package/dist/internal/route-lanes.d.ts +35 -0
  52. package/dist/internal/route-lanes.d.ts.map +1 -0
  53. package/dist/internal/route-lanes.js +150 -0
  54. package/dist/internal/route-lanes.js.map +1 -0
  55. package/dist/internal/route-program.d.ts +67 -0
  56. package/dist/internal/route-program.d.ts.map +1 -0
  57. package/dist/internal/route-program.js +199 -0
  58. package/dist/internal/route-program.js.map +1 -0
  59. package/dist/server/bare-error-lane.d.ts +1 -1
  60. package/dist/server/bare-error-lane.d.ts.map +1 -1
  61. package/dist/server/bare-error-lane.js +3 -3
  62. package/dist/server/bare-error-lane.js.map +1 -1
  63. package/dist/server/body-lane.d.ts +13 -0
  64. package/dist/server/body-lane.d.ts.map +1 -0
  65. package/dist/server/body-lane.js +134 -0
  66. package/dist/server/body-lane.js.map +1 -0
  67. package/dist/server/contract.d.ts +40 -14
  68. package/dist/server/contract.d.ts.map +1 -1
  69. package/dist/server/contract.js +7 -0
  70. package/dist/server/contract.js.map +1 -1
  71. package/dist/server/cookies.d.ts.map +1 -1
  72. package/dist/server/cookies.js +16 -2
  73. package/dist/server/cookies.js.map +1 -1
  74. package/dist/server/plugin.d.ts +2 -2
  75. package/dist/server/plugin.d.ts.map +1 -1
  76. package/dist/server/plugin.js +6 -1
  77. package/dist/server/plugin.js.map +1 -1
  78. package/dist/server/registry.d.ts +40 -12
  79. package/dist/server/registry.d.ts.map +1 -1
  80. package/dist/server/request-context.d.ts +4 -28
  81. package/dist/server/request-context.d.ts.map +1 -1
  82. package/dist/server/request-context.js +10 -185
  83. package/dist/server/request-context.js.map +1 -1
  84. package/dist/server/respond.d.ts.map +1 -1
  85. package/dist/server/respond.js +39 -2
  86. package/dist/server/respond.js.map +1 -1
  87. package/dist/server/runtime-core.d.ts +18 -2
  88. package/dist/server/runtime-core.d.ts.map +1 -1
  89. package/dist/server/runtime-core.js +0 -23
  90. package/dist/server/runtime-core.js.map +1 -1
  91. package/dist/server/server-types.d.ts +3 -0
  92. package/dist/server/server-types.d.ts.map +1 -1
  93. package/dist/server/server.d.ts +113 -84
  94. package/dist/server/server.d.ts.map +1 -1
  95. package/dist/server/server.js +506 -639
  96. package/dist/server/server.js.map +1 -1
  97. package/dist/server/validation.d.ts +5 -0
  98. package/dist/server/validation.d.ts.map +1 -0
  99. package/dist/server/validation.js +13 -0
  100. package/dist/server/validation.js.map +1 -0
  101. package/dist/server/ws-bun.d.ts +11 -3
  102. package/dist/server/ws-bun.d.ts.map +1 -1
  103. package/dist/server/ws-bun.js +42 -12
  104. package/dist/server/ws-bun.js.map +1 -1
  105. package/dist/server/ws-hook.d.ts +3 -2
  106. package/dist/server/ws-hook.d.ts.map +1 -1
  107. package/dist/server.d.ts +2 -2
  108. package/dist/server.d.ts.map +1 -1
  109. package/dist/server.js +1 -1
  110. package/dist/server.js.map +1 -1
  111. package/dist/single-copy.d.ts.map +1 -1
  112. package/dist/single-copy.js +3 -1
  113. package/dist/single-copy.js.map +1 -1
  114. package/package.json +6 -3
  115. package/src/durable-adapters.ts +20 -0
  116. package/src/durable-approval.ts +281 -0
  117. package/src/durable-effect.ts +300 -0
  118. package/src/durable-execution.ts +8 -1627
  119. package/src/durable-saga.ts +744 -0
  120. package/src/edge-kit.ts +2 -2
  121. package/src/index.ts +129 -2
  122. package/src/internal/durable-approval-protocol.ts +1 -1
  123. package/src/internal/durable-effect-protocol.ts +1 -1
  124. package/src/internal/durable-saga-protocol.ts +1 -1
  125. package/src/internal/durable-shared.ts +157 -0
  126. package/src/internal/durable-types.ts +194 -0
  127. package/src/internal/route-compiler.ts +234 -0
  128. package/src/internal/route-execution.ts +34 -84
  129. package/src/internal/route-lanes.ts +221 -0
  130. package/src/internal/route-program.ts +404 -0
  131. package/src/server/bare-error-lane.ts +3 -2
  132. package/src/server/body-lane.ts +203 -0
  133. package/src/server/contract.ts +70 -20
  134. package/src/server/cookies.ts +16 -2
  135. package/src/server/plugin.ts +11 -5
  136. package/src/server/registry.ts +71 -16
  137. package/src/server/request-context.ts +7 -262
  138. package/src/server/respond.ts +45 -2
  139. package/src/server/runtime-core.ts +28 -2
  140. package/src/server/server-types.ts +3 -0
  141. package/src/server/server.ts +1152 -1260
  142. package/src/server/validation.ts +19 -0
  143. package/src/server/ws-bun.ts +44 -11
  144. package/src/server/ws-hook.ts +3 -2
  145. package/src/server.ts +7 -2
  146. package/src/single-copy.ts +3 -1
package/src/edge-kit.ts CHANGED
@@ -16,12 +16,12 @@
16
16
  * or app state. It is the interface, not the framework.
17
17
  */
18
18
 
19
+ export { readBodyFramed } from "./server/body-lane.ts"
19
20
  export { plainError } from "./server/http.ts"
20
21
  export type { ProtoPoisoning } from "./server/proto-guard.ts"
21
22
  export { type QueryValue, queryObjectOf, searchOf } from "./server/query.ts"
22
- export { readBodyFramed } from "./server/request-context.ts"
23
23
  export { toResponse } from "./server/respond.ts"
24
24
  export type { ResponseResult } from "./server/runtime-core.ts"
25
25
  export { EMPTY_RESPONSE_CONTROLS } from "./server/runtime-core.ts"
26
26
  export type { CtxSet, MaybePromise, RequestSource } from "./server/server.ts"
27
- export { plainValidationError } from "./server/server.ts"
27
+ export { plainValidationError } from "./server/validation.ts"
package/src/index.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  *
4
4
  * Nifra 2.0 keeps the package root intentionally lean. Optional systems live at documented
5
5
  * subpaths so importing the root cannot activate unrelated runtimes.
6
+ *
7
+ * Every named export below is re-exported in a SINGLE hop from the module that defines it -
8
+ * deliberately not `export * from "./server.ts"`. A root binding that travels through a chained
9
+ * barrel can be dropped by a bundler that rewrites module loads (the single-copy plugin does
10
+ * exactly that), which surfaces as a runtime `ReferenceError` only in bundled output. The list
11
+ * must stay identical to `./server.ts`; `server-entry.test.ts` enforces the parity.
6
12
  */
7
13
 
8
14
  /**
@@ -10,8 +16,129 @@
10
16
  * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
11
17
  * `check:publish` asserts it equals `@nifrajs/core`'s package version.
12
18
  */
13
- export const VERSION = "3.1.0" as const
19
+ export const VERSION = "3.3.0" as const
14
20
 
15
21
  export type Version = typeof VERSION
16
22
 
17
- export * from "./server.ts"
23
+ export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
24
+ export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
25
+ export { isSameOriginRequest } from "./internal/same-origin.ts"
26
+ export { METHODS, type Method, Router, type RouterMatch } from "./router/router.ts"
27
+ export type {
28
+ InferInput,
29
+ InferOutput,
30
+ StandardIssue,
31
+ StandardResult,
32
+ StandardSchemaV1,
33
+ StandardTypes,
34
+ ValidationOutcome,
35
+ } from "./schema/standard.ts"
36
+ export type {
37
+ Context,
38
+ Params,
39
+ Platform,
40
+ Prettify,
41
+ ResponseControls,
42
+ RouteSchema,
43
+ } from "./server/context.ts"
44
+ export {
45
+ type CookieOptions,
46
+ type CookieSecret,
47
+ cookieNamePrefix,
48
+ parseCookies,
49
+ serializeCookie,
50
+ signValue,
51
+ unsignValue,
52
+ } from "./server/cookies.ts"
53
+ export {
54
+ type DurableObjectNamespaceLike,
55
+ type ExecutionContext,
56
+ type ScheduledController,
57
+ type ScheduledHandler,
58
+ toFetchHandler,
59
+ } from "./server/edge.ts"
60
+ export { isSameOriginPath, pathnameOf, type UrlParts, urlPartsOf } from "./server/http.ts"
61
+ export {
62
+ commonSecretPatterns,
63
+ jsonLogger,
64
+ type LogFields,
65
+ type Logger,
66
+ type RedactOptions,
67
+ redactLogFields,
68
+ silentLogger,
69
+ } from "./server/logger.ts"
70
+ export {
71
+ type FetchApp,
72
+ type LambdaEvent,
73
+ type LambdaHandler,
74
+ type LambdaResponse,
75
+ type LambdaV1Event,
76
+ type LambdaV2Event,
77
+ type NetlifyEvent,
78
+ type NetlifyHandler,
79
+ type PlatformResponse,
80
+ toLambdaHandler,
81
+ toNetlifyHandler,
82
+ toVercelHandler,
83
+ type VercelHandler,
84
+ } from "./server/platform-adapters.ts"
85
+ export type { Registry, ResponseMapFor, RouteInfo, RouteInfoFor } from "./server/registry.ts"
86
+ export {
87
+ type PlainRender,
88
+ type ResponseResult,
89
+ type StatusResponse,
90
+ status,
91
+ } from "./server/runtime-core.ts"
92
+ export {
93
+ type AdmissionController,
94
+ type AdmissionDecision,
95
+ type AnyServer,
96
+ type ContextPlugin,
97
+ type DefinePluginResult,
98
+ defineContextPlugin,
99
+ defineIdentityPlugin,
100
+ definePlugin,
101
+ defineRouterPlugin,
102
+ type Handler,
103
+ type IdentityPlugin,
104
+ type McpPromptDescriptor,
105
+ type McpResourceDescriptor,
106
+ type Middleware,
107
+ type NifraFeatureVersion,
108
+ type NifraPlugin,
109
+ type NodeRequestContext,
110
+ type NodeRequestHook,
111
+ type NodeResponseContext,
112
+ type NodeResponseHook,
113
+ type NodeServeOutcome,
114
+ type OnRequestResult,
115
+ type PluginTypeCollapsed,
116
+ type PromptArgument,
117
+ type PromptMessage,
118
+ type ResponseBodyHook,
119
+ type ResponseBodyReplacement,
120
+ type ResponseFinalization,
121
+ type ResponseHeadersHook,
122
+ type ResponseHeadersView,
123
+ type RouteDescriptor,
124
+ type RunningServer,
125
+ Server,
126
+ type ServerOptions,
127
+ server,
128
+ type ToolAnnotations,
129
+ } from "./server/server.ts"
130
+ export type {
131
+ SSEContext,
132
+ SSEInit,
133
+ SSEMessage,
134
+ SSEStream,
135
+ TypedSSEStream,
136
+ } from "./server/sse.ts"
137
+ export type {
138
+ NifraWebSocket,
139
+ StandardWebSocket,
140
+ WebSocketContext,
141
+ WebSocketData,
142
+ WebSocketHandler,
143
+ WebSocketUpgradeOutcome,
144
+ } from "./server/websocket.ts"
@@ -1,4 +1,4 @@
1
- import type { ApprovalConsumeResult, ApprovalRecord, ApprovalStore } from "../durable-execution.ts"
1
+ import type { ApprovalConsumeResult, ApprovalRecord, ApprovalStore } from "./durable-types.ts"
2
2
  import { safeEqual } from "./safe-equal.ts"
3
3
 
4
4
  export type DurableApprovalDecisionInput = Parameters<ApprovalStore["decide"]>[0]
@@ -1,4 +1,4 @@
1
- import type { DurableEffectRecord, DurableEffectState } from "../durable-execution.ts"
1
+ import type { DurableEffectRecord, DurableEffectState } from "./durable-types.ts"
2
2
 
3
3
  export interface DurableEffectTransitionInput {
4
4
  readonly effectId: string
@@ -1,4 +1,4 @@
1
- import type { SagaRecord, SagaState, SagaStepRecord, SagaStore } from "../durable-execution.ts"
1
+ import type { SagaRecord, SagaState, SagaStepRecord, SagaStore } from "./durable-types.ts"
2
2
 
3
3
  export type DurableSagaTransitionInput = Parameters<SagaStore["compareAndSet"]>[0]
4
4
 
@@ -0,0 +1,157 @@
1
+ import type { ReconciliationScanOptions, ReconciliationScanPage } from "./durable-types.ts"
2
+
3
+ export const TOKEN = /^[!-~]+$/
4
+ export const ERROR_CODE = /^[a-z][a-z0-9_.-]{0,63}$/
5
+ export const encoder = new TextEncoder()
6
+
7
+ export function assertToken(value: string, label: string, maxLength = 255): void {
8
+ if (
9
+ typeof value !== "string" ||
10
+ value.length === 0 ||
11
+ value.length > maxLength ||
12
+ !TOKEN.test(value)
13
+ ) {
14
+ throw new TypeError(`${label} must be a bounded printable token`)
15
+ }
16
+ }
17
+
18
+ export function assertPositiveMs(value: number, label: string): void {
19
+ if (!Number.isSafeInteger(value) || value < 1)
20
+ throw new RangeError(`${label} must be a positive safe integer`)
21
+ }
22
+
23
+ export function assertTimestamp(value: number, label: string): void {
24
+ if (!Number.isSafeInteger(value) || value < 0)
25
+ throw new RangeError(`${label} must be a non-negative safe integer timestamp`)
26
+ }
27
+
28
+ export function readClock(clock: () => number, label: string): number {
29
+ const value = clock()
30
+ assertTimestamp(value, label)
31
+ return value
32
+ }
33
+
34
+ export function addDuration(timestamp: number, duration: number, label: string): number {
35
+ const result = timestamp + duration
36
+ assertTimestamp(result, label)
37
+ return result
38
+ }
39
+
40
+ export function bytesToBase64Url(bytes: Uint8Array): string {
41
+ let binary = ""
42
+ for (const byte of bytes) binary += String.fromCharCode(byte)
43
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "")
44
+ }
45
+
46
+ export async function sha256(value: string): Promise<string> {
47
+ return bytesToBase64Url(
48
+ new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))),
49
+ )
50
+ }
51
+
52
+ export function cloneValue<T>(value: T): T {
53
+ return structuredClone(value)
54
+ }
55
+
56
+ export const DEFAULT_RECONCILIATION_LIMIT = 100
57
+ export const MAX_RECONCILIATION_LIMIT = 1_000
58
+
59
+ export function reconciliationLimit(value = DEFAULT_RECONCILIATION_LIMIT): number {
60
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_RECONCILIATION_LIMIT) {
61
+ throw new RangeError(`reconciliation limit must be between 1 and ${MAX_RECONCILIATION_LIMIT}`)
62
+ }
63
+ return value
64
+ }
65
+
66
+ export function cursorOffset(cursor?: string): number {
67
+ if (cursor === undefined) return 0
68
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(cursor)) throw new TypeError("invalid reconciliation cursor")
69
+ const offset = Number(cursor)
70
+ if (!Number.isSafeInteger(offset)) throw new TypeError("invalid reconciliation cursor")
71
+ return offset
72
+ }
73
+
74
+ export function memoryScan<
75
+ Record extends { readonly state: State; readonly updatedAt: number },
76
+ State extends string,
77
+ >(
78
+ source: Iterable<Record>,
79
+ input: ReconciliationScanOptions<State>,
80
+ ): ReconciliationScanPage<Record> {
81
+ const states = new Set(input.states)
82
+ const offset = cursorOffset(input.cursor)
83
+ const end = offset + input.limit
84
+ const records: Record[] = []
85
+ let seen = 0
86
+ let more = false
87
+ for (const record of source) {
88
+ if (!states.has(record.state)) continue
89
+ if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
90
+ if (seen >= end) {
91
+ more = true
92
+ break
93
+ }
94
+ if (seen >= offset) records.push(cloneValue(record))
95
+ seen++
96
+ }
97
+ return Object.freeze({
98
+ records: Object.freeze(records),
99
+ ...(more ? { cursor: String(offset + records.length) } : {}),
100
+ })
101
+ }
102
+
103
+ export function bucketAdd<State extends string>(
104
+ byState: Map<State, Set<string>>,
105
+ state: State,
106
+ id: string,
107
+ ): void {
108
+ let bucket = byState.get(state)
109
+ if (bucket === undefined) {
110
+ bucket = new Set()
111
+ byState.set(state, bucket)
112
+ }
113
+ bucket.add(id)
114
+ }
115
+
116
+ export function bucketMove<State extends string>(
117
+ byState: Map<State, Set<string>>,
118
+ from: State,
119
+ to: State,
120
+ id: string,
121
+ ): void {
122
+ if (from === to) return
123
+ byState.get(from)?.delete(id)
124
+ bucketAdd(byState, to, id)
125
+ }
126
+
127
+ export function indexedScan<Record extends { readonly updatedAt: number }, State extends string>(
128
+ byState: Map<State, Set<string>>,
129
+ resolve: (id: string) => Record | undefined,
130
+ input: ReconciliationScanOptions<State>,
131
+ ): ReconciliationScanPage<Record> {
132
+ const offset = cursorOffset(input.cursor)
133
+ const end = offset + input.limit
134
+ const records: Record[] = []
135
+ let seen = 0
136
+ let more = false
137
+ for (const state of new Set(input.states)) {
138
+ if (more) break
139
+ const bucket = byState.get(state)
140
+ if (bucket === undefined) continue
141
+ for (const id of bucket) {
142
+ const record = resolve(id)
143
+ if (record === undefined) continue
144
+ if (input.updatedBefore !== undefined && record.updatedAt > input.updatedBefore) continue
145
+ if (seen >= end) {
146
+ more = true
147
+ break
148
+ }
149
+ if (seen >= offset) records.push(cloneValue(record))
150
+ seen++
151
+ }
152
+ }
153
+ return Object.freeze({
154
+ records: Object.freeze(records),
155
+ ...(more ? { cursor: String(offset + records.length) } : {}),
156
+ })
157
+ }
@@ -0,0 +1,194 @@
1
+ /** Type-only records and transition contracts shared by durable execution domains. */
2
+ export type DurableEffectState = "admission" | "executing" | "committed" | "failed" | "unknown"
3
+
4
+ export interface ReconciliationScanOptions<State extends string> {
5
+ readonly states: readonly State[]
6
+ readonly updatedBefore?: number
7
+ readonly cursor?: string
8
+ readonly limit: number
9
+ }
10
+
11
+ export interface ReconciliationScanPage<Record> {
12
+ readonly records: readonly Record[]
13
+ readonly cursor?: string
14
+ }
15
+
16
+ export interface ReconciliationPage<Finding> {
17
+ readonly findings: readonly Finding[]
18
+ readonly cursor?: string
19
+ }
20
+
21
+ export interface DurableEffectRecord {
22
+ readonly effectId: string
23
+ readonly capability: string
24
+ readonly target?: string
25
+ readonly digest?: string
26
+ readonly tenantId?: string
27
+ readonly principalId?: string
28
+ readonly state: DurableEffectState
29
+ readonly createdAt: number
30
+ readonly updatedAt: number
31
+ readonly errorCode?: string
32
+ readonly version: number
33
+ }
34
+
35
+ export interface DurableEffectStore {
36
+ readonly durability?: "memory" | "durable"
37
+ create(record: DurableEffectRecord): boolean | Promise<boolean>
38
+ get(effectId: string): DurableEffectRecord | undefined | Promise<DurableEffectRecord | undefined>
39
+ /** Atomic compare-and-set; exactly one caller may advance the supplied `version` and `from` state. */
40
+ transition(input: {
41
+ readonly effectId: string
42
+ readonly version: number
43
+ readonly from: DurableEffectState
44
+ readonly to: DurableEffectState
45
+ readonly updatedAt: number
46
+ readonly errorCode?: string
47
+ }): boolean | Promise<boolean>
48
+ /** Bounded operational scan. Transition-only stores do not need to implement reconciliation. */
49
+ scan?(
50
+ input: ReconciliationScanOptions<DurableEffectState>,
51
+ ):
52
+ | ReconciliationScanPage<DurableEffectRecord>
53
+ | Promise<ReconciliationScanPage<DurableEffectRecord>>
54
+ /** @deprecated Compatibility fallback; production reconciliation should implement `scan`. */
55
+ list?(): readonly DurableEffectRecord[] | Promise<readonly DurableEffectRecord[]>
56
+ }
57
+
58
+ export type ApprovalState = "pending" | "approved" | "denied" | "consumed" | "expired"
59
+
60
+ export interface ApprovalRecord {
61
+ readonly approvalId: string
62
+ readonly effectId: string
63
+ readonly capability: string
64
+ readonly target?: string
65
+ readonly digest?: string
66
+ readonly tenantId: string
67
+ readonly principalId: string
68
+ readonly tokenHash: string
69
+ readonly state: ApprovalState
70
+ readonly createdAt: number
71
+ readonly expiresAt: number
72
+ readonly updatedAt: number
73
+ readonly decidedBy?: string
74
+ readonly version: number
75
+ }
76
+
77
+ export type ApprovalConsumeResult =
78
+ | { readonly state: "consumed" }
79
+ | {
80
+ readonly state: "missing" | "pending" | "denied" | "expired" | "replay" | "binding" | "token"
81
+ }
82
+
83
+ export interface ApprovalStore {
84
+ readonly durability?: "memory" | "durable"
85
+ create(record: ApprovalRecord): boolean | Promise<boolean>
86
+ get(approvalId: string): ApprovalRecord | undefined | Promise<ApprovalRecord | undefined>
87
+ decide(input: {
88
+ readonly approvalId: string
89
+ readonly tenantId: string
90
+ readonly decision: "approved" | "denied"
91
+ readonly decidedBy: string
92
+ readonly now: number
93
+ }): boolean | Promise<boolean>
94
+ /**
95
+ * Atomically validate binding/token/state and change `approved` to `consumed`. Two concurrent
96
+ * callers with one token must yield exactly one `consumed` and one `replay` result.
97
+ */
98
+ consume(input: {
99
+ readonly approvalId: string
100
+ readonly tenantId: string
101
+ readonly principalId: string
102
+ readonly capability: string
103
+ readonly target?: string
104
+ readonly digest?: string
105
+ readonly tokenHash: string
106
+ readonly now: number
107
+ }): ApprovalConsumeResult | Promise<ApprovalConsumeResult>
108
+ }
109
+
110
+ export type SagaState = "running" | "compensating" | "completed" | "compensated" | "manual-review"
111
+ export type SagaStepState =
112
+ | "executing"
113
+ | "committed"
114
+ | "failed"
115
+ | "ambiguous"
116
+ | "compensating"
117
+ | "compensation-failed"
118
+ | "compensated"
119
+
120
+ export interface SagaStepRecord {
121
+ readonly name: string
122
+ readonly effectId: string
123
+ readonly compensationEffectId: string
124
+ readonly state: SagaStepState
125
+ readonly compensationArgs: unknown
126
+ readonly result?: unknown
127
+ readonly attempts: number
128
+ readonly nextAttemptAt?: number
129
+ readonly errorCode?: string
130
+ }
131
+
132
+ export interface SagaRecord {
133
+ readonly sagaId: string
134
+ readonly definition: string
135
+ readonly state: SagaState
136
+ readonly input: unknown
137
+ readonly steps: readonly SagaStepRecord[]
138
+ readonly createdAt: number
139
+ readonly updatedAt: number
140
+ readonly version: number
141
+ }
142
+
143
+ export interface SagaStore {
144
+ readonly durability?: "memory" | "durable"
145
+ create(record: SagaRecord): boolean | Promise<boolean>
146
+ get(sagaId: string): SagaRecord | undefined | Promise<SagaRecord | undefined>
147
+ /** Atomic version compare-and-set; this is the saga engine's crash/concurrency boundary. */
148
+ compareAndSet(input: {
149
+ readonly sagaId: string
150
+ readonly version: number
151
+ readonly record: SagaRecord
152
+ }): boolean | Promise<boolean>
153
+ /** Bounded operational scan. Transition-only stores do not need to implement reconciliation. */
154
+ scan?(
155
+ input: ReconciliationScanOptions<SagaState>,
156
+ ): ReconciliationScanPage<SagaRecord> | Promise<ReconciliationScanPage<SagaRecord>>
157
+ /** @deprecated Compatibility fallback; production reconciliation should implement `scan`. */
158
+ list?(): readonly SagaRecord[] | Promise<readonly SagaRecord[]>
159
+ }
160
+
161
+ export interface SagaStepExecutionContext {
162
+ readonly effectId: string
163
+ readonly signal: AbortSignal
164
+ }
165
+ export interface SagaCompensationContext {
166
+ readonly effectId: string
167
+ readonly sagaId: string
168
+ readonly attempt: number
169
+ readonly signal: AbortSignal
170
+ }
171
+
172
+ export interface SagaRunContext<C extends Record<string, unknown>> {
173
+ step<K extends keyof C & string, T>(
174
+ name: K,
175
+ compensationArgs: C[K],
176
+ execute: (context: SagaStepExecutionContext) => T | PromiseLike<T>,
177
+ ): Promise<T>
178
+ }
179
+
180
+ export interface SagaDefinition<I, C extends Record<string, unknown>> {
181
+ readonly name: string
182
+ readonly capability?: string
183
+ readonly run: (context: SagaRunContext<C>, input: I) => void | PromiseLike<void>
184
+ readonly compensators: {
185
+ readonly [K in keyof C]: (
186
+ args: C[K],
187
+ context: SagaCompensationContext,
188
+ ) => void | PromiseLike<void>
189
+ }
190
+ readonly retry?: {
191
+ readonly maxAttempts?: number
192
+ readonly backoffMs?: (attempt: number) => number
193
+ }
194
+ }