@opengeni/db 3.4.0-canary.2 → 3.6.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 (67) hide show
  1. package/dist/canonical-human-identities.d.ts +20 -0
  2. package/dist/canonical-human-identities.js +7 -3
  3. package/dist/{chunk-ZWRIPCTR.js → chunk-2NT476HD.js} +2 -2
  4. package/dist/{chunk-PYMY3AJW.js → chunk-744DFC65.js} +202 -7
  5. package/dist/chunk-744DFC65.js.map +1 -0
  6. package/dist/{chunk-O6FTELKH.js → chunk-CDMMSUWJ.js} +3 -3
  7. package/dist/chunk-G3732M3T.js +299 -0
  8. package/dist/chunk-G3732M3T.js.map +1 -0
  9. package/dist/{chunk-TRBOWUG3.js → chunk-IW6O6G7R.js} +2 -2
  10. package/dist/{chunk-HY3F5Q4E.js → chunk-Q5YUGQVK.js} +3 -3
  11. package/dist/{chunk-V3TRUXFJ.js → chunk-UGDVSQ6U.js} +72 -19
  12. package/dist/chunk-UGDVSQ6U.js.map +1 -0
  13. package/dist/{chunk-C2AFP7SB.js → chunk-UJ2EW3QI.js} +6664 -6051
  14. package/dist/chunk-UJ2EW3QI.js.map +1 -0
  15. package/dist/{chunk-H7MXYG7R.js → chunk-WW7AJ5IC.js} +2 -2
  16. package/dist/editable-artifact-durable-export.js +2 -2
  17. package/dist/editable-artifacts.js +3 -3
  18. package/dist/index.d.ts +44 -6
  19. package/dist/index.js +2914 -1535
  20. package/dist/index.js.map +1 -1
  21. package/dist/managed-auth-session-set-schema.d.ts +1399 -0
  22. package/dist/managed-auth-session-sets.d.ts +145 -0
  23. package/dist/managed-auth-session-sets.js +50 -0
  24. package/dist/managed-auth-session-sets.js.map +1 -0
  25. package/dist/organization-recovery-schema.d.ts +2144 -0
  26. package/dist/organization-recovery.d.ts +186 -0
  27. package/dist/provision-roles.js +1 -1
  28. package/dist/runtime-posture.d.ts +7 -4
  29. package/dist/schema.d.ts +9 -6
  30. package/dist/schema.js +43 -1
  31. package/dist/session-background-commands.js +3 -3
  32. package/dist/session-control.d.ts +5 -3
  33. package/dist/session-queue-commands.d.ts +2 -0
  34. package/dist/session-tenancy.js +3 -3
  35. package/dist/video-generation.js +3 -3
  36. package/dist/work-claims-schema.d.ts +975 -0
  37. package/dist/work-claims.d.ts +83 -0
  38. package/drizzle/0362_managed_auth_session_sets.sql +2347 -0
  39. package/drizzle/0363_organization_recovery_custody.sql +2418 -0
  40. package/drizzle/0364_workspace_learning_policy_snapshot_lock_order.sql +221 -0
  41. package/drizzle/0365_permission_scoped_work_claims.sql +1306 -0
  42. package/drizzle/0366_session_discovery_title_search_index.sql +5 -0
  43. package/drizzle/0367_session_discovery_goal_search_index.sql +5 -0
  44. package/drizzle/0368_session_discovery_claim_search_index.sql +6 -0
  45. package/package.json +10 -6
  46. package/src/canonical-human-identities.ts +105 -27
  47. package/src/index.ts +1471 -433
  48. package/src/managed-auth-session-set-schema.ts +176 -0
  49. package/src/managed-auth-session-sets.ts +430 -0
  50. package/src/organization-recovery-schema.ts +277 -0
  51. package/src/organization-recovery.ts +424 -0
  52. package/src/provision-roles.ts +67 -0
  53. package/src/runtime-posture.ts +144 -6
  54. package/src/schema.ts +3 -0
  55. package/src/session-control.ts +255 -16
  56. package/src/session-queue-commands.ts +10 -1
  57. package/src/session-tool-call-settlement.ts +6 -1
  58. package/src/work-claims-schema.ts +148 -0
  59. package/src/work-claims.ts +200 -0
  60. package/dist/chunk-C2AFP7SB.js.map +0 -1
  61. package/dist/chunk-PYMY3AJW.js.map +0 -1
  62. package/dist/chunk-V3TRUXFJ.js.map +0 -1
  63. /package/dist/{chunk-ZWRIPCTR.js.map → chunk-2NT476HD.js.map} +0 -0
  64. /package/dist/{chunk-O6FTELKH.js.map → chunk-CDMMSUWJ.js.map} +0 -0
  65. /package/dist/{chunk-TRBOWUG3.js.map → chunk-IW6O6G7R.js.map} +0 -0
  66. /package/dist/{chunk-HY3F5Q4E.js.map → chunk-Q5YUGQVK.js.map} +0 -0
  67. /package/dist/{chunk-H7MXYG7R.js.map → chunk-WW7AJ5IC.js.map} +0 -0
@@ -0,0 +1,277 @@
1
+ import {
2
+ bigint,
3
+ boolean,
4
+ index,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ primaryKey,
9
+ text,
10
+ timestamp,
11
+ unique,
12
+ uniqueIndex,
13
+ uuid,
14
+ } from "drizzle-orm/pg-core";
15
+ import { sql } from "drizzle-orm";
16
+
17
+ export const organizationRecoveryPolicies = pgTable(
18
+ "organization_recovery_policies",
19
+ {
20
+ id: uuid("id").primaryKey().defaultRandom(),
21
+ accountId: uuid("account_id").notNull(),
22
+ revision: bigint("revision", { mode: "number" }).notNull(),
23
+ configuredByMembershipId: uuid("configured_by_membership_id").notNull(),
24
+ configuredByIdentityId: uuid("configured_by_identity_id").notNull(),
25
+ configuredAt: timestamp("configured_at", { withTimezone: true }).notNull().defaultNow(),
26
+ },
27
+ (table) => ({
28
+ idAccount: unique("organization_recovery_policies_id_account_unique").on(
29
+ table.id,
30
+ table.accountId,
31
+ ),
32
+ accountRevision: unique("organization_recovery_policies_account_revision_unique").on(
33
+ table.accountId,
34
+ table.revision,
35
+ ),
36
+ }),
37
+ );
38
+
39
+ export const organizationRecoveryPolicyHeads = pgTable("organization_recovery_policy_heads", {
40
+ accountId: uuid("account_id").primaryKey(),
41
+ currentPolicyId: uuid("current_policy_id").notNull().unique(),
42
+ revision: bigint("revision", { mode: "number" }).notNull(),
43
+ enabled: boolean("enabled").notNull().default(true),
44
+ activatedAt: timestamp("activated_at", { withTimezone: true }),
45
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
46
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
47
+ });
48
+
49
+ export const organizationRecoveryCustodians = pgTable(
50
+ "organization_recovery_custodians",
51
+ {
52
+ id: uuid("id").primaryKey().defaultRandom(),
53
+ accountId: uuid("account_id").notNull(),
54
+ policyId: uuid("policy_id").notNull(),
55
+ ordinal: integer("ordinal").notNull(),
56
+ membershipId: uuid("membership_id").notNull(),
57
+ canonicalIdentityId: uuid("canonical_identity_id").notNull(),
58
+ enrolledAt: timestamp("enrolled_at", { withTimezone: true }).notNull().defaultNow(),
59
+ },
60
+ (table) => ({
61
+ policyOrdinal: unique("organization_recovery_custodians_policy_ordinal_unique").on(
62
+ table.policyId,
63
+ table.ordinal,
64
+ ),
65
+ policyMembership: unique("organization_recovery_custodians_policy_membership_unique").on(
66
+ table.policyId,
67
+ table.membershipId,
68
+ ),
69
+ policyIdentity: unique("organization_recovery_custodians_policy_identity_unique").on(
70
+ table.policyId,
71
+ table.canonicalIdentityId,
72
+ ),
73
+ accountPolicy: index("organization_recovery_custodians_account_policy_idx").on(
74
+ table.accountId,
75
+ table.policyId,
76
+ table.ordinal,
77
+ ),
78
+ }),
79
+ );
80
+
81
+ export const organizationRecoveryCustodianAcceptances = pgTable(
82
+ "organization_recovery_custodian_acceptances",
83
+ {
84
+ id: uuid("id").primaryKey().defaultRandom(),
85
+ accountId: uuid("account_id").notNull(),
86
+ policyId: uuid("policy_id").notNull(),
87
+ custodianId: uuid("custodian_id").notNull(),
88
+ membershipId: uuid("membership_id").notNull(),
89
+ canonicalIdentityId: uuid("canonical_identity_id").notNull(),
90
+ authUserId: text("auth_user_id").notNull(),
91
+ loginBindingId: uuid("login_binding_id").notNull(),
92
+ membershipAuthorizationRevision: bigint("membership_authorization_revision", {
93
+ mode: "number",
94
+ }).notNull(),
95
+ identityRevision: bigint("identity_revision", { mode: "number" }).notNull(),
96
+ authRevision: bigint("auth_revision", { mode: "number" }).notNull(),
97
+ subjectRevision: bigint("subject_revision", { mode: "number" }).notNull(),
98
+ loginBindingRevision: bigint("login_binding_revision", { mode: "number" }).notNull(),
99
+ reauthOperationId: uuid("reauth_operation_id").notNull(),
100
+ acceptedAt: timestamp("accepted_at", { withTimezone: true }).notNull().defaultNow(),
101
+ },
102
+ (table) => ({
103
+ current: index("organization_recovery_acceptances_current_idx").on(
104
+ table.policyId,
105
+ table.canonicalIdentityId,
106
+ table.acceptedAt,
107
+ table.id,
108
+ ),
109
+ }),
110
+ );
111
+
112
+ export const organizationRecoveryOperations = pgTable(
113
+ "organization_recovery_operations",
114
+ {
115
+ id: uuid("id").primaryKey(),
116
+ accountId: uuid("account_id").notNull(),
117
+ policyId: uuid("policy_id").notNull(),
118
+ policyRevision: bigint("policy_revision", { mode: "number" }).notNull(),
119
+ revision: bigint("revision", { mode: "number" }).notNull().default(1),
120
+ state: text("state").notNull().default("collecting"),
121
+ targetMembershipId: uuid("target_membership_id").notNull(),
122
+ targetIdentityId: uuid("target_identity_id").notNull(),
123
+ targetAuthUserId: text("target_auth_user_id").notNull(),
124
+ targetMembershipAuthorizationRevision: bigint("target_membership_authorization_revision", {
125
+ mode: "number",
126
+ }).notNull(),
127
+ targetIdentityRevision: bigint("target_identity_revision", { mode: "number" }).notNull(),
128
+ targetAuthRevision: bigint("target_auth_revision", { mode: "number" }).notNull(),
129
+ targetSubjectRevision: bigint("target_subject_revision", { mode: "number" }).notNull(),
130
+ startedByCustodianId: uuid("started_by_custodian_id").notNull(),
131
+ startedByIdentityId: uuid("started_by_identity_id").notNull(),
132
+ quorumAt: timestamp("quorum_at", { withTimezone: true }),
133
+ executableAt: timestamp("executable_at", { withTimezone: true }),
134
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
135
+ notificationBatchId: uuid("notification_batch_id"),
136
+ notificationCount: integer("notification_count"),
137
+ notificationDigest: text("notification_digest"),
138
+ executedAt: timestamp("executed_at", { withTimezone: true }),
139
+ cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
140
+ supersededAt: timestamp("superseded_at", { withTimezone: true }),
141
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
142
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
143
+ },
144
+ (table) => ({
145
+ oneLive: uniqueIndex("organization_recovery_operations_one_live_idx")
146
+ .on(table.accountId)
147
+ .where(sql`${table.state} in ('collecting', 'cooling')`),
148
+ accountCreated: index("organization_recovery_operations_account_created_idx").on(
149
+ table.accountId,
150
+ table.createdAt,
151
+ table.id,
152
+ ),
153
+ }),
154
+ );
155
+
156
+ export const organizationRecoveryApprovals = pgTable(
157
+ "organization_recovery_approvals",
158
+ {
159
+ id: uuid("id").primaryKey().defaultRandom(),
160
+ accountId: uuid("account_id").notNull(),
161
+ operationId: uuid("operation_id").notNull(),
162
+ operationRevision: bigint("operation_revision", { mode: "number" }).notNull(),
163
+ policyId: uuid("policy_id").notNull(),
164
+ custodianId: uuid("custodian_id").notNull(),
165
+ membershipId: uuid("membership_id").notNull(),
166
+ canonicalIdentityId: uuid("canonical_identity_id").notNull(),
167
+ authUserId: text("auth_user_id").notNull(),
168
+ loginBindingId: uuid("login_binding_id").notNull(),
169
+ membershipAuthorizationRevision: bigint("membership_authorization_revision", {
170
+ mode: "number",
171
+ }).notNull(),
172
+ identityRevision: bigint("identity_revision", { mode: "number" }).notNull(),
173
+ authRevision: bigint("auth_revision", { mode: "number" }).notNull(),
174
+ subjectRevision: bigint("subject_revision", { mode: "number" }).notNull(),
175
+ loginBindingRevision: bigint("login_binding_revision", { mode: "number" }).notNull(),
176
+ custodianAcceptanceId: uuid("custodian_acceptance_id").notNull(),
177
+ reauthOperationId: uuid("reauth_operation_id").notNull(),
178
+ approvedAt: timestamp("approved_at", { withTimezone: true }).notNull().defaultNow(),
179
+ },
180
+ (table) => ({
181
+ current: index("organization_recovery_approvals_current_idx").on(
182
+ table.operationId,
183
+ table.canonicalIdentityId,
184
+ table.approvedAt,
185
+ table.id,
186
+ ),
187
+ }),
188
+ );
189
+
190
+ export const organizationRecoveryCommandReceipts = pgTable(
191
+ "organization_recovery_command_receipts",
192
+ {
193
+ accountId: uuid("account_id").notNull(),
194
+ operationId: uuid("operation_id").notNull(),
195
+ action: text("action").notNull(),
196
+ actorMembershipId: uuid("actor_membership_id").notNull(),
197
+ actorIdentityId: uuid("actor_identity_id").notNull(),
198
+ inputHash: text("input_hash").notNull(),
199
+ result: jsonb("result").notNull(),
200
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
201
+ },
202
+ (table) => ({
203
+ primaryKey: primaryKey({ columns: [table.accountId, table.operationId] }),
204
+ }),
205
+ );
206
+
207
+ export const organizationRecoveryEvents = pgTable(
208
+ "organization_recovery_events",
209
+ {
210
+ id: uuid("id").primaryKey().defaultRandom(),
211
+ accountId: uuid("account_id").notNull(),
212
+ policyId: uuid("policy_id"),
213
+ operationId: uuid("operation_id"),
214
+ commandOperationId: uuid("command_operation_id"),
215
+ eventType: text("event_type").notNull(),
216
+ actorMembershipId: uuid("actor_membership_id"),
217
+ actorIdentityId: uuid("actor_identity_id"),
218
+ eventRevision: bigint("event_revision", { mode: "number" }).notNull(),
219
+ evidence: jsonb("evidence").notNull().default({}),
220
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
221
+ },
222
+ (table) => ({
223
+ commandType: uniqueIndex("organization_recovery_events_command_type_unique")
224
+ .on(table.accountId, table.commandOperationId, table.eventType)
225
+ .where(sql`${table.commandOperationId} is not null`),
226
+ }),
227
+ );
228
+
229
+ export const organizationRecoveryNotificationOutbox = pgTable(
230
+ "organization_recovery_notification_outbox",
231
+ {
232
+ id: uuid("id").primaryKey().defaultRandom(),
233
+ accountId: uuid("account_id").notNull(),
234
+ policyId: uuid("policy_id").notNull(),
235
+ operationId: uuid("operation_id").notNull(),
236
+ batchId: uuid("batch_id").notNull(),
237
+ recipientMembershipId: uuid("recipient_membership_id").notNull(),
238
+ recipientIdentityId: uuid("recipient_identity_id").notNull(),
239
+ audience: text("audience").notNull(),
240
+ notificationType: text("notification_type").notNull(),
241
+ idempotencyKey: text("idempotency_key").notNull().unique(),
242
+ payloadDigest: text("payload_digest").notNull(),
243
+ payload: jsonb("payload").notNull(),
244
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
245
+ },
246
+ (table) => ({
247
+ batch: index("organization_recovery_outbox_batch_idx").on(
248
+ table.operationId,
249
+ table.batchId,
250
+ table.id,
251
+ ),
252
+ }),
253
+ );
254
+
255
+ export const organizationRecoveryNotificationAttempts = pgTable(
256
+ "organization_recovery_notification_attempts",
257
+ {
258
+ id: uuid("id").primaryKey().defaultRandom(),
259
+ outboxId: uuid("outbox_id").notNull(),
260
+ deliveryId: uuid("delivery_id").notNull(),
261
+ provider: text("provider").notNull(),
262
+ claimOwner: text("claim_owner").notNull(),
263
+ attemptNumber: integer("attempt_number").notNull(),
264
+ leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }).notNull(),
265
+ phase: text("phase").notNull(),
266
+ providerMessageId: text("provider_message_id"),
267
+ errorClass: text("error_class"),
268
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
269
+ },
270
+ (table) => ({
271
+ deliveryPhase: unique("organization_recovery_attempts_delivery_phase_unique").on(
272
+ table.outboxId,
273
+ table.deliveryId,
274
+ table.phase,
275
+ ),
276
+ }),
277
+ );
@@ -0,0 +1,424 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { Database } from "./database";
3
+ import { rawRows, setSubjectRlsContext, withRlsContext } from "./database";
4
+ import { nestedPostgresSqlState } from "./persistence-errors";
5
+
6
+ export type OrganizationRecoveryPolicyState =
7
+ | "pending_acceptance"
8
+ | "active"
9
+ | "degraded"
10
+ | "superseded"
11
+ | "disabled";
12
+
13
+ export type OrganizationRecoveryOperationState =
14
+ | "collecting"
15
+ | "cooling"
16
+ | "executed"
17
+ | "cancelled"
18
+ | "expired"
19
+ | "superseded";
20
+
21
+ export type OrganizationRecoveryActorEvidence = {
22
+ authorityHash: string;
23
+ actorEpoch: string;
24
+ };
25
+
26
+ export type OrganizationRecoveryActorFence = OrganizationRecoveryActorEvidence & {
27
+ requestId: string;
28
+ };
29
+
30
+ export type OrganizationRecoveryCapabilities = {
31
+ configure: boolean;
32
+ accept: boolean;
33
+ disable: boolean;
34
+ start: boolean;
35
+ approve: boolean;
36
+ cancel: boolean;
37
+ execute: boolean;
38
+ };
39
+
40
+ export type OrganizationRecoveryMemberSummary = {
41
+ membershipId: string;
42
+ name: string | null;
43
+ email: string | null;
44
+ };
45
+
46
+ export type OrganizationRecoveryOverview = {
47
+ organizationId: string;
48
+ availability: "available" | "recovery_unavailable";
49
+ unavailableReason:
50
+ | "no_policy"
51
+ | "pending_acceptance"
52
+ | "degraded"
53
+ | "disabled"
54
+ | "identity_unavailable"
55
+ | null;
56
+ recentReauthenticationAt: string | null;
57
+ eligibleMembers: OrganizationRecoveryMemberSummary[];
58
+ policy: null | {
59
+ id: string;
60
+ organizationId: string;
61
+ revision: number;
62
+ state: OrganizationRecoveryPolicyState;
63
+ createdAt: string;
64
+ updatedAt: string;
65
+ custodians: Array<{
66
+ ordinal: number;
67
+ membershipId: string;
68
+ name: string | null;
69
+ email: string | null;
70
+ enrollmentState: "pending_acceptance" | "accepted" | "ineligible";
71
+ acceptedAt: string | null;
72
+ }>;
73
+ };
74
+ operation: null | {
75
+ id: string;
76
+ organizationId: string;
77
+ policyId: string;
78
+ policyRevision: number;
79
+ revision: number;
80
+ state: OrganizationRecoveryOperationState;
81
+ target: OrganizationRecoveryMemberSummary;
82
+ approvalCount: number;
83
+ approvals: Array<{
84
+ membershipId: string;
85
+ name: string | null;
86
+ email: string | null;
87
+ approvedAt: string;
88
+ }>;
89
+ quorumAt: string | null;
90
+ executableAt: string | null;
91
+ expiresAt: string;
92
+ notificationJournaled: boolean;
93
+ executedAt: string | null;
94
+ cancelledAt: string | null;
95
+ createdAt: string;
96
+ updatedAt: string;
97
+ };
98
+ capabilities: OrganizationRecoveryCapabilities;
99
+ };
100
+
101
+ export type OrganizationRecoveryMutationResult = {
102
+ replay: boolean;
103
+ overview: OrganizationRecoveryOverview;
104
+ };
105
+
106
+ export class OrganizationRecoveryDeniedError extends Error {
107
+ readonly name = "OrganizationRecoveryDeniedError";
108
+ readonly code = "organization_recovery_denied";
109
+ }
110
+
111
+ export class OrganizationRecoveryUnavailableError extends Error {
112
+ readonly name = "OrganizationRecoveryUnavailableError";
113
+ readonly code = "recovery_unavailable";
114
+ }
115
+
116
+ export class OrganizationRecoveryRevisionConflictError extends Error {
117
+ readonly name = "OrganizationRecoveryRevisionConflictError";
118
+ readonly code = "organization_recovery_revision_conflict";
119
+ }
120
+
121
+ export class OrganizationRecoveryOperationReuseError extends Error {
122
+ readonly name = "OrganizationRecoveryOperationReuseError";
123
+ readonly code = "organization_recovery_operation_reuse";
124
+ }
125
+
126
+ function mapOrganizationRecoverySqlError(error: unknown): never {
127
+ const state = nestedPostgresSqlState(error);
128
+ if (state === "40001") throw new OrganizationRecoveryRevisionConflictError();
129
+ if (state === "23505") throw new OrganizationRecoveryOperationReuseError();
130
+ if (state === "55000") {
131
+ throw new OrganizationRecoveryUnavailableError(
132
+ error instanceof Error ? error.message : "Organization recovery is unavailable",
133
+ );
134
+ }
135
+ if (state === "42501" || state === "P0002") {
136
+ throw new OrganizationRecoveryDeniedError(
137
+ error instanceof Error ? error.message : "Organization recovery authority denied",
138
+ );
139
+ }
140
+ throw error;
141
+ }
142
+
143
+ function asOverview(value: unknown): OrganizationRecoveryOverview {
144
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
145
+ throw new Error("Organization recovery returned an invalid overview");
146
+ }
147
+ return value as OrganizationRecoveryOverview;
148
+ }
149
+
150
+ function asMutationResult(value: unknown): OrganizationRecoveryMutationResult {
151
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
152
+ throw new Error("Organization recovery returned an invalid mutation result");
153
+ }
154
+ const candidate = value as { replay?: unknown; overview?: unknown };
155
+ if (typeof candidate.replay !== "boolean") {
156
+ throw new Error("Organization recovery returned an invalid replay result");
157
+ }
158
+ return { replay: candidate.replay, overview: asOverview(candidate.overview) };
159
+ }
160
+
161
+ async function scopedRecoveryJson(
162
+ db: Database,
163
+ organizationId: string,
164
+ actorSubjectId: string,
165
+ query: (scopedDb: Database) => Promise<unknown>,
166
+ ): Promise<OrganizationRecoveryOverview> {
167
+ try {
168
+ return await withRlsContext(
169
+ db,
170
+ { accountId: organizationId, workspaceId: null },
171
+ async (scopedDb) => {
172
+ await setSubjectRlsContext(scopedDb, actorSubjectId);
173
+ return asOverview(await query(scopedDb));
174
+ },
175
+ );
176
+ } catch (error) {
177
+ mapOrganizationRecoverySqlError(error);
178
+ }
179
+ }
180
+
181
+ async function runRecoveryCommand(
182
+ db: Database,
183
+ command: Record<string, unknown> & {
184
+ organizationId: string;
185
+ actorSubjectId: string;
186
+ },
187
+ ): Promise<OrganizationRecoveryMutationResult> {
188
+ try {
189
+ return await withRlsContext(
190
+ db,
191
+ { accountId: command.organizationId, workspaceId: null },
192
+ async (scopedDb) => {
193
+ await setSubjectRlsContext(scopedDb, command.actorSubjectId);
194
+ const [row] = await rawRows<{ result: unknown }>(
195
+ scopedDb,
196
+ sql`select organization_recovery_command(${JSON.stringify(command)}::jsonb) as result`,
197
+ );
198
+ return asMutationResult(row?.result);
199
+ },
200
+ );
201
+ } catch (error) {
202
+ mapOrganizationRecoverySqlError(error);
203
+ }
204
+ }
205
+
206
+ export async function getOrganizationRecoveryOverview(
207
+ db: Database,
208
+ input: {
209
+ organizationId: string;
210
+ actorSubjectId: string;
211
+ actorAuthUserId: string;
212
+ actorAuthSessionId: string;
213
+ actorFence: OrganizationRecoveryActorEvidence | null;
214
+ },
215
+ ): Promise<OrganizationRecoveryOverview> {
216
+ return await scopedRecoveryJson(
217
+ db,
218
+ input.organizationId,
219
+ input.actorSubjectId,
220
+ async (scopedDb) => {
221
+ const [row] = await rawRows<{ result: unknown }>(
222
+ scopedDb,
223
+ sql`select get_organization_recovery_overview(
224
+ ${input.organizationId}::uuid, ${input.actorSubjectId},
225
+ ${input.actorFence ? JSON.stringify(input.actorFence) : null}::jsonb,
226
+ ${input.actorAuthSessionId}, ${input.actorAuthUserId}
227
+ ) as result`,
228
+ );
229
+ return row?.result;
230
+ },
231
+ );
232
+ }
233
+
234
+ type RecoveryMutationBase = {
235
+ organizationId: string;
236
+ actorSubjectId: string;
237
+ actorAuthUserId: string;
238
+ actorAuthSessionId: string;
239
+ operationId: string;
240
+ actorFence: OrganizationRecoveryActorFence;
241
+ };
242
+
243
+ export type OrganizationRecoveryNotificationClaim = {
244
+ attemptId: string;
245
+ outboxId: string;
246
+ deliveryId: string;
247
+ provider: string;
248
+ claimOwner: string;
249
+ attemptNumber: number;
250
+ leaseExpiresAt: string;
251
+ idempotencyKey: string;
252
+ recipientCanonicalIdentityId: string;
253
+ notificationType: "recovery_quorum_started";
254
+ payloadDigest: string;
255
+ payload: Record<string, unknown>;
256
+ };
257
+
258
+ export type OrganizationRecoveryNotificationSettlement = {
259
+ outboxId: string;
260
+ deliveryId: string;
261
+ claimOwner: string;
262
+ attemptNumber: number;
263
+ phase: "sent" | "failed" | "outcome_unknown";
264
+ providerMessageId: string | null;
265
+ errorClass: string | null;
266
+ settledAt: string;
267
+ replay: boolean;
268
+ };
269
+
270
+ export type OrganizationRecoveryNotificationReconciliation = {
271
+ outboxId: string;
272
+ deliveryId: string;
273
+ reconciliationOwner: string;
274
+ attemptNumber: number;
275
+ resolution: "sent" | "retry";
276
+ providerMessageId: string | null;
277
+ reconciledAt: string;
278
+ replay: boolean;
279
+ };
280
+
281
+ function asObject<T>(value: unknown, label: string): T {
282
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
283
+ throw new Error(`Organization recovery returned invalid ${label}`);
284
+ }
285
+ return value as T;
286
+ }
287
+
288
+ export async function prepareOrganizationRecoveryNotifications(
289
+ db: Database,
290
+ input: {
291
+ provider: string;
292
+ claimOwner: string;
293
+ limit: number;
294
+ leaseSeconds: number;
295
+ },
296
+ ): Promise<OrganizationRecoveryNotificationClaim[]> {
297
+ const [row] = await rawRows<{ result: unknown }>(
298
+ db,
299
+ sql`select prepare_organization_recovery_notifications(
300
+ ${input.provider}, ${input.claimOwner}, ${input.limit}::integer,
301
+ ${input.leaseSeconds}::integer
302
+ ) as result`,
303
+ );
304
+ if (!Array.isArray(row?.result)) {
305
+ throw new Error("Organization recovery returned invalid notification claims");
306
+ }
307
+ return row.result as OrganizationRecoveryNotificationClaim[];
308
+ }
309
+
310
+ export const claimOrganizationRecoveryNotifications = prepareOrganizationRecoveryNotifications;
311
+
312
+ export async function settleOrganizationRecoveryNotification(
313
+ db: Database,
314
+ input: {
315
+ outboxId: string;
316
+ deliveryId: string;
317
+ claimOwner: string;
318
+ phase: "sent" | "failed" | "outcome_unknown";
319
+ providerMessageId?: string | null;
320
+ errorClass?: string | null;
321
+ },
322
+ ): Promise<OrganizationRecoveryNotificationSettlement> {
323
+ const [row] = await rawRows<{ result: unknown }>(
324
+ db,
325
+ sql`select settle_organization_recovery_notification(
326
+ ${input.outboxId}::uuid, ${input.deliveryId}::uuid, ${input.claimOwner},
327
+ ${input.phase}, ${input.providerMessageId ?? null}, ${input.errorClass ?? null}
328
+ ) as result`,
329
+ );
330
+ return asObject<OrganizationRecoveryNotificationSettlement>(
331
+ row?.result,
332
+ "notification settlement",
333
+ );
334
+ }
335
+
336
+ export async function reconcileOrganizationRecoveryNotification(
337
+ db: Database,
338
+ input: {
339
+ outboxId: string;
340
+ deliveryId: string;
341
+ reconciliationOwner: string;
342
+ resolution: "sent" | "retry";
343
+ providerMessageId?: string | null;
344
+ },
345
+ ): Promise<OrganizationRecoveryNotificationReconciliation> {
346
+ const [row] = await rawRows<{ result: unknown }>(
347
+ db,
348
+ sql`select reconcile_organization_recovery_notification(
349
+ ${input.outboxId}::uuid, ${input.deliveryId}::uuid,
350
+ ${input.reconciliationOwner}, ${input.resolution},
351
+ ${input.providerMessageId ?? null}
352
+ ) as result`,
353
+ );
354
+ return asObject<OrganizationRecoveryNotificationReconciliation>(
355
+ row?.result,
356
+ "notification reconciliation",
357
+ );
358
+ }
359
+
360
+ export async function configureOrganizationRecoveryPolicy(
361
+ db: Database,
362
+ input: RecoveryMutationBase & {
363
+ expectedPolicyRevision: number;
364
+ custodianMembershipIds: [string, string, string];
365
+ },
366
+ ): Promise<OrganizationRecoveryMutationResult> {
367
+ return await runRecoveryCommand(db, { action: "configure_policy", ...input });
368
+ }
369
+
370
+ export async function acceptOrganizationRecoveryCustody(
371
+ db: Database,
372
+ input: RecoveryMutationBase & { expectedPolicyRevision: number },
373
+ ): Promise<OrganizationRecoveryMutationResult> {
374
+ return await runRecoveryCommand(db, { action: "accept_custody", ...input });
375
+ }
376
+
377
+ export async function disableOrganizationRecoveryPolicy(
378
+ db: Database,
379
+ input: RecoveryMutationBase & { expectedPolicyRevision: number },
380
+ ): Promise<OrganizationRecoveryMutationResult> {
381
+ return await runRecoveryCommand(db, { action: "disable_policy", ...input });
382
+ }
383
+
384
+ export async function startOrganizationRecoveryOperation(
385
+ db: Database,
386
+ input: RecoveryMutationBase & {
387
+ expectedPolicyRevision: number;
388
+ targetMembershipId: string;
389
+ },
390
+ ): Promise<OrganizationRecoveryMutationResult> {
391
+ return await runRecoveryCommand(db, { action: "start_operation", ...input });
392
+ }
393
+
394
+ type RecoveryOperationMutation = RecoveryMutationBase & {
395
+ recoveryOperationId: string;
396
+ expectedOperationRevision: number;
397
+ };
398
+
399
+ export async function approveOrganizationRecoveryOperation(
400
+ db: Database,
401
+ input: RecoveryOperationMutation,
402
+ ): Promise<OrganizationRecoveryMutationResult> {
403
+ return await runRecoveryCommand(db, {
404
+ action: "approve_operation",
405
+ ...input,
406
+ });
407
+ }
408
+
409
+ export async function cancelOrganizationRecoveryOperation(
410
+ db: Database,
411
+ input: RecoveryOperationMutation,
412
+ ): Promise<OrganizationRecoveryMutationResult> {
413
+ return await runRecoveryCommand(db, { action: "cancel_operation", ...input });
414
+ }
415
+
416
+ export async function executeOrganizationRecoveryOperation(
417
+ db: Database,
418
+ input: RecoveryOperationMutation,
419
+ ): Promise<OrganizationRecoveryMutationResult> {
420
+ return await runRecoveryCommand(db, {
421
+ action: "execute_operation",
422
+ ...input,
423
+ });
424
+ }