@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.
- package/dist/admin-log-ddl.d.ts +21 -0
- package/dist/admin-log-ddl.d.ts.map +1 -0
- package/dist/admin-log-ddl.js +32 -0
- package/dist/admin-log-ddl.js.map +1 -0
- package/dist/attachment-extractor.d.ts +21 -2
- package/dist/attachment-extractor.d.ts.map +1 -1
- package/dist/attachment-extractor.js +31 -6
- package/dist/attachment-extractor.js.map +1 -1
- package/dist/attachment-text.d.ts +64 -0
- package/dist/attachment-text.d.ts.map +1 -1
- package/dist/attachment-text.js +87 -16
- package/dist/attachment-text.js.map +1 -1
- package/dist/attribution.d.ts +36 -3
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +21 -8
- package/dist/attribution.js.map +1 -1
- package/dist/audit-outcome.d.ts +149 -0
- package/dist/audit-outcome.d.ts.map +1 -0
- package/dist/audit-outcome.js +233 -0
- package/dist/audit-outcome.js.map +1 -0
- package/dist/connect-links.d.ts +91 -0
- package/dist/connect-links.d.ts.map +1 -0
- package/dist/connect-links.js +189 -0
- package/dist/connect-links.js.map +1 -0
- package/dist/connect-state.d.ts +12 -3
- package/dist/connect-state.d.ts.map +1 -1
- package/dist/connect-state.js +8 -3
- package/dist/connect-state.js.map +1 -1
- package/dist/derived-object.d.ts +14 -0
- package/dist/derived-object.d.ts.map +1 -0
- package/dist/derived-object.js +2 -0
- package/dist/derived-object.js.map +1 -0
- package/dist/derived-objects.d.ts +108 -0
- package/dist/derived-objects.d.ts.map +1 -0
- package/dist/derived-objects.js +199 -0
- package/dist/derived-objects.js.map +1 -0
- package/dist/email-intent.d.ts +56 -0
- package/dist/email-intent.d.ts.map +1 -0
- package/dist/email-intent.js +71 -0
- package/dist/email-intent.js.map +1 -0
- package/dist/entity-edges.d.ts +6 -1
- package/dist/entity-edges.d.ts.map +1 -1
- package/dist/entity-edges.js +13 -0
- package/dist/entity-edges.js.map +1 -1
- package/dist/entity-grant-shape.d.ts +94 -0
- package/dist/entity-grant-shape.d.ts.map +1 -0
- package/dist/entity-grant-shape.js +329 -0
- package/dist/entity-grant-shape.js.map +1 -0
- package/dist/entity-grant.d.ts +45 -0
- package/dist/entity-grant.d.ts.map +1 -0
- package/dist/entity-grant.js +71 -0
- package/dist/entity-grant.js.map +1 -0
- package/dist/entity-state-reads.d.ts +40 -0
- package/dist/entity-state-reads.d.ts.map +1 -0
- package/dist/entity-state-reads.js +91 -0
- package/dist/entity-state-reads.js.map +1 -0
- package/dist/entity-state.d.ts +204 -0
- package/dist/entity-state.d.ts.map +1 -0
- package/dist/entity-state.js +356 -0
- package/dist/entity-state.js.map +1 -0
- package/dist/entity-trash.d.ts +245 -0
- package/dist/entity-trash.d.ts.map +1 -0
- package/dist/entity-trash.js +380 -0
- package/dist/entity-trash.js.map +1 -0
- package/dist/findings.d.ts +143 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +463 -0
- package/dist/findings.js.map +1 -0
- package/dist/grant-scoped-read.d.ts +41 -0
- package/dist/grant-scoped-read.d.ts.map +1 -0
- package/dist/grant-scoped-read.js +100 -0
- package/dist/grant-scoped-read.js.map +1 -0
- package/dist/index.d.ts +40 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/kernel-outbox.d.ts +14 -0
- package/dist/kernel-outbox.d.ts.map +1 -0
- package/dist/kernel-outbox.js +37 -0
- package/dist/kernel-outbox.js.map +1 -0
- package/dist/list-index.d.ts +34 -3
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +83 -31
- package/dist/list-index.js.map +1 -1
- package/dist/mail-sender.d.ts +78 -0
- package/dist/mail-sender.d.ts.map +1 -0
- package/dist/mail-sender.js +2 -0
- package/dist/mail-sender.js.map +1 -0
- package/dist/membership-executor.d.ts +7 -47
- package/dist/membership-executor.d.ts.map +1 -1
- package/dist/membership-executor.js +28 -53
- package/dist/membership-executor.js.map +1 -1
- package/dist/migration-digest.d.ts +161 -0
- package/dist/migration-digest.d.ts.map +1 -0
- package/dist/migration-digest.js +237 -0
- package/dist/migration-digest.js.map +1 -0
- package/dist/module-erasure.d.ts +156 -0
- package/dist/module-erasure.d.ts.map +1 -0
- package/dist/module-erasure.js +440 -0
- package/dist/module-erasure.js.map +1 -0
- package/dist/module-migrations.d.ts +6 -3
- package/dist/module-migrations.d.ts.map +1 -1
- package/dist/module-migrations.js +12 -5
- package/dist/module-migrations.js.map +1 -1
- package/dist/peer.d.ts +9 -1
- package/dist/peer.d.ts.map +1 -1
- package/dist/peer.js +29 -0
- package/dist/peer.js.map +1 -1
- package/dist/permission-checker.d.ts +6 -0
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts +51 -1
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +104 -2
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +8 -2
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +17 -0
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +479 -24
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +82 -7
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-role-admin.d.ts +68 -0
- package/dist/scope-role-admin.d.ts.map +1 -0
- package/dist/scope-role-admin.js +80 -0
- package/dist/scope-role-admin.js.map +1 -0
- package/dist/search-index.d.ts +21 -1
- package/dist/search-index.d.ts.map +1 -1
- package/dist/search-index.js +60 -37
- package/dist/search-index.js.map +1 -1
- package/dist/spine-guard.d.ts +71 -2
- package/dist/spine-guard.d.ts.map +1 -1
- package/dist/spine-guard.js +393 -10
- package/dist/spine-guard.js.map +1 -1
- package/dist/spine-restore.d.ts.map +1 -1
- package/dist/spine-restore.js +5 -0
- package/dist/spine-restore.js.map +1 -1
- package/dist/sql-identifier.d.ts +12 -0
- package/dist/sql-identifier.d.ts.map +1 -0
- package/dist/sql-identifier.js +17 -0
- package/dist/sql-identifier.js.map +1 -0
- package/dist/sql-statements.d.ts +71 -0
- package/dist/sql-statements.d.ts.map +1 -0
- package/dist/sql-statements.js +191 -0
- package/dist/sql-statements.js.map +1 -0
- package/dist/subject-redaction.d.ts +9 -1
- package/dist/subject-redaction.d.ts.map +1 -1
- package/dist/subject-redaction.js.map +1 -1
- package/dist/system-switch-record.d.ts +3 -0
- package/dist/system-switch-record.d.ts.map +1 -1
- package/dist/system-switch-record.js +2 -0
- package/dist/system-switch-record.js.map +1 -1
- package/dist/table-ownership.d.ts +103 -0
- package/dist/table-ownership.d.ts.map +1 -0
- package/dist/table-ownership.js +285 -0
- package/dist/table-ownership.js.map +1 -0
- package/dist/timeline.d.ts +4 -4
- package/dist/timeline.js +4 -4
- package/dist/visible-continuation.d.ts +38 -0
- package/dist/visible-continuation.d.ts.map +1 -0
- package/dist/visible-continuation.js +162 -0
- package/dist/visible-continuation.js.map +1 -0
- package/package.json +2 -2
package/dist/attribution.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribution.js","sourceRoot":"","sources":["../src/attribution.ts"],"names":[],"mappings":"
|
|
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"}
|