@substrat-run/kernel 0.138.0 → 0.140.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 (164) hide show
  1. package/dist/admin-log-ddl.d.ts +21 -0
  2. package/dist/admin-log-ddl.d.ts.map +1 -0
  3. package/dist/admin-log-ddl.js +32 -0
  4. package/dist/admin-log-ddl.js.map +1 -0
  5. package/dist/attachment-extractor.d.ts +21 -2
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +31 -6
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/attachment-text.d.ts +64 -0
  10. package/dist/attachment-text.d.ts.map +1 -1
  11. package/dist/attachment-text.js +87 -16
  12. package/dist/attachment-text.js.map +1 -1
  13. package/dist/attribution.d.ts +36 -3
  14. package/dist/attribution.d.ts.map +1 -1
  15. package/dist/attribution.js +21 -8
  16. package/dist/attribution.js.map +1 -1
  17. package/dist/audit-outcome.d.ts +149 -0
  18. package/dist/audit-outcome.d.ts.map +1 -0
  19. package/dist/audit-outcome.js +233 -0
  20. package/dist/audit-outcome.js.map +1 -0
  21. package/dist/connect-links.d.ts +91 -0
  22. package/dist/connect-links.d.ts.map +1 -0
  23. package/dist/connect-links.js +189 -0
  24. package/dist/connect-links.js.map +1 -0
  25. package/dist/connect-state.d.ts +12 -3
  26. package/dist/connect-state.d.ts.map +1 -1
  27. package/dist/connect-state.js +8 -3
  28. package/dist/connect-state.js.map +1 -1
  29. package/dist/derived-object.d.ts +14 -0
  30. package/dist/derived-object.d.ts.map +1 -0
  31. package/dist/derived-object.js +2 -0
  32. package/dist/derived-object.js.map +1 -0
  33. package/dist/derived-objects.d.ts +108 -0
  34. package/dist/derived-objects.d.ts.map +1 -0
  35. package/dist/derived-objects.js +199 -0
  36. package/dist/derived-objects.js.map +1 -0
  37. package/dist/email-intent.d.ts +56 -0
  38. package/dist/email-intent.d.ts.map +1 -0
  39. package/dist/email-intent.js +71 -0
  40. package/dist/email-intent.js.map +1 -0
  41. package/dist/entity-edges.d.ts +6 -1
  42. package/dist/entity-edges.d.ts.map +1 -1
  43. package/dist/entity-edges.js +13 -0
  44. package/dist/entity-edges.js.map +1 -1
  45. package/dist/entity-grant-shape.d.ts +94 -0
  46. package/dist/entity-grant-shape.d.ts.map +1 -0
  47. package/dist/entity-grant-shape.js +329 -0
  48. package/dist/entity-grant-shape.js.map +1 -0
  49. package/dist/entity-grant.d.ts +45 -0
  50. package/dist/entity-grant.d.ts.map +1 -0
  51. package/dist/entity-grant.js +71 -0
  52. package/dist/entity-grant.js.map +1 -0
  53. package/dist/entity-state-reads.d.ts +40 -0
  54. package/dist/entity-state-reads.d.ts.map +1 -0
  55. package/dist/entity-state-reads.js +91 -0
  56. package/dist/entity-state-reads.js.map +1 -0
  57. package/dist/entity-state.d.ts +204 -0
  58. package/dist/entity-state.d.ts.map +1 -0
  59. package/dist/entity-state.js +356 -0
  60. package/dist/entity-state.js.map +1 -0
  61. package/dist/entity-trash.d.ts +245 -0
  62. package/dist/entity-trash.d.ts.map +1 -0
  63. package/dist/entity-trash.js +380 -0
  64. package/dist/entity-trash.js.map +1 -0
  65. package/dist/findings.d.ts +143 -0
  66. package/dist/findings.d.ts.map +1 -0
  67. package/dist/findings.js +463 -0
  68. package/dist/findings.js.map +1 -0
  69. package/dist/grant-scoped-read.d.ts +41 -0
  70. package/dist/grant-scoped-read.d.ts.map +1 -0
  71. package/dist/grant-scoped-read.js +100 -0
  72. package/dist/grant-scoped-read.js.map +1 -0
  73. package/dist/index.d.ts +40 -8
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +26 -6
  76. package/dist/index.js.map +1 -1
  77. package/dist/kernel-outbox.d.ts +14 -0
  78. package/dist/kernel-outbox.d.ts.map +1 -0
  79. package/dist/kernel-outbox.js +37 -0
  80. package/dist/kernel-outbox.js.map +1 -0
  81. package/dist/list-index.d.ts +34 -3
  82. package/dist/list-index.d.ts.map +1 -1
  83. package/dist/list-index.js +83 -31
  84. package/dist/list-index.js.map +1 -1
  85. package/dist/mail-sender.d.ts +78 -0
  86. package/dist/mail-sender.d.ts.map +1 -0
  87. package/dist/mail-sender.js +2 -0
  88. package/dist/mail-sender.js.map +1 -0
  89. package/dist/membership-executor.d.ts +7 -47
  90. package/dist/membership-executor.d.ts.map +1 -1
  91. package/dist/membership-executor.js +28 -53
  92. package/dist/membership-executor.js.map +1 -1
  93. package/dist/migration-digest.d.ts +161 -0
  94. package/dist/migration-digest.d.ts.map +1 -0
  95. package/dist/migration-digest.js +237 -0
  96. package/dist/migration-digest.js.map +1 -0
  97. package/dist/module-erasure.d.ts +156 -0
  98. package/dist/module-erasure.d.ts.map +1 -0
  99. package/dist/module-erasure.js +440 -0
  100. package/dist/module-erasure.js.map +1 -0
  101. package/dist/module-migrations.d.ts +6 -3
  102. package/dist/module-migrations.d.ts.map +1 -1
  103. package/dist/module-migrations.js +12 -5
  104. package/dist/module-migrations.js.map +1 -1
  105. package/dist/peer.d.ts +9 -1
  106. package/dist/peer.d.ts.map +1 -1
  107. package/dist/peer.js +29 -0
  108. package/dist/peer.js.map +1 -1
  109. package/dist/permission-checker.d.ts +6 -0
  110. package/dist/permission-checker.d.ts.map +1 -1
  111. package/dist/permission-checker.js.map +1 -1
  112. package/dist/permission-eval.d.ts +51 -1
  113. package/dist/permission-eval.d.ts.map +1 -1
  114. package/dist/permission-eval.js +104 -2
  115. package/dist/permission-eval.js.map +1 -1
  116. package/dist/platform-sweep.d.ts +8 -2
  117. package/dist/platform-sweep.d.ts.map +1 -1
  118. package/dist/platform-sweep.js +17 -0
  119. package/dist/platform-sweep.js.map +1 -1
  120. package/dist/scope-host.d.ts +479 -24
  121. package/dist/scope-host.d.ts.map +1 -1
  122. package/dist/scope-host.js +82 -7
  123. package/dist/scope-host.js.map +1 -1
  124. package/dist/scope-role-admin.d.ts +68 -0
  125. package/dist/scope-role-admin.d.ts.map +1 -0
  126. package/dist/scope-role-admin.js +80 -0
  127. package/dist/scope-role-admin.js.map +1 -0
  128. package/dist/search-index.d.ts +21 -1
  129. package/dist/search-index.d.ts.map +1 -1
  130. package/dist/search-index.js +60 -37
  131. package/dist/search-index.js.map +1 -1
  132. package/dist/spine-guard.d.ts +71 -2
  133. package/dist/spine-guard.d.ts.map +1 -1
  134. package/dist/spine-guard.js +393 -10
  135. package/dist/spine-guard.js.map +1 -1
  136. package/dist/spine-restore.d.ts.map +1 -1
  137. package/dist/spine-restore.js +5 -0
  138. package/dist/spine-restore.js.map +1 -1
  139. package/dist/sql-identifier.d.ts +12 -0
  140. package/dist/sql-identifier.d.ts.map +1 -0
  141. package/dist/sql-identifier.js +17 -0
  142. package/dist/sql-identifier.js.map +1 -0
  143. package/dist/sql-statements.d.ts +71 -0
  144. package/dist/sql-statements.d.ts.map +1 -0
  145. package/dist/sql-statements.js +191 -0
  146. package/dist/sql-statements.js.map +1 -0
  147. package/dist/subject-redaction.d.ts +9 -1
  148. package/dist/subject-redaction.d.ts.map +1 -1
  149. package/dist/subject-redaction.js.map +1 -1
  150. package/dist/system-switch-record.d.ts +3 -0
  151. package/dist/system-switch-record.d.ts.map +1 -1
  152. package/dist/system-switch-record.js +2 -0
  153. package/dist/system-switch-record.js.map +1 -1
  154. package/dist/table-ownership.d.ts +103 -0
  155. package/dist/table-ownership.d.ts.map +1 -0
  156. package/dist/table-ownership.js +285 -0
  157. package/dist/table-ownership.js.map +1 -0
  158. package/dist/timeline.d.ts +4 -4
  159. package/dist/timeline.js +4 -4
  160. package/dist/visible-continuation.d.ts +38 -0
  161. package/dist/visible-continuation.d.ts.map +1 -0
  162. package/dist/visible-continuation.js +162 -0
  163. package/dist/visible-continuation.js.map +1 -0
  164. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAO,EACP,UAAsB,EACtB,UAAkC;IAElC,IAAI,SAAgC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE;QAC3B,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,IAAI,IAAI,KAAK,YAAY;gBAAE,OAAO,UAAU,CAAC;YAC7C,IAAI,IAAI,KAAK,OAAO;gBAAE,OAAO,SAAS,CAAC;YACvC,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;KACF,CAAC,CAAC;IACH,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAO,EACP,WAA4B,EAC5B,UAAkC;IAElC,IAAI,SAAgC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE;QAC3B,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,IAAI,IAAI,KAAK,YAAY,IAAI,WAAW,CAAC,UAAU,KAAK,SAAS;gBAAE,OAAO,WAAW,CAAC,UAAU,CAAC;YACjG,IAAI,IAAI,KAAK,UAAU,IAAI,WAAW,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,WAAW,CAAC,QAAQ,CAAC;YAC3F,IAAI,IAAI,KAAK,OAAO;gBAAE,OAAO,CAAC,SAAS,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACnE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;KACF,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAO,EACP,UAAsB,EACtB,UAAkC;IAElC,OAAO,cAAc,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,149 @@
1
+ import type { OpsFailureInput } from './scope-host.js';
2
+ /**
3
+ * The admin actions written intent-then-outcome, which a settle closes: the two the control plane
4
+ * writes around a vertical call, (#2089) the schedule and peer kill switches, which each adapter
5
+ * writes around the scope's move, and (#2114) the replay lever, written the same way around the
6
+ * watermark's move. A lever row written before #2114 carries only `replayId`, no `operationId`,
7
+ * so every reader passes it over as it passes over any row without one.
8
+ */
9
+ export declare const AUDITED_CHANGE_ACTIONS: readonly ["transferOwner", "manageScopeMember", "setPeerBinding", "revokeFromSystem", "restoreToSystem", "revokeFromPeer", "restoreToPeer", "moveImportCursor"];
10
+ export type AuditedChangeAction = (typeof AUDITED_CHANGE_ACTIONS)[number];
11
+ /** The structured line an outcome row that could not be written leaves, keyed so a log search finds every one. */
12
+ export declare const UNRECORDED_OUTCOME_LOG = "audit-outcome-unrecorded";
13
+ /** Where an unwritten outcome is reported: the host's error log (the kernel has none of its own). */
14
+ export type AuditLogError = (message: string, fields: Record<string, unknown>) => void;
15
+ /**
16
+ * Write one outcome row of an audited change (#2064, #2089 for the kill switches), never
17
+ * swallowing its failure. Answers null when the row landed. When it did not, the failure is
18
+ * logged as `UNRECORDED_OUTCOME_LOG` with the flow, the operation id and the phase, and its
19
+ * message is the answer, for the caller to surface: a `refused`/`failed` caller still answers
20
+ * with its own error (so a refusal never reads as retryable), and an `applied` caller answers
21
+ * success with `auditWarningOf`. The intent left without an outcome is closed as `unknown` by
22
+ * the control plane's scheduled settle either way.
23
+ */
24
+ export declare function recordAuditOutcome(write: () => unknown, line: {
25
+ flow: string;
26
+ operationId: string;
27
+ phase: string;
28
+ }, logError: AuditLogError): Promise<string | null>;
29
+ /** The `auditWarning` a change that went through answers when its `applied` row could not be written. */
30
+ export declare const auditWarningOf: (what: string, unrecorded: string) => string;
31
+ /** The intent a settle reads by id, inside its transaction. */
32
+ export declare const SETTLE_INTENT_SQL = "SELECT id, action, tenant_id, scope_id, vertical, after FROM _substrat_admin_log WHERE id = ?";
33
+ /**
34
+ * Both statements below write `+action`, so the action column cannot drive an index choice: an
35
+ * operation's few rows are reached through `_substrat_admin_log_operation` (the kernel's
36
+ * admin-log index list), never by walking every row of an action. A planner left to choose takes
37
+ * `(action, id)`, which reads the whole history of the action.
38
+ */
39
+ /**
40
+ * Whether the operation already has an outcome: a non-intent row of the SAME operation, its whole
41
+ * identity compared column by column. The operation id reaches `_substrat_admin_log_operation`.
42
+ * The action, tenant and scope are compared with `IS`, so a null tenant or scope matches only
43
+ * null and never acts as a wildcard, and written `+column` so that the operation id is the only
44
+ * index the statement can use: left to choose, the planner took `(action, id)` once, and
45
+ * `(tenant_id, id)` or `(scope_id, id)` would read as much. No
46
+ * value is serialized to compare, so nothing depends on two encoders agreeing. Never by row
47
+ * order: the intent, a real outcome and a settle's `unknown` are stamped by different writers
48
+ * whose ids and clocks need not agree.
49
+ */
50
+ export declare const SETTLE_OUTCOME_SQL = "SELECT 1 AS present FROM _substrat_admin_log\n WHERE json_extract(after, '$.operationId') = ?\n AND json_extract(after, '$.operationId') IS NOT NULL\n AND +action IS ?\n AND +tenant_id IS ?\n AND +scope_id IS ?\n AND json_extract(after, '$.phase') <> 'intent'\n LIMIT 1";
51
+ /** `SETTLE_OUTCOME_SQL`'s parameters for one operation, every part of its identity in order. */
52
+ export declare const settleOutcomeParamsOf: (operation: AuditedOperationRef) => [string, string, string | null, string | null];
53
+ /** How many operation ids one batched read binds: well under a Durable Object's 100 parameters. */
54
+ export declare const AUDITED_OPERATIONS_BATCH = 50;
55
+ /**
56
+ * Every row of the given operations, by the operation-id index. One statement per batch of
57
+ * `AUDITED_OPERATIONS_BATCH` ids. The caller narrows the rows to the action, tenant and scope it
58
+ * asked about.
59
+ */
60
+ export declare function auditedOperationsSql(count: number): string;
61
+ /** One row as `auditedOperationsSql` reads it. */
62
+ export interface AuditedOperationSqlRow {
63
+ id: string;
64
+ action: string;
65
+ tenant_id: string | null;
66
+ scope_id: string | null;
67
+ operation_id: string;
68
+ phase: string | null;
69
+ }
70
+ /** One row of an audited operation, as `HostAdmin.auditedOperations` answers it. */
71
+ export interface AuditedOperationRow {
72
+ id: string;
73
+ action: string;
74
+ tenantId: string | null;
75
+ scopeId: string | null;
76
+ operationId: string;
77
+ phase: string | null;
78
+ }
79
+ /** The operations a reader asks about: an operation is its action, its id and its scope. */
80
+ export interface AuditedOperationRef {
81
+ action: string;
82
+ operationId: string;
83
+ tenantId: string | null;
84
+ scopeId: string | null;
85
+ }
86
+ /**
87
+ * Run `auditedOperationsSql` over `refs` in batches through `all`, and keep only the rows of
88
+ * the exact operations asked about. An operation id from another action or scope is not one.
89
+ */
90
+ export declare function readAuditedOperations(all: (sql: string, params: string[]) => AuditedOperationSqlRow[], refs: readonly AuditedOperationRef[]): AuditedOperationRow[];
91
+ /**
92
+ * An audited operation's identity: its action, its operation id AND the tenant and scope it was
93
+ * made in. The one key every grouping, lookup and resolution uses, built only here. An id alone
94
+ * is not an operation: the same id in two scopes is two operations, never a conflict.
95
+ */
96
+ export type OperationKey = string & {
97
+ readonly __operationKey: unique symbol;
98
+ };
99
+ export declare const operationKeyOf: (r: AuditedOperationRef) => OperationKey;
100
+ /** One row as `SETTLE_INTENT_SQL` reads it. */
101
+ export interface SettleIntentRow {
102
+ id: string;
103
+ action: string;
104
+ tenant_id: string | null;
105
+ scope_id: string | null;
106
+ vertical: string | null;
107
+ after: string | null;
108
+ }
109
+ /** What a settle writes: the admin row's target and payload, and the ops-failure row. */
110
+ export interface UnknownOutcome {
111
+ action: AuditedChangeAction;
112
+ operationId: string;
113
+ /** The operation the intent belongs to, the settle check's whole key. */
114
+ operation: AuditedOperationRef;
115
+ target: {
116
+ tenantId: string | null;
117
+ scopeId: string | null;
118
+ vertical: string | null;
119
+ };
120
+ after: Record<string, unknown>;
121
+ failure: Omit<OpsFailureInput, 'actor'>;
122
+ }
123
+ /**
124
+ * The `unknown` outcome for one intent row: the intent's own fields with `phase: 'unknown'` and
125
+ * `error` saying why, parsed by the same schema the flow writes its rows with. Throws `not_found`
126
+ * when the row is absent, or is not the intent of an audited change.
127
+ */
128
+ export declare function unknownOutcomeOf(row: SettleIntentRow | undefined, intentId: string, error: string): UnknownOutcome;
129
+ /** The phases an audited change's rows carry. */
130
+ export type AuditedPhase = 'intent' | 'applied' | 'refused' | 'failed' | 'unknown';
131
+ /** Where an operation stands, by priority over its rows (see the module header). */
132
+ export interface EffectiveOutcome {
133
+ /** `conflicting`: more than one real outcome row, which the audit must never hold. */
134
+ outcome: 'applied' | 'refused' | 'failed' | 'unknown' | 'conflicting';
135
+ /** The real outcome phases found, in no particular order: one, or several when conflicting. */
136
+ real: string[];
137
+ }
138
+ /**
139
+ * Each audited operation's effective outcome among `rows`, keyed by `operationKeyOf`. A real
140
+ * outcome beats `unknown` regardless of which was written first. Two or more real outcome rows
141
+ * are `conflicting`. An operation with only an intent has no entry, so it is pending. Row ids
142
+ * and timestamps decide nothing.
143
+ */
144
+ export declare function effectiveOutcomes(rows: readonly (AuditedOperationRef & {
145
+ phase: string | null;
146
+ })[]): Map<OperationKey, EffectiveOutcome>;
147
+ /** Whether a row is an outcome its operation's effective outcome replaced: an `unknown` beaten by a real one. */
148
+ export declare function isSupersededOutcome(phase: string | null, effective: EffectiveOutcome | undefined): boolean;
149
+ //# sourceMappingURL=audit-outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-outcome.d.ts","sourceRoot":"","sources":["../src/audit-outcome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Closing an audited change whose outcome was never written (#2064), shared by both adapters.
3
+ *
4
+ * The owner hand-over (#1665) and dashboard member management (#1150) audit a change made in the
5
+ * vertical's own deployment in two rows paired by `operationId`: an `intent`, then `applied`,
6
+ * `refused` or `failed`. The schedule and peer kill switches (#1666, #1706; #2089) audit the
7
+ * scope's move the same way, from inside each adapter, and so does the replay lever
8
+ * (`moveImportCursor`, #2114), whose rows carry its `replayId` as their `operationId`. The two
9
+ * stores are separate, so an intent can be left with no outcome: its outcome write failed (`recordAuditOutcome` logs it), or the
10
+ * request died between the two. The control plane's scheduled
11
+ * pass finds such an intent and asks the host to settle it through
12
+ * `HostAdmin.settleUnrecordedOutcome`.
13
+ *
14
+ * **The settle is one transaction, and it claims the outcome only if none exists.** It reads
15
+ * the intent by id, checks there is no outcome row for the operation, then writes the `unknown`
16
+ * row and the ops-failure row that brings it to the staff digest. Two passes that race settle
17
+ * once. An outcome that lands before the settle runs wins, and the settle writes nothing.
18
+ * Because the ops-failure row is written in the same unit, an `unknown` row cannot exist without
19
+ * the digest hearing of it.
20
+ *
21
+ * **A real outcome supersedes `unknown`, by priority, not by order.** The log is append-only, so
22
+ * a real outcome that lands after the settle is still recorded. Every reader resolves an
23
+ * operation by `effectiveOutcomes`: a real outcome (`applied`, `refused`, `failed`) beats
24
+ * `unknown` whichever row was written first, since the two writers' ids and clocks need not
25
+ * agree. Two real outcomes for one operation break the audit's invariant, so they resolve to
26
+ * `conflicting`, which is reported and never guessed past. The control plane bounds the vertical call well inside the
27
+ * settle's grace window, so a live request cannot normally be overtaken this way. The rule is
28
+ * stated for the case where one is.
29
+ */
30
+ import { AUDIT_ERROR_MAX, auditOperationId, memberChangeAudit, ownerTransferAudit, peerBindingAudit, substratError } from '@substrat-run/contracts';
31
+ import { SWITCH_ACTIONS } from './system-switch-record.js';
32
+ /**
33
+ * The admin actions written intent-then-outcome, which a settle closes: the two the control plane
34
+ * writes around a vertical call, (#2089) the schedule and peer kill switches, which each adapter
35
+ * writes around the scope's move, and (#2114) the replay lever, written the same way around the
36
+ * watermark's move. A lever row written before #2114 carries only `replayId`, no `operationId`,
37
+ * so every reader passes it over as it passes over any row without one.
38
+ */
39
+ export const AUDITED_CHANGE_ACTIONS = [
40
+ 'transferOwner',
41
+ 'manageScopeMember',
42
+ 'setPeerBinding',
43
+ ...SWITCH_ACTIONS,
44
+ 'moveImportCursor',
45
+ ];
46
+ /** The structured line an outcome row that could not be written leaves, keyed so a log search finds every one. */
47
+ export const UNRECORDED_OUTCOME_LOG = 'audit-outcome-unrecorded';
48
+ /**
49
+ * Write one outcome row of an audited change (#2064, #2089 for the kill switches), never
50
+ * swallowing its failure. Answers null when the row landed. When it did not, the failure is
51
+ * logged as `UNRECORDED_OUTCOME_LOG` with the flow, the operation id and the phase, and its
52
+ * message is the answer, for the caller to surface: a `refused`/`failed` caller still answers
53
+ * with its own error (so a refusal never reads as retryable), and an `applied` caller answers
54
+ * success with `auditWarningOf`. The intent left without an outcome is closed as `unknown` by
55
+ * the control plane's scheduled settle either way.
56
+ */
57
+ export async function recordAuditOutcome(write, line, logError) {
58
+ try {
59
+ await write();
60
+ return null;
61
+ }
62
+ catch (auditError) {
63
+ const message = auditError instanceof Error ? auditError.message : String(auditError);
64
+ try {
65
+ logError(UNRECORDED_OUTCOME_LOG, { ...line, auditError: message });
66
+ }
67
+ catch {
68
+ // Reporting an unwritten row must not replace the operation’s own error or warning.
69
+ }
70
+ return message;
71
+ }
72
+ }
73
+ /** The `auditWarning` a change that went through answers when its `applied` row could not be written. */
74
+ export const auditWarningOf = (what, unrecorded) => `${what} completed, but its outcome could not be written to the admin log: ${unrecorded}`;
75
+ /** The intent a settle reads by id, inside its transaction. */
76
+ export const SETTLE_INTENT_SQL = 'SELECT id, action, tenant_id, scope_id, vertical, after FROM _substrat_admin_log WHERE id = ?';
77
+ /**
78
+ * Both statements below write `+action`, so the action column cannot drive an index choice: an
79
+ * operation's few rows are reached through `_substrat_admin_log_operation` (the kernel's
80
+ * admin-log index list), never by walking every row of an action. A planner left to choose takes
81
+ * `(action, id)`, which reads the whole history of the action.
82
+ */
83
+ /**
84
+ * Whether the operation already has an outcome: a non-intent row of the SAME operation, its whole
85
+ * identity compared column by column. The operation id reaches `_substrat_admin_log_operation`.
86
+ * The action, tenant and scope are compared with `IS`, so a null tenant or scope matches only
87
+ * null and never acts as a wildcard, and written `+column` so that the operation id is the only
88
+ * index the statement can use: left to choose, the planner took `(action, id)` once, and
89
+ * `(tenant_id, id)` or `(scope_id, id)` would read as much. No
90
+ * value is serialized to compare, so nothing depends on two encoders agreeing. Never by row
91
+ * order: the intent, a real outcome and a settle's `unknown` are stamped by different writers
92
+ * whose ids and clocks need not agree.
93
+ */
94
+ export const SETTLE_OUTCOME_SQL = `SELECT 1 AS present FROM _substrat_admin_log
95
+ WHERE json_extract(after, '$.operationId') = ?
96
+ AND json_extract(after, '$.operationId') IS NOT NULL
97
+ AND +action IS ?
98
+ AND +tenant_id IS ?
99
+ AND +scope_id IS ?
100
+ AND json_extract(after, '$.phase') <> 'intent'
101
+ LIMIT 1`;
102
+ /** `SETTLE_OUTCOME_SQL`'s parameters for one operation, every part of its identity in order. */
103
+ export const settleOutcomeParamsOf = (operation) => [
104
+ operation.operationId,
105
+ operation.action,
106
+ operation.tenantId,
107
+ operation.scopeId,
108
+ ];
109
+ /** How many operation ids one batched read binds: well under a Durable Object's 100 parameters. */
110
+ export const AUDITED_OPERATIONS_BATCH = 50;
111
+ /**
112
+ * Every row of the given operations, by the operation-id index. One statement per batch of
113
+ * `AUDITED_OPERATIONS_BATCH` ids. The caller narrows the rows to the action, tenant and scope it
114
+ * asked about.
115
+ */
116
+ export function auditedOperationsSql(count) {
117
+ return `SELECT id, action, tenant_id, scope_id,
118
+ json_extract(after, '$.operationId') AS operation_id, json_extract(after, '$.phase') AS phase
119
+ FROM _substrat_admin_log
120
+ WHERE json_extract(after, '$.operationId') IN (${Array.from({ length: count }, () => '?').join(', ')})
121
+ AND json_extract(after, '$.operationId') IS NOT NULL
122
+ AND +action IN (${AUDITED_CHANGE_ACTIONS.map((a) => `'${a}'`).join(', ')})`;
123
+ }
124
+ /**
125
+ * Run `auditedOperationsSql` over `refs` in batches through `all`, and keep only the rows of
126
+ * the exact operations asked about. An operation id from another action or scope is not one.
127
+ */
128
+ export function readAuditedOperations(all, refs) {
129
+ const wanted = new Set(refs.map(operationKeyOf));
130
+ const ids = [...new Set(refs.map((r) => r.operationId))];
131
+ const rows = [];
132
+ for (let i = 0; i < ids.length; i += AUDITED_OPERATIONS_BATCH) {
133
+ const batch = ids.slice(i, i + AUDITED_OPERATIONS_BATCH);
134
+ for (const r of all(auditedOperationsSql(batch.length), batch)) {
135
+ const row = { id: r.id, action: r.action, tenantId: r.tenant_id, scopeId: r.scope_id, operationId: r.operation_id, phase: r.phase };
136
+ if (wanted.has(operationKeyOf(row)))
137
+ rows.push(row);
138
+ }
139
+ }
140
+ return rows;
141
+ }
142
+ export const operationKeyOf = (r) => JSON.stringify([r.action, r.operationId, r.tenantId, r.scopeId]);
143
+ const isAudited = (action) => AUDITED_CHANGE_ACTIONS.includes(action);
144
+ const isSwitch = (action) => SWITCH_ACTIONS.includes(action);
145
+ /** The actions whose rows have no schema of their own, so an `unknown` row is the intent's fields. */
146
+ const copiesIntent = (action) => isSwitch(action) || action === 'moveImportCursor';
147
+ /**
148
+ * The `unknown` row of a kill switch's operation (#2089), or of the replay lever's (#2114): the
149
+ * intent's own fields (its subject, position and reason) with `phase: 'unknown'` and the error.
150
+ * Neither has a row schema of its own: each adapter writes the switch's rows from
151
+ * `switchAuditSubject` and the lever's from its move, and the history readers count only
152
+ * `applied` rows, so an `unknown` row moves no position.
153
+ */
154
+ const intentUnknownOf = (after, error) => ({
155
+ ...after,
156
+ phase: 'unknown',
157
+ error: error.slice(0, AUDIT_ERROR_MAX),
158
+ });
159
+ /**
160
+ * The `unknown` outcome for one intent row: the intent's own fields with `phase: 'unknown'` and
161
+ * `error` saying why, parsed by the same schema the flow writes its rows with. Throws `not_found`
162
+ * when the row is absent, or is not the intent of an audited change.
163
+ */
164
+ export function unknownOutcomeOf(row, intentId, error) {
165
+ const after = row?.after ? JSON.parse(row.after) : null;
166
+ if (!row || !isAudited(row.action) || after?.phase !== 'intent' || !auditOperationId.safeParse(after.operationId).success) {
167
+ throw substratError('not_found', `no audited-change intent ${intentId} to settle`);
168
+ }
169
+ const parsed = copiesIntent(row.action) ? intentUnknownOf(after, error) : auditedUnknownOf(row, after, error);
170
+ const operationId = after.operationId;
171
+ return {
172
+ action: row.action,
173
+ operationId,
174
+ operation: { action: row.action, operationId, tenantId: row.tenant_id, scopeId: row.scope_id },
175
+ target: { tenantId: row.tenant_id, scopeId: row.scope_id, vertical: row.vertical },
176
+ after: parsed,
177
+ failure: {
178
+ operation: `audit.${row.action}`,
179
+ stage: 'outcome-unknown',
180
+ tenantId: row.tenant_id,
181
+ scopeId: row.scope_id,
182
+ vertical: row.vertical,
183
+ // The operation id is the row's handle: the digest reads the admin log by it, to drop an
184
+ // `unknown` that a real outcome has since superseded.
185
+ reference: operationId,
186
+ message: `operation ${operationId}: ${error}`,
187
+ },
188
+ };
189
+ }
190
+ /** A control-plane flow's `unknown` row, parsed by the same schema the flow writes its rows with. */
191
+ function auditedUnknownOf(row, after, error) {
192
+ const entry = { ...after, tenantId: row.tenant_id, scopeId: row.scope_id, phase: 'unknown', error };
193
+ const { tenantId: _t, scopeId: _s, ...parsed } = row.action === 'transferOwner' ? ownerTransferAudit.parse(entry)
194
+ : row.action === 'manageScopeMember' ? memberChangeAudit.parse(entry)
195
+ : peerBindingAudit.parse(entry);
196
+ return parsed;
197
+ }
198
+ const REAL_OUTCOMES = new Set(['applied', 'refused', 'failed']);
199
+ /**
200
+ * Each audited operation's effective outcome among `rows`, keyed by `operationKeyOf`. A real
201
+ * outcome beats `unknown` regardless of which was written first. Two or more real outcome rows
202
+ * are `conflicting`. An operation with only an intent has no entry, so it is pending. Row ids
203
+ * and timestamps decide nothing.
204
+ */
205
+ export function effectiveOutcomes(rows) {
206
+ const seen = new Map();
207
+ for (const row of rows) {
208
+ if (!isAudited(row.action) || row.phase === null || row.phase === 'intent')
209
+ continue;
210
+ const key = operationKeyOf(row);
211
+ const entry = seen.get(key) ?? { real: [], unknown: false };
212
+ if (REAL_OUTCOMES.has(row.phase))
213
+ entry.real.push(row.phase);
214
+ else if (row.phase === 'unknown')
215
+ entry.unknown = true;
216
+ seen.set(key, entry);
217
+ }
218
+ const out = new Map();
219
+ for (const [key, { real, unknown }] of seen) {
220
+ if (real.length > 1)
221
+ out.set(key, { outcome: 'conflicting', real });
222
+ else if (real.length === 1)
223
+ out.set(key, { outcome: real[0], real });
224
+ else if (unknown)
225
+ out.set(key, { outcome: 'unknown', real });
226
+ }
227
+ return out;
228
+ }
229
+ /** Whether a row is an outcome its operation's effective outcome replaced: an `unknown` beaten by a real one. */
230
+ export function isSupersededOutcome(phase, effective) {
231
+ return phase === 'unknown' && effective !== undefined && effective.real.length > 0;
232
+ }
233
+ //# sourceMappingURL=audit-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-outcome.js","sourceRoot":"","sources":["../src/audit-outcome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { type ConnectLink, type ConnectLinkConsume, type ScopeId, type TenantId } from '@substrat-run/contracts';
2
+ import type { RedactionSql } from './subject-redaction.js';
3
+ /**
4
+ * The platform-held connect link (connections.md §3.5.4), as statements both adapters run
5
+ * against their directory — the SQLite host over better-sqlite3, the hosted one inside
6
+ * ControlPlaneDO — so the lifecycle is written once, the way the findings store is.
7
+ *
8
+ * A vertical mints one to be MAILED: a bookkeeping bureau's staff send it to a client
9
+ * company's Fortnox administrator, who opens it days later. A signature alone cannot be
10
+ * withdrawn and cannot be spent, which is what §3.5.3's in-session URL gets away with by
11
+ * living minutes; a link living a week cannot. So the signed state names this row, and the
12
+ * row decides: outstanding and unexpired opens, anything else refuses.
13
+ *
14
+ * Every read and write is keyed by (tenant, scope, id). A link of another scope or tenant
15
+ * is not a forbidden row — it is an absent one, and answers exactly as `unknown` does.
16
+ *
17
+ * Expiry is judged against the `now` the caller passes: the pure host's clock, or the
18
+ * coordinator's wall clock on the hosted one. ISO 8601 UTC text on both sides, so a string
19
+ * comparison is a chronological one.
20
+ */
21
+ export declare const CONNECT_LINKS_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_connect_links (\n id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n scope_id TEXT NOT NULL,\n vertical TEXT NOT NULL,\n provider TEXT NOT NULL,\n status TEXT NOT NULL,\n created_by TEXT NOT NULL,\n subject_ref TEXT,\n return_url TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n used_at TEXT,\n account_ref TEXT,\n account_label TEXT\n );\n CREATE INDEX IF NOT EXISTS _substrat_connect_links_scope\n ON _substrat_connect_links (tenant_id, scope_id, created_at);\n";
22
+ export interface ConnectLinkKeyRow {
23
+ tenantId: string;
24
+ scopeId: string;
25
+ id: string;
26
+ }
27
+ /**
28
+ * The admin-log row a link move hands back, for the caller to write in the SAME unit as the
29
+ * move: a consume whose audit row failed would otherwise leave the link spent with no
30
+ * connection and no record, and the retry refused as `used`. Shaped here, once, so both
31
+ * adapters write the same row. A move that changed nothing hands back nothing.
32
+ */
33
+ export interface ConnectLinkAudit {
34
+ action: 'mintConnectLink' | 'revokeConnectLink' | 'consumeConnectLink' | 'restoreConnectLink';
35
+ target: {
36
+ tenantId: TenantId;
37
+ scopeId: ScopeId;
38
+ vertical: string;
39
+ };
40
+ before: unknown;
41
+ after: unknown;
42
+ }
43
+ export declare function insertConnectLink(sql: RedactionSql, row: {
44
+ id: string;
45
+ tenantId: string;
46
+ scopeId: string;
47
+ vertical: string;
48
+ provider: string;
49
+ createdBy: string;
50
+ subjectRef: string | null;
51
+ returnUrl: string | null;
52
+ createdAt: string;
53
+ expiresAt: string;
54
+ }, audit: (row: ConnectLinkAudit) => void): ConnectLink;
55
+ export declare function readConnectLink(sql: RedactionSql, key: ConnectLinkKeyRow): ConnectLink | undefined;
56
+ export declare function listConnectLinks(sql: RedactionSql, filter: {
57
+ tenantId: string;
58
+ scopeId?: string;
59
+ ids?: readonly string[];
60
+ provider?: string;
61
+ outstandingOnly?: boolean;
62
+ }, now: string): ConnectLink[];
63
+ /**
64
+ * Withdraw an outstanding link. Idempotent, and only an outstanding row moves: a used link
65
+ * has already been spent (its connection is what to disconnect), and a revoked one stays
66
+ * revoked. `undefined` when no such link exists in this scope; `changed` says whether this
67
+ * call is the one that revoked it, so a no-op is not audited.
68
+ */
69
+ export declare function revokeConnectLinkRow(sql: RedactionSql, key: ConnectLinkKeyRow, audit: (row: ConnectLinkAudit) => void): {
70
+ link: ConnectLink;
71
+ changed: boolean;
72
+ } | undefined;
73
+ /**
74
+ * Spend a link — the callback's act, BEFORE it stores the credential, so of two racing
75
+ * callbacks exactly one gets past it. Single-use is decided by the UPDATE's WHERE, not by
76
+ * the read in front of it: the read only picks which refusal to answer with.
77
+ */
78
+ export declare function consumeConnectLinkRow(sql: RedactionSql, input: ConnectLinkKeyRow & {
79
+ provider: string;
80
+ accountRef?: string;
81
+ accountLabel?: string;
82
+ }, now: string, audit: (row: ConnectLinkAudit) => void): ConnectLinkConsume;
83
+ /**
84
+ * The compensating half of consume: the callback spent the link, then failed to store the
85
+ * credential, so nothing was connected and the link goes back to outstanding — a platform
86
+ * hiccup costs a retry, not a fresh link mailed to a client. Only a `used` row moves (a
87
+ * revocation is never undone by this), and only an unexpired one: a link that lapsed while
88
+ * the store failed stays spent rather than coming back already dead.
89
+ */
90
+ export declare function restoreConnectLinkRow(sql: RedactionSql, key: ConnectLinkKeyRow, now: string, audit: (row: ConnectLinkAudit) => void): ConnectLink | undefined;
91
+ //# sourceMappingURL=connect-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-links.d.ts","sourceRoot":"","sources":["../src/connect-links.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,kBAAkB,EAEvB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,qnBAmB7B,CAAC;AA0CF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;IAC9F,MAAM,EAAE;QAAE,QAAQ,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAWD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE;IACH,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,EACD,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GACrC,WAAW,CA4Bb;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAGlG;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,YAAY,EACjB,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,EACrH,GAAG,EAAE,MAAM,GACV,WAAW,EAAE,CAgBf;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GACrC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAmBrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,iBAAiB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3F,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GACrC,kBAAkB,CAuBpB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,iBAAiB,EACtB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GACrC,WAAW,GAAG,SAAS,CAiBzB"}