@substrat-run/kernel 0.88.0 → 0.90.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.
@@ -12,7 +12,7 @@ import { type DenialBucket, type DenialFilter, type PermissionDenial } from '@su
12
12
  * arrive from an HTTP query string. Values are bound, never interpolated.
13
13
  */
14
14
  /** Every column of `_substrat_denials`, in the order `mapDenialRow` expects. */
15
- export declare const DENIAL_COLUMNS = "id, actor, permission, tenant_id, scope_id, operation, at, drained_at";
15
+ export declare const DENIAL_COLUMNS = "id, actor, permission, tenant_id, scope_id, operation, impersonation, at, drained_at";
16
16
  /** The raw row shape, as either adapter hands it back. */
17
17
  export interface DenialRow {
18
18
  id: string;
@@ -21,6 +21,8 @@ export interface DenialRow {
21
21
  tenant_id: string;
22
22
  scope_id: string | null;
23
23
  operation: string | null;
24
+ /** K-42: the staff actor + session, as JSON, when the refusal was under one. */
25
+ impersonation: string | null;
24
26
  at: string;
25
27
  drained_at: string | null;
26
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"denial-query.d.ts","sourceRoot":"","sources":["../src/denial-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAItB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AAEH,gFAAgF;AAChF,eAAO,MAAM,cAAc,0EAA0E,CAAC;AAEtG,0DAA0D;AAC1D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,gBAAgB,CAW7D;AAgCD,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;CAAE,CAOnG;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CAC7B,CAYA;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,YAAY,CASrE;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CAC7B,CAOA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,QAGL,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB"}
1
+ {"version":3,"file":"denial-query.d.ts","sourceRoot":"","sources":["../src/denial-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,YAAY,EAEjB,KAAK,gBAAgB,EAItB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AAEH,gFAAgF;AAChF,eAAO,MAAM,cAAc,yFAC6D,CAAC;AAEzF,0DAA0D;AAC1D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gFAAgF;IAChF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,gBAAgB,CAa7D;AAgCD,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;CAAE,CAOnG;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CAC7B,CAYA;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,YAAY,CASrE;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CAC7B,CAOA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,QAGL,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB"}
@@ -12,7 +12,7 @@ import { DEFAULT_DENIAL_LIMIT, denialFilter, } from '@substrat-run/contracts';
12
12
  * arrive from an HTTP query string. Values are bound, never interpolated.
13
13
  */
14
14
  /** Every column of `_substrat_denials`, in the order `mapDenialRow` expects. */
15
- export const DENIAL_COLUMNS = 'id, actor, permission, tenant_id, scope_id, operation, at, drained_at';
15
+ export const DENIAL_COLUMNS = 'id, actor, permission, tenant_id, scope_id, operation, impersonation, at, drained_at';
16
16
  /**
17
17
  * The stored spelling of an actor. The writer persists `JSON.stringify(actor)`, so a
18
18
  * principal is stored WITH its quotes (`"01J…"`) while a system or connection actor is
@@ -39,6 +39,7 @@ export function mapDenialRow(row) {
39
39
  tenantId: row.tenant_id,
40
40
  scopeId: (row.scope_id ?? null),
41
41
  operation: row.operation ?? null,
42
+ impersonation: row.impersonation == null ? null : JSON.parse(row.impersonation),
42
43
  at: row.at,
43
44
  drainedAt: row.drained_at ?? null,
44
45
  };
@@ -1 +1 @@
1
- {"version":3,"file":"denial-query.js","sourceRoot":"","sources":["../src/denial-query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"denial-query.js","sourceRoot":"","sources":["../src/denial-query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { type CheckSubject } 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 declare const IDEMPOTENCY_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_idempotency (\n subject TEXT NOT NULL,\n key TEXT NOT NULL,\n operation TEXT NOT NULL,\n -- SHA-256 over (operation, parsed input). A second request under this key\n -- whose fingerprint differs is a REUSE, refused with 409 \u2014 never served the\n -- first request's response, which is a different request's answer.\n fingerprint TEXT NOT NULL,\n -- The operation's return value as JSON. NULL means one of two things, which\n -- `oversized` separates: the operation returned nothing, or the result was\n -- too large to record and a replay must be refused rather than re-executed.\n result TEXT,\n oversized INTEGER NOT NULL DEFAULT 0,\n -- #129's tag, replayed with the body so a retry hands the client the same\n -- ETag the original did. Without it a replayed response has no validator and\n -- the client's next conditional write has nothing to send.\n entity_version TEXT,\n recorded_at TEXT NOT NULL,\n PRIMARY KEY (subject, key)\n );\n CREATE INDEX IF NOT EXISTS _substrat_idempotency_recorded\n ON _substrat_idempotency (recorded_at);\n";
56
+ /** The stored row, as both adapters' SQL returns it. */
57
+ export interface IdempotencyRow {
58
+ readonly operation: string;
59
+ readonly fingerprint: string;
60
+ readonly result: string | null;
61
+ readonly oversized: number;
62
+ readonly entity_version: string | null;
63
+ }
64
+ /** What a replay answers with: the recorded return value and the recorded tag. */
65
+ export interface IdempotentReplay {
66
+ readonly result: unknown;
67
+ readonly entityVersion: string | null;
68
+ }
69
+ /** The subject a key is scoped to, in the form the column stores. */
70
+ export declare function idempotencySubject(subject: CheckSubject): string;
71
+ /**
72
+ * Refuse a malformed key at the door.
73
+ *
74
+ * `validation_failed` rather than `conflict`: nothing is in conflict, the caller
75
+ * sent a header we cannot store. Refused rather than ignored, for the reason the
76
+ * `If-Match` path gives — a caller who believes their retry is safe and whose
77
+ * key was silently dropped is in exactly the position this feature exists to
78
+ * prevent, arrived at through the feature itself.
79
+ */
80
+ export declare function assertIdempotencyKey(key: string): void;
81
+ /** The lookup a retry is answered from. */
82
+ export declare function idempotencyLookupQuery(subject: CheckSubject, key: string): {
83
+ sql: string;
84
+ params: [string, string];
85
+ };
86
+ /**
87
+ * Decide what a second request under this key gets.
88
+ *
89
+ * Two refusals and one replay, both refusals `conflict` (409) with a reason slug
90
+ * this feature owns:
91
+ *
92
+ * - **Reuse.** Same key, different request. The client's assertion that this is
93
+ * the request it sent before is false, and the one thing that must not happen
94
+ * is serving the earlier request's response to it.
95
+ * - **Unavailable.** The original response was too large to record. Refused
96
+ * rather than re-executed, which is the fail-closed direction: an error the
97
+ * caller can act on, instead of the duplicate work the key was sent to avoid.
98
+ */
99
+ export declare function replayFor(key: string, fingerprint: string, row: IdempotencyRow): IdempotentReplay;
100
+ /**
101
+ * The row a completed operation leaves behind.
102
+ *
103
+ * Serialisation happens here rather than at each call site so the size decision
104
+ * has one home: over `IDEMPOTENCY_RESULT_LIMIT` the body is dropped and the key
105
+ * is recorded as oversized, which is what makes a later replay a refusal instead
106
+ * of a silent re-execution.
107
+ *
108
+ * `undefined` and `null` results are both stored as a NULL body with
109
+ * `oversized = 0`; a replay returns `undefined` for either. An operation whose
110
+ * return value a caller distinguishes on that difference has a bigger problem
111
+ * than this table.
112
+ */
113
+ export declare function idempotencyRecordStatement(subject: CheckSubject, key: string, operation: string, fingerprint: string, result: unknown, entityVersion: string | null, at: string): {
114
+ sql: string;
115
+ params: [string, string, string, string, string | null, number, string | null, string];
116
+ };
117
+ /**
118
+ * Age rows out, in the same transaction as the write that added one.
119
+ *
120
+ * Opportunistic rather than swept, deliberately. A sweeper would be a second
121
+ * schedule, a second failure mode and a second thing to deploy, for a table whose
122
+ * only writer is already here holding a transaction open. Bounded work: the
123
+ * `recorded_at` index makes it a range delete, and it runs only on an invocation
124
+ * that carried a key — so a fleet that never uses the feature never pays for it.
125
+ *
126
+ * The consequence worth stating: a scope that stops receiving keyed requests
127
+ * keeps its last rows past the window. They are inert (nothing reads a row
128
+ * without a key that matches it) and the next keyed request clears them.
129
+ */
130
+ export declare function idempotencyPruneStatement(now: string): {
131
+ sql: string;
132
+ params: [string];
133
+ };
134
+ /**
135
+ * The refusal an operation that declared `idempotency: false` answers a key with.
136
+ *
137
+ * Not a `conflict` — nothing conflicts — and not silence, which is the failure
138
+ * mode every branch of this feature is written to avoid. The operation opted out
139
+ * because its response must not be recorded; a caller who sent a key and got a
140
+ * 200 would believe a retry is safe when the second one will execute again.
141
+ */
142
+ export declare function idempotencyOptedOutMessage(operation: string): string;
143
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,+nCAuB3B,CAAC;AAEF,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMtD;AAED,2CAA2C;AAC3C,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,GACV;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAO3C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,cAAc,GAClB,gBAAgB,CAsBlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,EACf,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,EAAE,EAAE,MAAM,GACT;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,CAAA;CAAE,CAmBzG;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAA;CAAE,CAMxF;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKpE"}
@@ -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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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The rules an impersonated session is held to (K-42, #868) — kernel-side, so
3
+ * both adapters enforce the same ones.
4
+ *
5
+ * An adapter supplies storage and a clock. Everything that decides whether a
6
+ * session may be opened, whether it may still be used, and what it may do lives
7
+ * here: a rule implemented twice is a rule that will eventually be two rules,
8
+ * and this is not a surface where the two versions may drift.
9
+ *
10
+ * What is deliberately NOT here: the permission evaluation. An impersonated
11
+ * operation checks as the impersonated principal through the ordinary
12
+ * `PermissionChecker`, with no override branch anywhere — the same discipline
13
+ * that made `system:<moduleId>` a subject rather than a bypass (#383). A door
14
+ * that grants authority by opening is a door nobody can audit.
15
+ */
16
+ import { SubstratError, type BeginImpersonationInput, type ImpersonationFilter, type ImpersonationSession, type ImpersonationSessionId, type ImpersonationStamp, type Instant, type PlatformActorId, type ScopeId, type TenantId } from '@substrat-run/contracts';
17
+ /**
18
+ * A session refused: expired, ended, pointed at the wrong scope, or asked to do
19
+ * something a `read-only` session may not.
20
+ *
21
+ * `forbidden` rather than `permission_denied`, and the distinction is worth the
22
+ * separate class: the principal being impersonated may well hold the permission,
23
+ * and the check may well have passed. What refused is the SESSION. Recording
24
+ * this in the denial log would put a row there about a permission nobody was
25
+ * actually denied.
26
+ */
27
+ export declare class ImpersonationRefused extends SubstratError {
28
+ constructor(message: string);
29
+ }
30
+ /**
31
+ * Mint a session from a staff request. Pure: the caller supplies the id and the
32
+ * instant, so a frozen clock produces a deterministic session (#812) and the
33
+ * adapter keeps its one source of ULIDs.
34
+ *
35
+ * `minutes` is CAPPED BY THE SCHEMA rather than clamped here — a caller asking
36
+ * for a day is refused with a parse error naming the ceiling, because silently
37
+ * handing back a shorter session than was asked for is how somebody comes to
38
+ * believe they have one that is still open.
39
+ */
40
+ export declare function newImpersonationSession(id: string, actor: PlatformActorId, input: BeginImpersonationInput, now: Instant): ImpersonationSession;
41
+ /**
42
+ * Is this session usable, right now, for this scope?
43
+ *
44
+ * Called at the DOOR and again on EVERY INVOKE. Twice deliberately: a stub is a
45
+ * capability and nothing forces a caller to drop it, so a session checked only
46
+ * when the stub was minted would be a session whose expiry meant nothing to the
47
+ * one caller holding it — the exact shape of a time-box that is not one.
48
+ *
49
+ * Expiry is compared as ISO text, which sorts lexicographically, so neither
50
+ * adapter parses a date to answer it.
51
+ */
52
+ export declare function assertSessionUsable(session: ImpersonationSession, now: Instant, at?: {
53
+ tenantId: TenantId;
54
+ scopeId: ScopeId;
55
+ }): void;
56
+ /**
57
+ * The effecting verbs a `read-only` session refuses, by name.
58
+ *
59
+ * Refusing at the verb is the half a person notices; the transaction being
60
+ * rolled back rather than committed is the half that is actually load-bearing,
61
+ * since `ctx.sql.exec` can write a row without going through any of these. Both
62
+ * are needed: without the rollback the guarantee is a promise about which
63
+ * functions module code happens to call, and without the refusal a support
64
+ * engineer watches a save appear to succeed and silently vanish.
65
+ */
66
+ export declare function assertImpersonationWrites(session: ImpersonationSession | undefined, verb: string): void;
67
+ /** What the kernel stamps on a record raised under this session. */
68
+ export declare function impersonationStampOf(session: ImpersonationSession): ImpersonationStamp;
69
+ /** Every column of `_substrat_impersonations`, in the order `mapImpersonationRow` expects. */
70
+ export declare const IMPERSONATION_COLUMNS = "id, actor, principal, tenant_id, scope_id, reason, mode, started_at, expires_at, ended_at";
71
+ /** The raw row shape, as either adapter hands it back. */
72
+ export interface ImpersonationRow {
73
+ id: string;
74
+ actor: string;
75
+ principal: string;
76
+ tenant_id: string;
77
+ scope_id: string;
78
+ reason: string;
79
+ mode: string;
80
+ started_at: string;
81
+ expires_at: string;
82
+ ended_at: string | null;
83
+ }
84
+ export declare function mapImpersonationRow(row: ImpersonationRow): ImpersonationSession;
85
+ export declare function impersonationRowValues(session: ImpersonationSession): (string | null)[];
86
+ /** Read one session by id — the door's own lookup, shared so both adapters spell it once. */
87
+ export declare function impersonationByIdQuery(id: ImpersonationSessionId | string): {
88
+ sql: string;
89
+ params: string[];
90
+ };
91
+ /**
92
+ * A bounded page of sessions, newest first. `active` is resolved against the
93
+ * caller's `now` rather than a stored flag: a session becomes inactive by the
94
+ * clock moving, and a flag would need somebody to come and set it.
95
+ */
96
+ export declare function impersonationListQuery(filter: ImpersonationFilter | undefined, now: Instant): {
97
+ sql: string;
98
+ params: (string | number)[];
99
+ };
100
+ /** The DDL both adapters create the session store from — one spelling, one shape. */
101
+ export declare const IMPERSONATION_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_impersonations (\n id TEXT PRIMARY KEY,\n -- The REAL actor. Never a principal: a platform actor is branded apart from\n -- one precisely so a staff member can never read as a person in a trail.\n actor TEXT NOT NULL,\n principal TEXT NOT NULL,\n tenant_id TEXT NOT NULL,\n scope_id TEXT NOT NULL,\n reason TEXT NOT NULL,\n mode TEXT NOT NULL,\n started_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n -- Explicitly closed, as distinct from expired. Never deleted (K-21): a\n -- session that once existed is why some rows carry the stamp they do.\n ended_at TEXT\n );\n CREATE INDEX IF NOT EXISTS _substrat_impersonations_tenant ON _substrat_impersonations (tenant_id, id);\n CREATE INDEX IF NOT EXISTS _substrat_impersonations_actor ON _substrat_impersonations (actor, id);\n";
102
+ //# sourceMappingURL=impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impersonation.d.ts","sourceRoot":"","sources":["../src/impersonation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAOL,aAAa,EACb,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,OAAO,EACZ,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;GASG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,KAAK,EAAE,uBAAuB,EAC9B,GAAG,EAAE,OAAO,GACX,oBAAoB,CAgBtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,OAAO,EACZ,EAAE,CAAC,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC5C,IAAI,CAmBN;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,oBAAoB,GAAG,SAAS,EACzC,IAAI,EAAE,MAAM,GACX,IAAI,CAMN;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG,kBAAkB,CAEtF;AAED,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,8FAC2D,CAAC;AAE9F,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,oBAAoB,CAa/E;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAavF;AAED,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,sBAAsB,GAAG,MAAM,GAAG;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAKA;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,mBAAmB,GAAG,SAAS,EACvC,GAAG,EAAE,OAAO,GACX;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;CAAE,CAgC9C;AAED,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,04BAmB7B,CAAC"}