@substrat-run/kernel 0.87.0 → 0.89.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.
@@ -0,0 +1,197 @@
1
+ import { IDEMPOTENCY_REPLAY_UNAVAILABLE, IDEMPOTENCY_RESULT_LIMIT, IDEMPOTENCY_RETENTION_MS, IDEMPOTENCY_REUSED, isValidIdempotencyKey, subjectRef, substratError, } from '@substrat-run/contracts';
2
+ /**
3
+ * The spine half of request idempotency (#116) — what remembers a key, and what
4
+ * a second request carrying it is answered with.
5
+ *
6
+ * `@substrat-run/contracts` owns the wire (the header names, what makes a key
7
+ * well-formed, what makes two requests the same request); this owns the table
8
+ * and the decisions read off it. Both adapters call these rather than writing
9
+ * the SQL twice, for the reason `entityVersionQuery` gives one file over: two
10
+ * surfaces answering one question must not drift on what the answer means. Here
11
+ * the answer decides whether work happens at all.
12
+ *
13
+ * ## Recorded INSIDE the operation's transaction, which is the whole design
14
+ *
15
+ * The row is written in the same transaction as the work it describes, after the
16
+ * handler and before `COMMIT`. Three properties fall out of that one placement,
17
+ * and none of them needed a mechanism of its own:
18
+ *
19
+ * - **A failed request is retried, not replayed.** The operation threw, the
20
+ * transaction rolled back, and the row went with it. There is nothing to find,
21
+ * so the retry executes — which is correct, because nothing happened the first
22
+ * time. Recording failures would have meant deciding which of them are
23
+ * permanent, and that is a judgement no generic layer can make.
24
+ * - **A replayed response describes work that actually committed.** The row and
25
+ * the rows it is about are the same transaction; there is no window in which
26
+ * one exists without the other.
27
+ * - **The dedupe cannot be defeated by a concurrent retry.** Invokes serialise
28
+ * per scope in both adapters, so the second request takes its turn after the
29
+ * first has committed — no in-flight state, no "still processing" 409.
30
+ *
31
+ * ## What a replay is NOT
32
+ *
33
+ * It is not a fresh authorization. The recorded response is returned without
34
+ * running the handler, and the permission check lives inside the handler — so a
35
+ * caller whose access was revoked in the last 24 hours can still replay their own
36
+ * response. This is bounded by the two things that make it defensible: the row is
37
+ * keyed by the SUBJECT, so a caller can only ever reach responses they themselves
38
+ * received, and the window is a day. It is stated here rather than discovered,
39
+ * because the alternative — re-running the operation to re-check the permission —
40
+ * is the duplicate execution this feature exists to prevent.
41
+ */
42
+ /**
43
+ * The dedupe table, kernel-owned so that no vertical carries a migration for it.
44
+ *
45
+ * Keyed `(subject, key)` rather than `(key)`, and that is a safety property, not
46
+ * a namespacing convenience: a key is a string a client chose, two clients will
47
+ * choose `1`, and a lookup that found the other one's row would replay a response
48
+ * across a principal boundary. With the subject in the primary key that is not a
49
+ * check that could be forgotten — it is a row that cannot be reached.
50
+ *
51
+ * `operation` is stored as well as hashed into `fingerprint`. The hash is what
52
+ * decides a mismatch; the column is what makes the table readable when someone is
53
+ * working out why a client is getting 409s.
54
+ */
55
+ export const IDEMPOTENCY_DDL = `
56
+ CREATE TABLE IF NOT EXISTS _substrat_idempotency (
57
+ subject TEXT NOT NULL,
58
+ key TEXT NOT NULL,
59
+ operation TEXT NOT NULL,
60
+ -- SHA-256 over (operation, parsed input). A second request under this key
61
+ -- whose fingerprint differs is a REUSE, refused with 409 — never served the
62
+ -- first request's response, which is a different request's answer.
63
+ fingerprint TEXT NOT NULL,
64
+ -- The operation's return value as JSON. NULL means one of two things, which
65
+ -- \`oversized\` separates: the operation returned nothing, or the result was
66
+ -- too large to record and a replay must be refused rather than re-executed.
67
+ result TEXT,
68
+ oversized INTEGER NOT NULL DEFAULT 0,
69
+ -- #129's tag, replayed with the body so a retry hands the client the same
70
+ -- ETag the original did. Without it a replayed response has no validator and
71
+ -- the client's next conditional write has nothing to send.
72
+ entity_version TEXT,
73
+ recorded_at TEXT NOT NULL,
74
+ PRIMARY KEY (subject, key)
75
+ );
76
+ CREATE INDEX IF NOT EXISTS _substrat_idempotency_recorded
77
+ ON _substrat_idempotency (recorded_at);
78
+ `;
79
+ /** The subject a key is scoped to, in the form the column stores. */
80
+ export function idempotencySubject(subject) {
81
+ return subjectRef(subject);
82
+ }
83
+ /**
84
+ * Refuse a malformed key at the door.
85
+ *
86
+ * `validation_failed` rather than `conflict`: nothing is in conflict, the caller
87
+ * sent a header we cannot store. Refused rather than ignored, for the reason the
88
+ * `If-Match` path gives — a caller who believes their retry is safe and whose
89
+ * key was silently dropped is in exactly the position this feature exists to
90
+ * prevent, arrived at through the feature itself.
91
+ */
92
+ export function assertIdempotencyKey(key) {
93
+ if (isValidIdempotencyKey(key))
94
+ return;
95
+ throw substratError('validation_failed', 'Idempotency-Key must be 1-255 visible ASCII characters with no spaces');
96
+ }
97
+ /** The lookup a retry is answered from. */
98
+ export function idempotencyLookupQuery(subject, key) {
99
+ return {
100
+ sql: 'SELECT operation, fingerprint, result, oversized, entity_version ' +
101
+ 'FROM _substrat_idempotency WHERE subject = ? AND key = ?',
102
+ params: [idempotencySubject(subject), key],
103
+ };
104
+ }
105
+ /**
106
+ * Decide what a second request under this key gets.
107
+ *
108
+ * Two refusals and one replay, both refusals `conflict` (409) with a reason slug
109
+ * this feature owns:
110
+ *
111
+ * - **Reuse.** Same key, different request. The client's assertion that this is
112
+ * the request it sent before is false, and the one thing that must not happen
113
+ * is serving the earlier request's response to it.
114
+ * - **Unavailable.** The original response was too large to record. Refused
115
+ * rather than re-executed, which is the fail-closed direction: an error the
116
+ * caller can act on, instead of the duplicate work the key was sent to avoid.
117
+ */
118
+ export function replayFor(key, fingerprint, row) {
119
+ if (row.fingerprint !== fingerprint) {
120
+ throw substratError('conflict', `Idempotency-Key '${key}' was already used for a different request ` +
121
+ `(${row.operation}). A key identifies one request; use a fresh one`, { reason: IDEMPOTENCY_REUSED });
122
+ }
123
+ if (row.oversized !== 0) {
124
+ throw substratError('conflict', `the original response for Idempotency-Key '${key}' was too large to record, ` +
125
+ 'so this retry cannot be answered from it — the original request did complete, ' +
126
+ 'and re-running it would duplicate the work the key exists to prevent', { reason: IDEMPOTENCY_REPLAY_UNAVAILABLE });
127
+ }
128
+ return {
129
+ result: row.result === null ? undefined : JSON.parse(row.result),
130
+ entityVersion: row.entity_version,
131
+ };
132
+ }
133
+ /**
134
+ * The row a completed operation leaves behind.
135
+ *
136
+ * Serialisation happens here rather than at each call site so the size decision
137
+ * has one home: over `IDEMPOTENCY_RESULT_LIMIT` the body is dropped and the key
138
+ * is recorded as oversized, which is what makes a later replay a refusal instead
139
+ * of a silent re-execution.
140
+ *
141
+ * `undefined` and `null` results are both stored as a NULL body with
142
+ * `oversized = 0`; a replay returns `undefined` for either. An operation whose
143
+ * return value a caller distinguishes on that difference has a bigger problem
144
+ * than this table.
145
+ */
146
+ export function idempotencyRecordStatement(subject, key, operation, fingerprint, result, entityVersion, at) {
147
+ const serialised = result === undefined ? null : JSON.stringify(result) ?? null;
148
+ const oversized = serialised !== null && serialised.length > IDEMPOTENCY_RESULT_LIMIT;
149
+ return {
150
+ sql: 'INSERT INTO _substrat_idempotency ' +
151
+ '(subject, key, operation, fingerprint, result, oversized, entity_version, recorded_at) ' +
152
+ 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
153
+ params: [
154
+ idempotencySubject(subject),
155
+ key,
156
+ operation,
157
+ fingerprint,
158
+ oversized ? null : serialised,
159
+ oversized ? 1 : 0,
160
+ entityVersion,
161
+ at,
162
+ ],
163
+ };
164
+ }
165
+ /**
166
+ * Age rows out, in the same transaction as the write that added one.
167
+ *
168
+ * Opportunistic rather than swept, deliberately. A sweeper would be a second
169
+ * schedule, a second failure mode and a second thing to deploy, for a table whose
170
+ * only writer is already here holding a transaction open. Bounded work: the
171
+ * `recorded_at` index makes it a range delete, and it runs only on an invocation
172
+ * that carried a key — so a fleet that never uses the feature never pays for it.
173
+ *
174
+ * The consequence worth stating: a scope that stops receiving keyed requests
175
+ * keeps its last rows past the window. They are inert (nothing reads a row
176
+ * without a key that matches it) and the next keyed request clears them.
177
+ */
178
+ export function idempotencyPruneStatement(now) {
179
+ const cutoff = new Date(Date.parse(now) - IDEMPOTENCY_RETENTION_MS).toISOString();
180
+ return {
181
+ sql: 'DELETE FROM _substrat_idempotency WHERE recorded_at < ?',
182
+ params: [cutoff],
183
+ };
184
+ }
185
+ /**
186
+ * The refusal an operation that declared `idempotency: false` answers a key with.
187
+ *
188
+ * Not a `conflict` — nothing conflicts — and not silence, which is the failure
189
+ * mode every branch of this feature is written to avoid. The operation opted out
190
+ * because its response must not be recorded; a caller who sent a key and got a
191
+ * 200 would believe a retry is safe when the second one will execute again.
192
+ */
193
+ export function idempotencyOptedOutMessage(operation) {
194
+ return (`${operation} declares \`idempotency: false\` and cannot honour an Idempotency-Key — ` +
195
+ 'its response is not recorded, so a retry would execute it a second time');
196
+ }
197
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AccessLogFilter, AttachmentUploadInput, AuditLogFilter, BlobStoreProvisionInput, BlobStoreRecord, ConsumerHandler, ExecutorDeadLetter, ExecutorDrainReport, ExecutorHandler, ExecutorRetryPolicy, ConnectorConnection, ScopedConnectorConnection, ConnectorContext, ConnectorHandler, ConnectorOptions, ConnectorRequestInit, ConnectorResponse, Clock, FetchLike, GuardPredicate, HostAdmin, MigrateScopeOutcome, MigrationFrontier, ModuleRegistration, ConsumersOf, EventContract, EventPayloadOf, EventTypeOf, TypedConsumerHandler, TypedConsumers, OpenedAttachment, OperationContext, PageParams, OperationHandler, OpsFailureFilter, OpsFailureInput, ProvisionScopeInput, RoleFilter, ScheduleRegistration, ScheduleRunReport, ScopeAttachments, ScopedSql, ScopeFilter, ScopeHost, ScopeStub, ScopeStubOptions, SqlMigration, SqlValue, TenantBlobStore, TenantRelationalStore, TenantStoreProvisionInput, TenantStoreRecord, } from './scope-host.js';
1
+ export type { AccessLogFilter, AttachmentUploadInput, AuditLogFilter, BlobStoreProvisionInput, BlobStoreRecord, ConsumerHandler, ExecutorDeadLetter, ExecutorDrainReport, ExecutorHandler, ExecutorRetryPolicy, ConnectorConnection, ScopedConnectorConnection, ConnectorContext, ConnectorHandler, ConnectorOptions, ConnectorRequestInit, ConnectorResponse, Clock, FetchLike, GuardPredicate, HostAdmin, MigrateScopeOutcome, MigrationFrontier, ModuleRegistration, ConsumersOf, EventContract, EventPayloadOf, EventTypeOf, TypedConsumerHandler, TypedConsumers, OpenedAttachment, OperationContext, PageParams, OperationHandler, OpsFailureFilter, OpsFailureInput, ProvisionScopeInput, RoleFilter, ScheduleRegistration, ScheduleRunReport, ScopeAttachments, ScopedSql, ScopeFilter, ScopeHost, ScopeStub, ScopeStubOptions, InvokeOptions, SqlMigration, SqlValue, TenantBlobStore, TenantRelationalStore, TenantStoreProvisionInput, TenantStoreRecord, } from './scope-host.js';
2
2
  export { attachmentBlobKey, consumersFor, entitlementDenial, backoffAt, parseValidationRecords, resolveRetryPolicy, OPS_FAILURE_RETENTION_DAYS, } from './scope-host.js';
3
3
  export { isSecretBoxConfigured, SecretBoxUnconfiguredError, unconfiguredSecretBox, webCryptoSecretBox, } from './secret-box.js';
4
4
  export type { SealedSecret, SecretBox } from './secret-box.js';
@@ -25,6 +25,11 @@ export { readRoutedNode, RouterAssertionError } from './routed-node.js';
25
25
  export type { RoutedNode, HeaderReader } from './routed-node.js';
26
26
  export { assertPlatformCall, PlatformCallError, PLATFORM_SECRET_HEADER, PLATFORM_REQUEST_HEADER, CONNECTOR_ATTACHMENT_RECORD_HEADER, } from './platform-call.js';
27
27
  export { PLATFORM_REQUEST_COLUMNS, platformRequestHistoryQuery, } from './platform-request-query.js';
28
+ export { DENIAL_COLUMNS, DENIAL_WINDOW_QUERY, denialListQuery, denialSummaryQuery, denialTotalsQuery, mapDenialRow, mapDenialBucketRow, storedActor, type DenialRow, type DenialBucketRow, type DenialWindowRow, } from './denial-query.js';
29
+ export { entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, type EntityVersion, type EntityVersionRow, } from './entity-version.js';
30
+ export { readTimeline, readHistory } from './timeline.js';
31
+ export type { TimelineReader } from './timeline.js';
32
+ export { IDEMPOTENCY_DDL, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencySubject, idempotencyOptedOutMessage, replayFor, type IdempotencyRow, type IdempotentReplay, } from './idempotency.js';
28
33
  export { isTerminalDispatchFailure, isTerminalProviderError, providerErrorStatus, RETRYABLE_CLIENT_STATUSES, } from './provider-error.js';
29
34
  export { runPlatformSweep, startPlatformSweeper } from './platform-sweep.js';
30
35
  export type { AccessLogSink, AccessLogSweepReport, ConnectorSweeper, MigrationSweepReport, PlatformSweepOptions, PlatformSweepReport, PlatformSweeperHandle, ScheduleSweepReport, StartPlatformSweeperOptions, } from './platform-sweep.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -14,6 +14,10 @@ export { assertReadOnlyQuery } from './read-only-sql.js';
14
14
  export { readRoutedNode, RouterAssertionError } from './routed-node.js';
15
15
  export { assertPlatformCall, PlatformCallError, PLATFORM_SECRET_HEADER, PLATFORM_REQUEST_HEADER, CONNECTOR_ATTACHMENT_RECORD_HEADER, } from './platform-call.js';
16
16
  export { PLATFORM_REQUEST_COLUMNS, platformRequestHistoryQuery, } from './platform-request-query.js';
17
+ export { DENIAL_COLUMNS, DENIAL_WINDOW_QUERY, denialListQuery, denialSummaryQuery, denialTotalsQuery, mapDenialRow, mapDenialBucketRow, storedActor, } from './denial-query.js';
18
+ export { entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, } from './entity-version.js';
19
+ export { readTimeline, readHistory } from './timeline.js';
20
+ export { IDEMPOTENCY_DDL, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencySubject, idempotencyOptedOutMessage, replayFor, } from './idempotency.js';
17
21
  export { isTerminalDispatchFailure, isTerminalProviderError, providerErrorStatus, RETRYABLE_CLIENT_STATUSES, } from './provider-error.js';
18
22
  export { runPlatformSweep, startPlatformSweeper } from './platform-sweep.js';
19
23
  export { MIGRATION_FLAG_THRESHOLD, migrationFleet, migrationProgress, migrationSummary, scopeMigrationState, } from './migration-progress.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAsDA,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,oCAAoC,EACpC,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,MAAM,EACN,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,EACvB,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAOzB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExE,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,kCAAkC,GACnC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAY7E,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuDA,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,oCAAoC,EACpC,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,MAAM,EACN,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,EACvB,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAOzB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExE,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACvB,kCAAkC,GACnC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,2BAA2B,GAC5B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,WAAW,GAIZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,mBAAmB,GAGpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE1D,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,EAClB,0BAA0B,EAC1B,SAAS,GAGV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAY7E,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,6 +1,7 @@
1
- import type { AdminAction, ListPage, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, BindHostnameInput, AdminLogEntry, OpsFailureEntry, CapabilityGrant, CreateTenantInput, Decision, Instant, DomainEvent, DomainEventInput, PlatformRequestInput, PlatformRequestId, PlatformRequest, PlatformRequestFilter, PlatformRequestStatus, PlatformRequestFailure, EntitlementGrant, EntitlementGrantInput, EntitlementView, MeterReading, EntityRef, IdentityLink, IdentityPool, Jurisdiction, ModuleId, ModuleManifest, ScheduleSpec, SystemGrant, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, BlobStoreHandle, Visibility, Vertical, VerticalChannel, VerticalVersion, TenantStatus, Page, CountedPage } from '@substrat-run/contracts';
1
+ import type { AdminAction, ListPage, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, BindHostnameInput, AdminLogEntry, OpsFailureEntry, CapabilityGrant, CreateTenantInput, Decision, Instant, DomainEvent, DomainEventInput, PlatformRequestInput, PlatformRequestId, PlatformRequest, PlatformRequestFilter, PlatformRequestStatus, PlatformRequestFailure, EntitlementGrant, EntitlementGrantInput, EntitlementView, MeterReading, EntityRef, IdentityLink, IdentityPool, Jurisdiction, ModuleId, ModuleManifest, ScheduleSpec, SystemGrant, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, PermissionDenial, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, BlobStoreHandle, Visibility, Vertical, VerticalChannel, VerticalVersion, TenantStatus, Page, CountedPage } from '@substrat-run/contracts';
2
2
  import type { SealedSecret } from './secret-box.js';
3
3
  import type { SearchHit, SearchOptions } from './search-index.js';
4
+ import type { EntityVersion } from './entity-version.js';
4
5
  /**
5
6
  * What a caller asks a paged read for (#811).
6
7
  *
@@ -107,6 +108,39 @@ export interface OperationContext {
107
108
  * code writing `_substrat_*`), so an intent's status is only ever the platform's answer.
108
109
  */
109
110
  platformRequests(filter?: PlatformRequestFilter): PlatformRequest[];
111
+ /**
112
+ * This entity's version (#901) — the ULID of the last event about it, or
113
+ * `null` if nothing has ever been emitted about it.
114
+ *
115
+ * There is no version column anywhere, and there is deliberately not going to
116
+ * be one: `_substrat_outbox` has recorded `entity_type` and `entity_id`
117
+ * against a monotonic ULID since it was written, so every mutation that
118
+ * followed the fat-event rule already versioned the thing it touched. See
119
+ * `entity-version.ts` for why the alternative — a `_version` column bumped by
120
+ * an emitted trigger — was rejected despite working.
121
+ *
122
+ * ```ts
123
+ * const before = ctx.versionOf({ entityType: 'customer', entityId: id });
124
+ * // …mutate, emit…
125
+ * ctx.versionOf({ entityType: 'customer', entityId: id }) !== before // true
126
+ * ```
127
+ *
128
+ * **Conservative, by construction.** ANY event about the entity moves this,
129
+ * including one that changed nothing the caller read. A precondition built on
130
+ * it can refuse a write that would have been safe; it cannot admit one that
131
+ * would not. That is the correct direction to fail, and it is a real
132
+ * difference from a per-row counter.
133
+ *
134
+ * Rule 3 permits a projection read of `_substrat_*`, so a vertical *could*
135
+ * hand-roll this `SELECT`. It should not: the spine's schema is private and a
136
+ * vertical pinned to it is pinned to a table the kernel may re-shape. Same
137
+ * reasoning as `platformRequests`.
138
+ *
139
+ * **Checks no permission** — nothing on `ctx` does. A version is not a read of
140
+ * the entity, but it is evidence the entity exists, so an operation that hands
141
+ * one to an untrusted caller does its own `assertAllowed` first.
142
+ */
143
+ versionOf(entity: EntityRef): EntityVersion | null;
110
144
  /** Node-level check; pass `entity` for per-entity checks (portal access, §4.2 rule 3). */
111
145
  check(permission: PermissionKey, entity?: EntityRef): Promise<Decision>;
112
146
  /**
@@ -284,11 +318,85 @@ export interface OperationContext {
284
318
  atomic<T>(fn: () => T | Promise<T>): Promise<T>;
285
319
  }
286
320
  export type OperationHandler<I = unknown, O = unknown> = (ctx: OperationContext, input: I) => O | Promise<O>;
321
+ /**
322
+ * The per-invocation transport channel: what the caller requires to be true
323
+ * before the operation runs, and the transport facts it needs back (#129, #116).
324
+ *
325
+ * A separate parameter rather than fields on the input, because these are facts
326
+ * about the REQUEST and not about the domain. A handler's declared input is what
327
+ * the operation MEANS, and threading a retry token or an entity tag through it
328
+ * would make every in-process caller state something it does not have.
329
+ * `mountOperations` reads them off headers; a test, a seed or a schedule omits
330
+ * them entirely.
331
+ *
332
+ * Per INVOCATION rather than on `ScopeStubOptions`, where `onPlatformRequests`
333
+ * lives, and the difference is not stylistic. A stub is minted by the vertical's
334
+ * own `resolveStub`, so anything hung off it requires that vertical to cooperate;
335
+ * `If-Match` and the `ETag` are wholly the mount's business and must work with no
336
+ * change to a vertical at all. They are also genuinely per-call — one stub serves
337
+ * one request, but nothing in the contract says so.
338
+ *
339
+ * Deliberately one bag rather than a parameter per concern. `If-Match` and
340
+ * `Idempotency-Key` are ONE precondition pass at one point in the invoke — before
341
+ * the guards, inside the transaction — which is what #116's note asked of
342
+ * whichever landed first. #129 built the bag; #116 declared into it and added no
343
+ * second interception point.
344
+ */
345
+ export interface InvokeOptions {
346
+ /**
347
+ * The version the caller believes it is writing over, verbatim from `If-Match`
348
+ * (quoted, and possibly a list — `ifMatchAdmits` owns the parsing).
349
+ *
350
+ * Honoured only by an operation that DECLARES `concurrency`. Sending it to one
351
+ * that does not is an error rather than a no-op: a caller who believes it is
352
+ * protected and is not is the failure this whole mechanism exists to prevent,
353
+ * and silence is exactly how that belief survives.
354
+ */
355
+ readonly ifMatch?: string;
356
+ /**
357
+ * Called after a guarded operation COMMITS, with the entity's version as it
358
+ * stands at commit — the `ETag` the transport hands back.
359
+ *
360
+ * Read after the handler and inside the same transaction, so the tag describes
361
+ * the row as the caller's own write left it rather than as the caller found it.
362
+ * A client that echoed back what it sent would loop on its own stale value.
363
+ *
364
+ * Never called for a rolled-back operation, and never for an operation that
365
+ * declares no `concurrency`: a version that did not survive its transaction is
366
+ * not a tag anyone may hold, and an operation that opted out must not pay for a
367
+ * spine read on every invocation.
368
+ */
369
+ readonly onEntityVersion?: (version: string | null) => void;
370
+ /**
371
+ * The client's retry token, verbatim from `Idempotency-Key` (#116).
372
+ *
373
+ * Honoured by every operation on an unsafe method — there is no declaration to
374
+ * make, because a retried write creating a second entity is a hazard on all of
375
+ * them. The exception is an operation that declared `idempotency: false`, whose
376
+ * response must not be recorded; sending a key to one is an error rather than a
377
+ * no-op, for the same reason an unhonoured `If-Match` is.
378
+ *
379
+ * A first request under a key runs, and its return value is recorded inside the
380
+ * operation's own transaction. A second request under the same key returns that
381
+ * recording without running the handler. A second request under the same key
382
+ * with a DIFFERENT input is refused — a key names one request, and serving the
383
+ * first one's response to a second one would be a lie a client acts on.
384
+ */
385
+ readonly idempotencyKey?: string;
386
+ /**
387
+ * Called when this invocation was answered from a recording rather than run.
388
+ *
389
+ * The transport sets `Idempotency-Replayed` from it. Advisory: a caller that
390
+ * ignores this is not wrong about anything, it simply cannot tell a retry from
391
+ * a first request — which is enough of a debugging cost to be worth a callback.
392
+ */
393
+ readonly onIdempotentReplay?: () => void;
394
+ }
287
395
  /** The capability stub — the ONLY way code outside the scope reaches it. */
288
396
  export interface ScopeStub {
289
397
  readonly tenantId: TenantId;
290
398
  readonly scopeId: ScopeId;
291
- invoke<O = unknown, I = unknown>(operation: string, input?: I): Promise<O>;
399
+ invoke<O = unknown, I = unknown>(operation: string, input?: I, options?: InvokeOptions): Promise<O>;
292
400
  }
293
401
  /**
294
402
  * Observers a caller may attach when minting a stub (#458). Harness-level, not
@@ -801,6 +909,80 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
801
909
  manifest: ModuleManifest;
802
910
  migrations?: SqlMigration[];
803
911
  operations?: Record<string, OperationHandler<never, unknown>>;
912
+ /**
913
+ * name → the schema the host parses an invocation's input against, BEFORE the
914
+ * guards and the handler see it (#893).
915
+ *
916
+ * Derived from the declared operation surface — `operationInputsOf(ops)` — and
917
+ * never written a second time. A module that declares its operations gets the
918
+ * parse by handing the same object over:
919
+ *
920
+ * ```ts
921
+ * operations: { 'rally/book': bookOp, … },
922
+ * operationInputs: operationInputsOf(rallyOperations),
923
+ * ```
924
+ *
925
+ * **This is where "parse, don't trust" is kept, rather than in 85 handlers.**
926
+ * `OperationShape.input` calls itself *"the SAME Zod object the handler
927
+ * parses"* and across the fleet it mostly was not — rally declared 32 inputs
928
+ * and parsed 2. One place that cannot be forgotten beats a rule every new
929
+ * operation has to remember, which is the same argument `mountOperations`
930
+ * already makes for the page trio.
931
+ *
932
+ * A name here that no operation binds is an error: it is a schema enforcing
933
+ * nothing, and it reads as coverage. A bound operation with no entry is
934
+ * allowed and means what it always meant — nothing was declared to parse.
935
+ *
936
+ * Typed structurally rather than as `z.ZodType` so the kernel keeps its single
937
+ * dependency and no zod version is pinned by the scope-host contract. The
938
+ * shape is the whole surface the host uses: throw to refuse, return the value
939
+ * to accept.
940
+ */
941
+ operationInputs?: Record<string, {
942
+ parse(value: unknown): unknown;
943
+ }>;
944
+ /**
945
+ * name → the entity whose version this operation's `If-Match` is compared
946
+ * against, and the input field carrying its id (#129).
947
+ *
948
+ * Derived from the declared operation surface — `operationConcurrencyOf(ops)` —
949
+ * and never written a second time, exactly as `operationInputs` is:
950
+ *
951
+ * ```ts
952
+ * operations: { 'callout/update-customer': updateCustomerOp, … },
953
+ * operationInputs: operationInputsOf(calloutOperations),
954
+ * operationConcurrency: operationConcurrencyOf(calloutOperations),
955
+ * ```
956
+ *
957
+ * **The host compares, not the handler.** A precondition a handler evaluates is
958
+ * a precondition a handler can forget, and the one that is forgotten is
959
+ * indistinguishable from one that passed. Here the comparison happens between
960
+ * `BEGIN` and the guards for every caller and every transport, or the operation
961
+ * does not claim to have it.
962
+ *
963
+ * A name here that no operation binds is an error, for the same reason it is on
964
+ * `operationInputs`: it reads as coverage while enforcing nothing.
965
+ */
966
+ operationConcurrency?: Record<string, {
967
+ entity: string;
968
+ idFrom: string;
969
+ }>;
970
+ /**
971
+ * The operations that declared `idempotency: false` (#116) — the ones whose
972
+ * response must not be recorded, and which therefore refuse an
973
+ * `Idempotency-Key` instead of honouring it.
974
+ *
975
+ * Derived like the two above, and never written a second time:
976
+ *
977
+ * ```ts
978
+ * operationIdempotencyOptOuts: operationIdempotencyOptOutsOf(calloutOperations),
979
+ * ```
980
+ *
981
+ * A list of refusals rather than a list of participants, because that is what
982
+ * the declaration is. Absent means every operation honours a key, which is the
983
+ * default and the reason there is nothing to remember.
984
+ */
985
+ operationIdempotencyOptOuts?: readonly string[];
804
986
  /**
805
987
  * eventType → handler; the types must appear in manifest.events.consumes.
806
988
  *
@@ -1322,6 +1504,18 @@ export interface HostAdmin {
1322
1504
  * the spine is kept because a fork must carry the event/migration state to be faithful.
1323
1505
  */
1324
1506
  exportScope(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId): Promise<ScopeDump>;
1507
+ /**
1508
+ * A bounded page of raw denial rows, newest first. Narrow with `actor` (who was
1509
+ * refused), `permission` (which key), `operation`, and a `since`/`until` window.
1510
+ */
1511
+ listDenials(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise<PermissionDenial[]>;
1512
+ /**
1513
+ * The same log bucketed per (actor, permission) — K-35's "first occurrence + count
1514
+ * per actor/key/window" — busiest first, with the filtered totals and the unfiltered
1515
+ * window facts beside them. This is the view an operator opens first: "who has been
1516
+ * probing for access they don't hold" is a question about counts, not about rows.
1517
+ */
1518
+ summarizeDenials(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise<DenialSummary>;
1325
1519
  /**
1326
1520
  * A COMPLETE dump of the directory itself: tenants, scopes, hostnames, verticals,
1327
1521
  * entitlements, identities, and the audit spine. The platform-level analogue of
@@ -1909,6 +2103,13 @@ export declare function attachmentBlobKey(scopeId: string, attachmentId: string)
1909
2103
  * `held` is every key projected for the tenant, expired ones included and marked: a lapsed
1910
2104
  * grant denies exactly like an absent one, and "you have it, it ran out" is a different fix
1911
2105
  * from "you never had it".
2106
+ *
2107
+ * **Its code is `not_found`** at all three throw sites (#113). Not an oversight and not
2108
+ * `forbidden`: the taxonomy's `not_found` row already covers "exists, and must read as
2109
+ * absent" for K-3's cross-tenant case, and every vertical had independently arrived at a
2110
+ * 404 here for the same reason — *"a 403 would confirm the feature exists"*
2111
+ * ([`todo/routes.ts`](../../../demos/todo/src/routes.ts)). Naming the code once is what
2112
+ * retires those hand-written patterns.
1912
2113
  */
1913
2114
  export declare function entitlementDenial(operation: string, requiredKey: string, held: readonly {
1914
2115
  key: string;