@opengeni/db 0.14.0 → 0.16.2
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/{chunk-ELMUCYZF.js → chunk-4RUK5CON.js} +183 -42
- package/dist/chunk-4RUK5CON.js.map +1 -0
- package/dist/chunk-HZD7KVAR.js +4648 -0
- package/dist/chunk-HZD7KVAR.js.map +1 -0
- package/dist/{chunk-Y5WZZVQK.js → chunk-SK7YYHBI.js} +5 -2
- package/dist/chunk-SK7YYHBI.js.map +1 -0
- package/dist/codex-token-resolver.d.ts +64 -0
- package/dist/connection-token-resolver.d.ts +90 -0
- package/dist/environment-crypto.d.ts +11 -0
- package/dist/event-payload-sanitizer.d.ts +32 -0
- package/dist/index.d.ts +6217 -10
- package/dist/index.js +6992 -2728
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +157 -0
- package/dist/memory-domain.d.ts +44 -0
- package/dist/migrate.d.ts +5 -7
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +49 -0
- package/dist/persistence-errors.d.ts +69 -0
- package/dist/preference-registry-schema.d.ts +1147 -0
- package/dist/preference-registry.d.ts +878 -0
- package/dist/provision-roles.d.ts +4 -6478
- package/dist/provision-roles.js +1 -1
- package/dist/role-relationships.d.ts +35 -0
- package/dist/runtime-posture-cli.d.ts +1 -0
- package/dist/runtime-posture.d.ts +103 -0
- package/dist/schema.d.ts +23533 -3
- package/dist/schema.js +17 -1
- package/dist/session-control.d.ts +331 -0
- package/dist/session-queue-commands.d.ts +186 -0
- package/dist/session-tool-call-settlement.d.ts +32 -0
- package/dist/turn-initiator.d.ts +28 -0
- package/dist/workspace-instruction-policies-schema.d.ts +735 -0
- package/dist/workspace-instruction-policies.d.ts +64 -0
- package/drizzle/0136_unified_session_tool_policy.sql +109 -0
- package/drizzle/0137_preference_registry.sql +1347 -0
- package/drizzle/0138_sandbox_checkpoint_artifacts_and_deadlines.sql +1074 -0
- package/drizzle/0139_codex_provider_artifact_invalidations.sql +51 -0
- package/drizzle/0140_sandbox_restore_and_reaper_fences.sql +276 -0
- package/drizzle/0142_sandbox_archive_capture_gate.sql +80 -0
- package/drizzle/0143_session_codex_compaction_mode.sql +20 -0
- package/drizzle/0144_sandbox_viewer_force_drain_gate.sql +95 -0
- package/drizzle/0145_model_call_facts.sql +96 -0
- package/drizzle/0146_slack_bot_delete_idempotency.sql +105 -0
- package/drizzle/0147_draft_latency_mode.sql +12 -0
- package/drizzle/0148_session_turn_latency_mode.sql +12 -0
- package/package.json +6 -6
- package/src/index.ts +5095 -430
- package/src/insights.ts +837 -0
- package/src/migrate.ts +9 -1
- package/src/new-session-drafts.ts +4 -0
- package/src/preference-registry-schema.ts +170 -0
- package/src/preference-registry.ts +1220 -0
- package/src/provision-roles.ts +95 -22
- package/src/role-relationships.ts +132 -0
- package/src/runtime-posture.ts +28 -26
- package/src/schema.ts +484 -12
- package/src/session-queue-commands.ts +47 -46
- package/dist/chunk-ELMUCYZF.js.map +0 -1
- package/dist/chunk-PQKVUWQW.js +0 -4047
- package/dist/chunk-PQKVUWQW.js.map +0 -1
- package/dist/chunk-Y5WZZVQK.js.map +0 -1
- package/dist/schema-FoPub9yt.d.ts +0 -22666
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
PREFERENCE_REGISTRY_DESCRIPTOR_DESCRIPTION_MAX_CHARS,
|
|
4
|
+
PREFERENCE_REGISTRY_DESCRIPTOR_MAX_COUNT,
|
|
5
|
+
PREFERENCE_REGISTRY_DESCRIPTOR_MAX_UTF8_BYTES,
|
|
6
|
+
PREFERENCE_REGISTRY_TITLE_MAX_CHARS,
|
|
7
|
+
PreferenceRegistryDescriptor,
|
|
8
|
+
PreferenceRegistryDetailResponse,
|
|
9
|
+
PreferenceRegistryEvent,
|
|
10
|
+
PreferenceRegistryFullContent,
|
|
11
|
+
PreferenceRegistryListResponse,
|
|
12
|
+
PreferenceRegistryRecord,
|
|
13
|
+
PreferenceRegistryRevisionSummary,
|
|
14
|
+
PreferenceRegistrySnapshot,
|
|
15
|
+
PreferenceRegistryScopeTarget,
|
|
16
|
+
type CreatePreferenceRegistryProposalRequest,
|
|
17
|
+
type PreferenceRegistryConflictStrategy,
|
|
18
|
+
type PreferenceRegistryProvenanceSource,
|
|
19
|
+
type PreferenceRegistryScope,
|
|
20
|
+
type PreferenceRegistryStatus,
|
|
21
|
+
type PreferenceRegistryTrust,
|
|
22
|
+
} from "@opengeni/contracts";
|
|
23
|
+
import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql, type SQL } from "drizzle-orm";
|
|
24
|
+
import type { Database } from "./index";
|
|
25
|
+
import { setSubjectRlsContext, withWorkspaceRls, withWorkspaceSubjectRls } from "./index";
|
|
26
|
+
import { nestedPostgresSqlState, safeDatabaseErrorFacts } from "./persistence-errors";
|
|
27
|
+
import * as schema from "./schema";
|
|
28
|
+
|
|
29
|
+
type PreferenceRow = typeof schema.preferenceRegistryPreferences.$inferSelect;
|
|
30
|
+
type RevisionRow = typeof schema.preferenceRegistryRevisions.$inferSelect;
|
|
31
|
+
type EventRow = typeof schema.preferenceRegistryEvents.$inferSelect;
|
|
32
|
+
|
|
33
|
+
const importedSources = new Set<PreferenceRegistryProvenanceSource>([
|
|
34
|
+
"knowledge_proposal",
|
|
35
|
+
"imported_document",
|
|
36
|
+
"slack",
|
|
37
|
+
"meeting_transcript",
|
|
38
|
+
"call_transcript",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export class PreferenceRegistryNotFoundError extends Error {
|
|
42
|
+
readonly name = "PreferenceRegistryNotFoundError";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PreferenceRegistryInvalidOperationError extends Error {
|
|
46
|
+
readonly name = "PreferenceRegistryInvalidOperationError";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PreferenceRegistryConflictError extends Error {
|
|
50
|
+
readonly name = "PreferenceRegistryConflictError";
|
|
51
|
+
readonly code = "PREFERENCE_REGISTRY_CONFLICT";
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
message: string,
|
|
55
|
+
readonly currentRevisionId: string | null,
|
|
56
|
+
readonly scopeVersion: number | null,
|
|
57
|
+
) {
|
|
58
|
+
super(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class PreferenceRegistryInitiatorError extends Error {
|
|
63
|
+
readonly name = "PreferenceRegistryInitiatorError";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class PreferenceRegistryStableKeyConflictError extends Error {
|
|
67
|
+
readonly name = "PreferenceRegistryStableKeyConflictError";
|
|
68
|
+
readonly code = "PREFERENCE_REGISTRY_STABLE_KEY_CONFLICT";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type PreferenceRegistryAttemptClaims = {
|
|
72
|
+
accountId: string;
|
|
73
|
+
workspaceId: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
turnId: string;
|
|
76
|
+
attemptId: string;
|
|
77
|
+
executionGeneration: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type PreferenceRegistryAttemptAuthority = PreferenceRegistryAttemptClaims & {
|
|
81
|
+
initiatingHumanSubjectId: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type PreferenceRegistryScopeAuthorizer = (scope: PreferenceRegistryScope) => void;
|
|
85
|
+
|
|
86
|
+
function iso(value: Date | string): string {
|
|
87
|
+
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function contentHash(value: string): string {
|
|
91
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Plain one-line descriptor text; never include full imported source content. */
|
|
95
|
+
export function sanitizePreferenceDescriptorText(value: string): string {
|
|
96
|
+
return value
|
|
97
|
+
.normalize("NFKC")
|
|
98
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
99
|
+
.replace(/[<>{}[\]`]/g, " ")
|
|
100
|
+
.replace(/\s+/gu, " ")
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function boundedDescriptorText(value: string, maxChars: number): string {
|
|
105
|
+
return sanitizePreferenceDescriptorText(value).slice(0, maxChars).trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isStableKeyConflict(error: unknown): boolean {
|
|
109
|
+
if (nestedPostgresSqlState(error) !== "23505") return false;
|
|
110
|
+
return (safeDatabaseErrorFacts(error).constraint ?? "").startsWith(
|
|
111
|
+
"preference_registry_preferences_",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function targetFor(
|
|
116
|
+
scope: PreferenceRegistryScope,
|
|
117
|
+
workspaceId: string,
|
|
118
|
+
subjectId: string,
|
|
119
|
+
): PreferenceRegistryScopeTarget {
|
|
120
|
+
if (scope === "organization") return { scope, workspaceId: null, subjectId: null };
|
|
121
|
+
if (scope === "workspace") return { scope, workspaceId, subjectId: null };
|
|
122
|
+
return { scope, workspaceId: null, subjectId };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function targetFromRow(row: PreferenceRow): PreferenceRegistryScopeTarget {
|
|
126
|
+
return {
|
|
127
|
+
scope: row.scope as PreferenceRegistryScope,
|
|
128
|
+
workspaceId: row.scopeWorkspaceId,
|
|
129
|
+
subjectId: row.scopeSubjectId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function trustFor(
|
|
134
|
+
scope: PreferenceRegistryScope,
|
|
135
|
+
source: PreferenceRegistryProvenanceSource,
|
|
136
|
+
): PreferenceRegistryTrust {
|
|
137
|
+
if (importedSources.has(source)) return "untrusted_proposal";
|
|
138
|
+
if (scope === "organization") return "organization_managed";
|
|
139
|
+
if (scope === "workspace") return "workspace_managed";
|
|
140
|
+
return "personal";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function revisionSummary(row: RevisionRow, scope: PreferenceRegistryScope) {
|
|
144
|
+
return PreferenceRegistryRevisionSummary.parse({
|
|
145
|
+
id: row.id,
|
|
146
|
+
preferenceId: row.preferenceId,
|
|
147
|
+
revision: row.revision,
|
|
148
|
+
contentHash: row.contentHash,
|
|
149
|
+
title: row.title,
|
|
150
|
+
description: row.description,
|
|
151
|
+
precedence: {
|
|
152
|
+
tier: scope,
|
|
153
|
+
rank: row.precedenceRank,
|
|
154
|
+
conflictStrategy: row.conflictStrategy as PreferenceRegistryConflictStrategy,
|
|
155
|
+
conflictsWith: row.conflictsWith,
|
|
156
|
+
},
|
|
157
|
+
provenance: {
|
|
158
|
+
source: row.provenanceSource as PreferenceRegistryProvenanceSource,
|
|
159
|
+
sourceId: row.provenanceSourceId,
|
|
160
|
+
trust: row.trust as PreferenceRegistryTrust,
|
|
161
|
+
},
|
|
162
|
+
expiresAt: row.expiresAt ? iso(row.expiresAt) : null,
|
|
163
|
+
correctsRevisionId: row.correctsRevisionId,
|
|
164
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
165
|
+
createdAt: iso(row.createdAt),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function effectiveStatus(
|
|
170
|
+
row: PreferenceRow,
|
|
171
|
+
revision: RevisionRow | null,
|
|
172
|
+
now: Date,
|
|
173
|
+
): PreferenceRegistryStatus {
|
|
174
|
+
if (row.status === "active" && revision?.expiresAt && revision.expiresAt <= now) return "expired";
|
|
175
|
+
return row.status as PreferenceRegistryStatus;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function recordFromRows(row: PreferenceRow, active: RevisionRow | null, now = new Date()) {
|
|
179
|
+
return PreferenceRegistryRecord.parse({
|
|
180
|
+
id: row.id,
|
|
181
|
+
accountId: row.accountId,
|
|
182
|
+
stableKey: row.stableKey,
|
|
183
|
+
target: targetFromRow(row),
|
|
184
|
+
status: effectiveStatus(row, active, now),
|
|
185
|
+
scopeVersion: row.scopeVersion,
|
|
186
|
+
activationVersion: row.activationVersion,
|
|
187
|
+
activeRevision: active ? revisionSummary(active, row.scope as PreferenceRegistryScope) : null,
|
|
188
|
+
supersededByPreferenceId: row.supersededByPreferenceId,
|
|
189
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
190
|
+
createdAt: iso(row.createdAt),
|
|
191
|
+
updatedAt: iso(row.updatedAt),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function eventTarget(scope: string | null, workspaceId: string | null, subjectId: string | null) {
|
|
196
|
+
return scope === null
|
|
197
|
+
? null
|
|
198
|
+
: PreferenceRegistryScopeTarget.parse({ scope, workspaceId, subjectId });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function eventFromRow(row: EventRow) {
|
|
202
|
+
return PreferenceRegistryEvent.parse({
|
|
203
|
+
id: row.id,
|
|
204
|
+
accountId: row.accountId,
|
|
205
|
+
preferenceId: row.preferenceId,
|
|
206
|
+
type: row.type,
|
|
207
|
+
version: row.version,
|
|
208
|
+
oldRevisionId: row.oldRevisionId,
|
|
209
|
+
newRevisionId: row.newRevisionId,
|
|
210
|
+
oldTarget: eventTarget(row.oldScope, row.oldWorkspaceId, row.oldSubjectId),
|
|
211
|
+
newTarget: eventTarget(row.newScope, row.newWorkspaceId, row.newSubjectId),
|
|
212
|
+
relatedPreferenceId: row.relatedPreferenceId,
|
|
213
|
+
actorSubjectId: row.actorSubjectId,
|
|
214
|
+
reason: row.reason,
|
|
215
|
+
createdAt: iso(row.createdAt),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function lockPreferenceIds(db: Database, ids: readonly string[]): Promise<string[]> {
|
|
220
|
+
const uniqueIds = [...new Set(ids)].sort();
|
|
221
|
+
const idArray = sql.join(
|
|
222
|
+
uniqueIds.map((id) => sql`${id}::uuid`),
|
|
223
|
+
sql`, `,
|
|
224
|
+
);
|
|
225
|
+
const rows = (await db.execute(sql`
|
|
226
|
+
SELECT preference_id
|
|
227
|
+
FROM preference_registry_lock_heads(ARRAY[${idArray}]::uuid[])
|
|
228
|
+
`)) as unknown as Array<{ preference_id: string }>;
|
|
229
|
+
const lockedIds = rows.map((row) => row.preference_id).sort();
|
|
230
|
+
if (
|
|
231
|
+
lockedIds.length !== uniqueIds.length ||
|
|
232
|
+
lockedIds.some((id, index) => id !== uniqueIds[index])
|
|
233
|
+
) {
|
|
234
|
+
throw new PreferenceRegistryNotFoundError("Preference was not found");
|
|
235
|
+
}
|
|
236
|
+
return uniqueIds;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function lockedPreference(db: Database, id: string): Promise<PreferenceRow> {
|
|
240
|
+
await lockPreferenceIds(db, [id]);
|
|
241
|
+
const [row] = await db
|
|
242
|
+
.select()
|
|
243
|
+
.from(schema.preferenceRegistryPreferences)
|
|
244
|
+
.where(eq(schema.preferenceRegistryPreferences.id, id))
|
|
245
|
+
.limit(1);
|
|
246
|
+
if (!row) throw new PreferenceRegistryNotFoundError("Preference was not found");
|
|
247
|
+
return row;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function lockedPreferences(db: Database, ids: readonly string[]): Promise<PreferenceRow[]> {
|
|
251
|
+
const uniqueIds = await lockPreferenceIds(db, ids);
|
|
252
|
+
const rows = await db
|
|
253
|
+
.select()
|
|
254
|
+
.from(schema.preferenceRegistryPreferences)
|
|
255
|
+
.where(inArray(schema.preferenceRegistryPreferences.id, uniqueIds))
|
|
256
|
+
.orderBy(asc(schema.preferenceRegistryPreferences.id));
|
|
257
|
+
if (rows.length !== uniqueIds.length) {
|
|
258
|
+
throw new PreferenceRegistryNotFoundError("Preference was not found");
|
|
259
|
+
}
|
|
260
|
+
return rows;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function revisionForPreference(
|
|
264
|
+
db: Database,
|
|
265
|
+
preferenceId: string,
|
|
266
|
+
revisionId: string,
|
|
267
|
+
): Promise<RevisionRow> {
|
|
268
|
+
const [row] = await db
|
|
269
|
+
.select()
|
|
270
|
+
.from(schema.preferenceRegistryRevisions)
|
|
271
|
+
.where(
|
|
272
|
+
and(
|
|
273
|
+
eq(schema.preferenceRegistryRevisions.preferenceId, preferenceId),
|
|
274
|
+
eq(schema.preferenceRegistryRevisions.id, revisionId),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
.limit(1);
|
|
278
|
+
if (!row) throw new PreferenceRegistryNotFoundError("Preference revision was not found");
|
|
279
|
+
return row;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function targetColumns(target: PreferenceRegistryScopeTarget) {
|
|
283
|
+
return {
|
|
284
|
+
scope: target.scope,
|
|
285
|
+
scopeWorkspaceId: target.workspaceId,
|
|
286
|
+
scopeSubjectId: target.subjectId,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function withPreferenceRegistryGovernanceRls<T>(
|
|
291
|
+
db: Database,
|
|
292
|
+
input: {
|
|
293
|
+
workspaceId: string;
|
|
294
|
+
actorSubjectId: string;
|
|
295
|
+
principalKind: string | undefined;
|
|
296
|
+
},
|
|
297
|
+
fn: (db: Database) => Promise<T>,
|
|
298
|
+
): Promise<T> {
|
|
299
|
+
if (input.principalKind !== "human_session") {
|
|
300
|
+
throw new PreferenceRegistryInitiatorError(
|
|
301
|
+
"Preference governance requires an authenticated human session principal",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return await withWorkspaceSubjectRls(
|
|
305
|
+
db,
|
|
306
|
+
input.workspaceId,
|
|
307
|
+
input.actorSubjectId,
|
|
308
|
+
async (scopedDb) => {
|
|
309
|
+
const rows = (await scopedDb.execute(sql`
|
|
310
|
+
SELECT set_config('opengeni.principal_kind', ${input.principalKind}, true)
|
|
311
|
+
AS principal_kind
|
|
312
|
+
`)) as unknown as Array<{ principal_kind: string }>;
|
|
313
|
+
if (rows[0]?.principal_kind !== "human_session") {
|
|
314
|
+
throw new PreferenceRegistryInitiatorError(
|
|
315
|
+
"Preference governance principal kind was not applied to the transaction",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return await fn(scopedDb);
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function requireExpectedScopeVersion(row: PreferenceRow, expectedScopeVersion: number): void {
|
|
324
|
+
if (row.scopeVersion !== expectedScopeVersion) {
|
|
325
|
+
throw new PreferenceRegistryConflictError(
|
|
326
|
+
"The preference scope changed in another request",
|
|
327
|
+
row.activeRevisionId,
|
|
328
|
+
row.scopeVersion,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function lifecycleEvent(
|
|
334
|
+
db: Database,
|
|
335
|
+
input: {
|
|
336
|
+
operation:
|
|
337
|
+
| "proposal_created"
|
|
338
|
+
| "activate"
|
|
339
|
+
| "correct"
|
|
340
|
+
| "reject"
|
|
341
|
+
| "deactivate"
|
|
342
|
+
| "supersede"
|
|
343
|
+
| "scope";
|
|
344
|
+
preferenceId: string;
|
|
345
|
+
expectedScopeVersion: number;
|
|
346
|
+
expectedRevisionId: string | null;
|
|
347
|
+
revisionId?: string | null;
|
|
348
|
+
newScope?: PreferenceRegistryScope | null;
|
|
349
|
+
relatedPreferenceId?: string | null;
|
|
350
|
+
actorSubjectId: string;
|
|
351
|
+
reason: string;
|
|
352
|
+
},
|
|
353
|
+
): Promise<EventRow> {
|
|
354
|
+
const rows = (await db.execute(sql`
|
|
355
|
+
SELECT event_id
|
|
356
|
+
FROM preference_registry_apply_lifecycle(
|
|
357
|
+
${input.operation},
|
|
358
|
+
${input.preferenceId}::uuid,
|
|
359
|
+
${input.expectedScopeVersion},
|
|
360
|
+
${input.expectedRevisionId}::uuid,
|
|
361
|
+
${input.revisionId ?? null}::uuid,
|
|
362
|
+
${input.newScope ?? null},
|
|
363
|
+
${input.relatedPreferenceId ?? null}::uuid,
|
|
364
|
+
${input.actorSubjectId},
|
|
365
|
+
${input.reason}
|
|
366
|
+
)
|
|
367
|
+
`)) as unknown as Array<{ event_id: string }>;
|
|
368
|
+
const eventId = rows[0]?.event_id;
|
|
369
|
+
if (!eventId) throw new Error("Preference lifecycle mutation returned no event");
|
|
370
|
+
const [event] = await db
|
|
371
|
+
.select()
|
|
372
|
+
.from(schema.preferenceRegistryEvents)
|
|
373
|
+
.where(eq(schema.preferenceRegistryEvents.id, eventId))
|
|
374
|
+
.limit(1);
|
|
375
|
+
if (!event) throw new Error("Preference lifecycle event was not visible after mutation");
|
|
376
|
+
return event;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function preferenceAfterMutation(db: Database, preferenceId: string): Promise<PreferenceRow> {
|
|
380
|
+
const [row] = await db
|
|
381
|
+
.select()
|
|
382
|
+
.from(schema.preferenceRegistryPreferences)
|
|
383
|
+
.where(eq(schema.preferenceRegistryPreferences.id, preferenceId))
|
|
384
|
+
.limit(1);
|
|
385
|
+
if (!row) throw new PreferenceRegistryNotFoundError("Preference was not found");
|
|
386
|
+
return row;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function insertRevision(
|
|
390
|
+
db: Database,
|
|
391
|
+
input: {
|
|
392
|
+
accountId: string;
|
|
393
|
+
preferenceId: string;
|
|
394
|
+
scope: PreferenceRegistryScope;
|
|
395
|
+
actorSubjectId: string;
|
|
396
|
+
title: string;
|
|
397
|
+
description: string;
|
|
398
|
+
content: string;
|
|
399
|
+
precedenceRank: number;
|
|
400
|
+
conflictStrategy: PreferenceRegistryConflictStrategy;
|
|
401
|
+
conflictsWith: string[];
|
|
402
|
+
provenanceSource: PreferenceRegistryProvenanceSource;
|
|
403
|
+
provenanceSourceId: string | null;
|
|
404
|
+
expiresAt: string | null;
|
|
405
|
+
correctsRevisionId: string | null;
|
|
406
|
+
},
|
|
407
|
+
): Promise<RevisionRow> {
|
|
408
|
+
const title = boundedDescriptorText(input.title, PREFERENCE_REGISTRY_TITLE_MAX_CHARS);
|
|
409
|
+
const description = boundedDescriptorText(
|
|
410
|
+
input.description,
|
|
411
|
+
PREFERENCE_REGISTRY_DESCRIPTOR_DESCRIPTION_MAX_CHARS,
|
|
412
|
+
);
|
|
413
|
+
if (!title || !description) {
|
|
414
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
415
|
+
"Preference title and description must contain visible plain text",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const [created] = await db
|
|
419
|
+
.insert(schema.preferenceRegistryRevisions)
|
|
420
|
+
.values({
|
|
421
|
+
accountId: input.accountId,
|
|
422
|
+
preferenceId: input.preferenceId,
|
|
423
|
+
title,
|
|
424
|
+
description,
|
|
425
|
+
content: input.content,
|
|
426
|
+
contentHash: contentHash(input.content),
|
|
427
|
+
precedenceRank: input.precedenceRank,
|
|
428
|
+
conflictStrategy: input.conflictStrategy,
|
|
429
|
+
conflictsWith: [...new Set(input.conflictsWith)].sort(),
|
|
430
|
+
provenanceSource: input.provenanceSource,
|
|
431
|
+
provenanceSourceId: input.provenanceSourceId,
|
|
432
|
+
trust: trustFor(input.scope, input.provenanceSource),
|
|
433
|
+
expiresAt: input.expiresAt ? new Date(input.expiresAt) : null,
|
|
434
|
+
correctsRevisionId: input.correctsRevisionId,
|
|
435
|
+
createdBySubjectId: input.actorSubjectId,
|
|
436
|
+
})
|
|
437
|
+
.returning();
|
|
438
|
+
if (!created) throw new Error("Preference revision was not created");
|
|
439
|
+
return created;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function createPreferenceRegistryProposal(
|
|
443
|
+
db: Database,
|
|
444
|
+
input: CreatePreferenceRegistryProposalRequest & {
|
|
445
|
+
accountId: string;
|
|
446
|
+
workspaceId: string;
|
|
447
|
+
actorSubjectId: string;
|
|
448
|
+
principalKind: string | undefined;
|
|
449
|
+
},
|
|
450
|
+
) {
|
|
451
|
+
const target = targetFor(input.scope, input.workspaceId, input.actorSubjectId);
|
|
452
|
+
try {
|
|
453
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
454
|
+
const [preference] = await scopedDb
|
|
455
|
+
.insert(schema.preferenceRegistryPreferences)
|
|
456
|
+
.values({
|
|
457
|
+
accountId: input.accountId,
|
|
458
|
+
stableKey: input.stableKey,
|
|
459
|
+
...targetColumns(target),
|
|
460
|
+
createdBySubjectId: input.actorSubjectId,
|
|
461
|
+
})
|
|
462
|
+
.returning();
|
|
463
|
+
if (!preference) throw new Error("Preference proposal was not created");
|
|
464
|
+
const revision = await insertRevision(scopedDb, {
|
|
465
|
+
...input,
|
|
466
|
+
preferenceId: preference.id,
|
|
467
|
+
correctsRevisionId: null,
|
|
468
|
+
});
|
|
469
|
+
await lifecycleEvent(scopedDb, {
|
|
470
|
+
operation: "proposal_created",
|
|
471
|
+
preferenceId: preference.id,
|
|
472
|
+
expectedScopeVersion: preference.scopeVersion,
|
|
473
|
+
expectedRevisionId: null,
|
|
474
|
+
revisionId: revision.id,
|
|
475
|
+
actorSubjectId: input.actorSubjectId,
|
|
476
|
+
reason: importedSources.has(input.provenanceSource)
|
|
477
|
+
? "Imported source proposal; inactive pending authorized human review"
|
|
478
|
+
: "Human-created preference proposal; inactive pending activation",
|
|
479
|
+
});
|
|
480
|
+
return recordFromRows(preference, null);
|
|
481
|
+
});
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (isStableKeyConflict(error)) {
|
|
484
|
+
throw new PreferenceRegistryStableKeyConflictError(
|
|
485
|
+
"A preference with this stable key already exists for the target scope",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
type PreferenceRegistryListInput = {
|
|
493
|
+
scope?: PreferenceRegistryScope | undefined;
|
|
494
|
+
status?: PreferenceRegistryStatus | undefined;
|
|
495
|
+
limit: number;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
async function listPreferenceRegistryScoped(db: Database, input: PreferenceRegistryListInput) {
|
|
499
|
+
const now = new Date();
|
|
500
|
+
const conditions: SQL[] = [];
|
|
501
|
+
if (input.scope) conditions.push(eq(schema.preferenceRegistryPreferences.scope, input.scope));
|
|
502
|
+
if (input.status === "active") {
|
|
503
|
+
conditions.push(
|
|
504
|
+
eq(schema.preferenceRegistryPreferences.status, "active"),
|
|
505
|
+
or(
|
|
506
|
+
isNull(schema.preferenceRegistryRevisions.expiresAt),
|
|
507
|
+
gt(schema.preferenceRegistryRevisions.expiresAt, now),
|
|
508
|
+
)!,
|
|
509
|
+
);
|
|
510
|
+
} else if (input.status === "expired") {
|
|
511
|
+
conditions.push(
|
|
512
|
+
eq(schema.preferenceRegistryPreferences.status, "active"),
|
|
513
|
+
lte(schema.preferenceRegistryRevisions.expiresAt, now),
|
|
514
|
+
);
|
|
515
|
+
} else if (input.status) {
|
|
516
|
+
conditions.push(eq(schema.preferenceRegistryPreferences.status, input.status));
|
|
517
|
+
}
|
|
518
|
+
const rows = await db
|
|
519
|
+
.select({
|
|
520
|
+
preference: schema.preferenceRegistryPreferences,
|
|
521
|
+
revision: schema.preferenceRegistryRevisions,
|
|
522
|
+
})
|
|
523
|
+
.from(schema.preferenceRegistryPreferences)
|
|
524
|
+
.leftJoin(
|
|
525
|
+
schema.preferenceRegistryRevisions,
|
|
526
|
+
and(
|
|
527
|
+
eq(
|
|
528
|
+
schema.preferenceRegistryRevisions.id,
|
|
529
|
+
schema.preferenceRegistryPreferences.activeRevisionId,
|
|
530
|
+
),
|
|
531
|
+
eq(
|
|
532
|
+
schema.preferenceRegistryRevisions.accountId,
|
|
533
|
+
schema.preferenceRegistryPreferences.accountId,
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
)
|
|
537
|
+
.where(conditions.length ? and(...conditions) : undefined)
|
|
538
|
+
.orderBy(
|
|
539
|
+
asc(schema.preferenceRegistryPreferences.stableKey),
|
|
540
|
+
asc(schema.preferenceRegistryPreferences.id),
|
|
541
|
+
)
|
|
542
|
+
.limit(input.limit);
|
|
543
|
+
const preferences = rows.map(({ preference, revision }) =>
|
|
544
|
+
recordFromRows(preference, revision, now),
|
|
545
|
+
);
|
|
546
|
+
return PreferenceRegistryListResponse.parse({ preferences });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function listPreferenceRegistry(
|
|
550
|
+
db: Database,
|
|
551
|
+
input: {
|
|
552
|
+
workspaceId: string;
|
|
553
|
+
subjectId: string;
|
|
554
|
+
} & PreferenceRegistryListInput,
|
|
555
|
+
) {
|
|
556
|
+
return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
|
|
557
|
+
return await listPreferenceRegistryScoped(scopedDb, input);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export async function listPreferenceRegistryForAttempt(
|
|
562
|
+
db: Database,
|
|
563
|
+
input: PreferenceRegistryAttemptClaims & PreferenceRegistryListInput,
|
|
564
|
+
) {
|
|
565
|
+
return await withPreferenceRegistryAttemptAuthority(db, input, async (scopedDb) => {
|
|
566
|
+
return await listPreferenceRegistryScoped(scopedDb, input);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function getPreferenceRegistryDetailScoped(db: Database, preferenceId: string) {
|
|
571
|
+
const [preference] = await db
|
|
572
|
+
.select()
|
|
573
|
+
.from(schema.preferenceRegistryPreferences)
|
|
574
|
+
.where(eq(schema.preferenceRegistryPreferences.id, preferenceId))
|
|
575
|
+
.limit(1);
|
|
576
|
+
if (!preference) throw new PreferenceRegistryNotFoundError("Preference was not found");
|
|
577
|
+
const [revisions, events] = await Promise.all([
|
|
578
|
+
db
|
|
579
|
+
.select()
|
|
580
|
+
.from(schema.preferenceRegistryRevisions)
|
|
581
|
+
.where(eq(schema.preferenceRegistryRevisions.preferenceId, preferenceId))
|
|
582
|
+
.orderBy(desc(schema.preferenceRegistryRevisions.revision)),
|
|
583
|
+
db
|
|
584
|
+
.select()
|
|
585
|
+
.from(schema.preferenceRegistryEvents)
|
|
586
|
+
.where(eq(schema.preferenceRegistryEvents.preferenceId, preferenceId))
|
|
587
|
+
.orderBy(desc(schema.preferenceRegistryEvents.version)),
|
|
588
|
+
]);
|
|
589
|
+
const active = preference.activeRevisionId
|
|
590
|
+
? (revisions.find((revision) => revision.id === preference.activeRevisionId) ?? null)
|
|
591
|
+
: null;
|
|
592
|
+
return PreferenceRegistryDetailResponse.parse({
|
|
593
|
+
preference: recordFromRows(preference, active),
|
|
594
|
+
revisions: revisions.map((revision) =>
|
|
595
|
+
revisionSummary(revision, preference.scope as PreferenceRegistryScope),
|
|
596
|
+
),
|
|
597
|
+
events: events.map(eventFromRow),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function getPreferenceRegistryDetail(
|
|
602
|
+
db: Database,
|
|
603
|
+
input: { workspaceId: string; subjectId: string; preferenceId: string },
|
|
604
|
+
) {
|
|
605
|
+
return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
|
|
606
|
+
return await getPreferenceRegistryDetailScoped(scopedDb, input.preferenceId);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function getPreferenceRegistryDetailForAttempt(
|
|
611
|
+
db: Database,
|
|
612
|
+
input: PreferenceRegistryAttemptClaims & { preferenceId: string },
|
|
613
|
+
) {
|
|
614
|
+
return await withPreferenceRegistryAttemptAuthority(db, input, async (scopedDb) => {
|
|
615
|
+
return await getPreferenceRegistryDetailScoped(scopedDb, input.preferenceId);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export async function activatePreferenceRegistryRevision(
|
|
620
|
+
db: Database,
|
|
621
|
+
input: {
|
|
622
|
+
accountId: string;
|
|
623
|
+
workspaceId: string;
|
|
624
|
+
actorSubjectId: string;
|
|
625
|
+
principalKind: string | undefined;
|
|
626
|
+
preferenceId: string;
|
|
627
|
+
revisionId: string;
|
|
628
|
+
expectedCurrentRevisionId: string | null;
|
|
629
|
+
expectedScopeVersion: number;
|
|
630
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
631
|
+
reason: string;
|
|
632
|
+
eventType?: "activated" | "corrected";
|
|
633
|
+
},
|
|
634
|
+
) {
|
|
635
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
636
|
+
const preference = await lockedPreference(scopedDb, input.preferenceId);
|
|
637
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
638
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
639
|
+
if (preference.activeRevisionId !== input.expectedCurrentRevisionId) {
|
|
640
|
+
throw new PreferenceRegistryConflictError(
|
|
641
|
+
"The active preference revision changed in another request",
|
|
642
|
+
preference.activeRevisionId,
|
|
643
|
+
preference.scopeVersion,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (preference.status === "superseded") {
|
|
647
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
648
|
+
"A superseded preference cannot be activated",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (preference.status === "rejected") {
|
|
652
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
653
|
+
"A rejected preference proposal cannot be activated",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const revision = await revisionForPreference(scopedDb, preference.id, input.revisionId);
|
|
657
|
+
if (revision.id === preference.activeRevisionId) {
|
|
658
|
+
throw new PreferenceRegistryInvalidOperationError("The requested revision is already active");
|
|
659
|
+
}
|
|
660
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
661
|
+
operation: input.eventType === "corrected" ? "correct" : "activate",
|
|
662
|
+
preferenceId: preference.id,
|
|
663
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
664
|
+
expectedRevisionId: input.expectedCurrentRevisionId,
|
|
665
|
+
revisionId: revision.id,
|
|
666
|
+
actorSubjectId: input.actorSubjectId,
|
|
667
|
+
reason: input.reason,
|
|
668
|
+
});
|
|
669
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
670
|
+
return {
|
|
671
|
+
preference: recordFromRows(updated, revision),
|
|
672
|
+
event: eventFromRow(event),
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export async function correctPreferenceRegistry(
|
|
678
|
+
db: Database,
|
|
679
|
+
input: {
|
|
680
|
+
accountId: string;
|
|
681
|
+
workspaceId: string;
|
|
682
|
+
actorSubjectId: string;
|
|
683
|
+
principalKind: string | undefined;
|
|
684
|
+
preferenceId: string;
|
|
685
|
+
expectedCurrentRevisionId: string;
|
|
686
|
+
expectedScopeVersion: number;
|
|
687
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
688
|
+
title: string;
|
|
689
|
+
description: string;
|
|
690
|
+
content: string;
|
|
691
|
+
precedenceRank: number;
|
|
692
|
+
conflictStrategy: PreferenceRegistryConflictStrategy;
|
|
693
|
+
conflictsWith: string[];
|
|
694
|
+
expiresAt: string | null;
|
|
695
|
+
reason: string;
|
|
696
|
+
},
|
|
697
|
+
) {
|
|
698
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
699
|
+
const preference = await lockedPreference(scopedDb, input.preferenceId);
|
|
700
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
701
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
702
|
+
if (preference.status === "superseded" || preference.status === "rejected") {
|
|
703
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
704
|
+
"A rejected or superseded preference cannot be corrected",
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (preference.activeRevisionId !== input.expectedCurrentRevisionId) {
|
|
708
|
+
throw new PreferenceRegistryConflictError(
|
|
709
|
+
"The active preference revision changed before correction",
|
|
710
|
+
preference.activeRevisionId,
|
|
711
|
+
preference.scopeVersion,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const oldRevision = await revisionForPreference(
|
|
715
|
+
scopedDb,
|
|
716
|
+
preference.id,
|
|
717
|
+
input.expectedCurrentRevisionId,
|
|
718
|
+
);
|
|
719
|
+
const revision = await insertRevision(scopedDb, {
|
|
720
|
+
...input,
|
|
721
|
+
scope: preference.scope as PreferenceRegistryScope,
|
|
722
|
+
provenanceSource: "human",
|
|
723
|
+
provenanceSourceId: `correction:${oldRevision.id}`,
|
|
724
|
+
correctsRevisionId: oldRevision.id,
|
|
725
|
+
});
|
|
726
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
727
|
+
operation: "correct",
|
|
728
|
+
preferenceId: preference.id,
|
|
729
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
730
|
+
expectedRevisionId: oldRevision.id,
|
|
731
|
+
revisionId: revision.id,
|
|
732
|
+
actorSubjectId: input.actorSubjectId,
|
|
733
|
+
reason: input.reason,
|
|
734
|
+
});
|
|
735
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
736
|
+
return { preference: recordFromRows(updated, revision), event: eventFromRow(event) };
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export async function changePreferenceRegistryScope(
|
|
741
|
+
db: Database,
|
|
742
|
+
input: {
|
|
743
|
+
accountId: string;
|
|
744
|
+
workspaceId: string;
|
|
745
|
+
actorSubjectId: string;
|
|
746
|
+
principalKind: string | undefined;
|
|
747
|
+
preferenceId: string;
|
|
748
|
+
scope: PreferenceRegistryScope;
|
|
749
|
+
expectedScopeVersion: number;
|
|
750
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
751
|
+
reason: string;
|
|
752
|
+
},
|
|
753
|
+
) {
|
|
754
|
+
try {
|
|
755
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
756
|
+
const preference = await lockedPreference(scopedDb, input.preferenceId);
|
|
757
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
758
|
+
input.authorizeScope(input.scope);
|
|
759
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
760
|
+
if (preference.status === "superseded" || preference.status === "rejected") {
|
|
761
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
762
|
+
"A rejected or superseded preference cannot change scope",
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (preference.scope === input.scope) {
|
|
766
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
767
|
+
"Preference already uses the requested scope",
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const oldTarget = targetFromRow(preference);
|
|
771
|
+
const nextTarget = targetFor(input.scope, input.workspaceId, input.actorSubjectId);
|
|
772
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
773
|
+
operation: "scope",
|
|
774
|
+
preferenceId: preference.id,
|
|
775
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
776
|
+
expectedRevisionId: preference.activeRevisionId,
|
|
777
|
+
newScope: input.scope,
|
|
778
|
+
actorSubjectId: input.actorSubjectId,
|
|
779
|
+
reason: input.reason,
|
|
780
|
+
});
|
|
781
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
782
|
+
const active = updated.activeRevisionId
|
|
783
|
+
? await revisionForPreference(scopedDb, updated.id, updated.activeRevisionId)
|
|
784
|
+
: null;
|
|
785
|
+
if (
|
|
786
|
+
event.oldScope !== oldTarget.scope ||
|
|
787
|
+
event.oldWorkspaceId !== oldTarget.workspaceId ||
|
|
788
|
+
event.oldSubjectId !== oldTarget.subjectId ||
|
|
789
|
+
event.newScope !== nextTarget.scope ||
|
|
790
|
+
event.newWorkspaceId !== nextTarget.workspaceId ||
|
|
791
|
+
event.newSubjectId !== nextTarget.subjectId
|
|
792
|
+
) {
|
|
793
|
+
throw new Error("Preference scope event did not preserve the locked transition");
|
|
794
|
+
}
|
|
795
|
+
return { preference: recordFromRows(updated, active), event: eventFromRow(event) };
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (isStableKeyConflict(error)) {
|
|
799
|
+
throw new PreferenceRegistryStableKeyConflictError(
|
|
800
|
+
"A preference with this stable key already exists for the target scope",
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export async function deactivatePreferenceRegistry(
|
|
808
|
+
db: Database,
|
|
809
|
+
input: {
|
|
810
|
+
accountId: string;
|
|
811
|
+
workspaceId: string;
|
|
812
|
+
actorSubjectId: string;
|
|
813
|
+
principalKind: string | undefined;
|
|
814
|
+
preferenceId: string;
|
|
815
|
+
expectedCurrentRevisionId: string;
|
|
816
|
+
expectedScopeVersion: number;
|
|
817
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
818
|
+
reason: string;
|
|
819
|
+
},
|
|
820
|
+
) {
|
|
821
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
822
|
+
const preference = await lockedPreference(scopedDb, input.preferenceId);
|
|
823
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
824
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
825
|
+
if (
|
|
826
|
+
preference.status !== "active" ||
|
|
827
|
+
preference.activeRevisionId !== input.expectedCurrentRevisionId
|
|
828
|
+
) {
|
|
829
|
+
throw new PreferenceRegistryConflictError(
|
|
830
|
+
"The active preference revision changed before deactivation",
|
|
831
|
+
preference.activeRevisionId,
|
|
832
|
+
preference.scopeVersion,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const revision = await revisionForPreference(
|
|
836
|
+
scopedDb,
|
|
837
|
+
preference.id,
|
|
838
|
+
input.expectedCurrentRevisionId,
|
|
839
|
+
);
|
|
840
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
841
|
+
operation: "deactivate",
|
|
842
|
+
preferenceId: preference.id,
|
|
843
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
844
|
+
expectedRevisionId: revision.id,
|
|
845
|
+
actorSubjectId: input.actorSubjectId,
|
|
846
|
+
reason: input.reason,
|
|
847
|
+
});
|
|
848
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
849
|
+
return { preference: recordFromRows(updated, null), event: eventFromRow(event) };
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export async function supersedePreferenceRegistry(
|
|
854
|
+
db: Database,
|
|
855
|
+
input: {
|
|
856
|
+
accountId: string;
|
|
857
|
+
workspaceId: string;
|
|
858
|
+
actorSubjectId: string;
|
|
859
|
+
principalKind: string | undefined;
|
|
860
|
+
preferenceId: string;
|
|
861
|
+
replacementPreferenceId: string;
|
|
862
|
+
expectedCurrentRevisionId: string;
|
|
863
|
+
expectedScopeVersion: number;
|
|
864
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
865
|
+
reason: string;
|
|
866
|
+
},
|
|
867
|
+
) {
|
|
868
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
869
|
+
if (input.preferenceId === input.replacementPreferenceId) {
|
|
870
|
+
throw new PreferenceRegistryInvalidOperationError("A preference cannot supersede itself");
|
|
871
|
+
}
|
|
872
|
+
const locked = await lockedPreferences(scopedDb, [
|
|
873
|
+
input.preferenceId,
|
|
874
|
+
input.replacementPreferenceId,
|
|
875
|
+
]);
|
|
876
|
+
const preference = locked.find((row) => row.id === input.preferenceId)!;
|
|
877
|
+
const replacement = locked.find((row) => row.id === input.replacementPreferenceId)!;
|
|
878
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
879
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
880
|
+
if (preference.activeRevisionId !== input.expectedCurrentRevisionId) {
|
|
881
|
+
throw new PreferenceRegistryConflictError(
|
|
882
|
+
"The active preference revision changed before supersession",
|
|
883
|
+
preference.activeRevisionId,
|
|
884
|
+
preference.scopeVersion,
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (preference.status !== "active") {
|
|
888
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
889
|
+
"Only an active preference can be superseded",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (replacement.status !== "active" || !replacement.activeRevisionId) {
|
|
893
|
+
throw new PreferenceRegistryInvalidOperationError("Replacement preference must be active");
|
|
894
|
+
}
|
|
895
|
+
const [replacementRevision] = await scopedDb
|
|
896
|
+
.select()
|
|
897
|
+
.from(schema.preferenceRegistryRevisions)
|
|
898
|
+
.where(
|
|
899
|
+
and(
|
|
900
|
+
eq(schema.preferenceRegistryRevisions.id, replacement.activeRevisionId),
|
|
901
|
+
eq(schema.preferenceRegistryRevisions.preferenceId, replacement.id),
|
|
902
|
+
or(
|
|
903
|
+
isNull(schema.preferenceRegistryRevisions.expiresAt),
|
|
904
|
+
gt(schema.preferenceRegistryRevisions.expiresAt, sql`transaction_timestamp()`),
|
|
905
|
+
),
|
|
906
|
+
),
|
|
907
|
+
)
|
|
908
|
+
.limit(1);
|
|
909
|
+
if (!replacementRevision) {
|
|
910
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
911
|
+
"Replacement preference must have an unexpired active revision",
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
if (preference.scope !== replacement.scope) {
|
|
915
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
916
|
+
"Supersession requires the same scope tier",
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
920
|
+
operation: "supersede",
|
|
921
|
+
preferenceId: preference.id,
|
|
922
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
923
|
+
expectedRevisionId: preference.activeRevisionId,
|
|
924
|
+
relatedPreferenceId: replacement.id,
|
|
925
|
+
actorSubjectId: input.actorSubjectId,
|
|
926
|
+
reason: input.reason,
|
|
927
|
+
});
|
|
928
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
929
|
+
const revision = await revisionForPreference(
|
|
930
|
+
scopedDb,
|
|
931
|
+
preference.id,
|
|
932
|
+
preference.activeRevisionId!,
|
|
933
|
+
);
|
|
934
|
+
return { preference: recordFromRows(updated, revision), event: eventFromRow(event) };
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export async function rejectPreferenceRegistryProposal(
|
|
939
|
+
db: Database,
|
|
940
|
+
input: {
|
|
941
|
+
accountId: string;
|
|
942
|
+
workspaceId: string;
|
|
943
|
+
actorSubjectId: string;
|
|
944
|
+
principalKind: string | undefined;
|
|
945
|
+
preferenceId: string;
|
|
946
|
+
revisionId: string;
|
|
947
|
+
expectedScopeVersion: number;
|
|
948
|
+
authorizeScope: PreferenceRegistryScopeAuthorizer;
|
|
949
|
+
reason: string;
|
|
950
|
+
},
|
|
951
|
+
) {
|
|
952
|
+
return await withPreferenceRegistryGovernanceRls(db, input, async (scopedDb) => {
|
|
953
|
+
const preference = await lockedPreference(scopedDb, input.preferenceId);
|
|
954
|
+
input.authorizeScope(preference.scope as PreferenceRegistryScope);
|
|
955
|
+
requireExpectedScopeVersion(preference, input.expectedScopeVersion);
|
|
956
|
+
if (preference.activeRevisionId !== null || preference.status !== "proposed") {
|
|
957
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
958
|
+
"Only an inactive proposal can be rejected",
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
await revisionForPreference(scopedDb, preference.id, input.revisionId);
|
|
962
|
+
const event = await lifecycleEvent(scopedDb, {
|
|
963
|
+
operation: "reject",
|
|
964
|
+
preferenceId: preference.id,
|
|
965
|
+
expectedScopeVersion: input.expectedScopeVersion,
|
|
966
|
+
expectedRevisionId: null,
|
|
967
|
+
revisionId: input.revisionId,
|
|
968
|
+
actorSubjectId: input.actorSubjectId,
|
|
969
|
+
reason: input.reason,
|
|
970
|
+
});
|
|
971
|
+
const updated = await preferenceAfterMutation(scopedDb, preference.id);
|
|
972
|
+
return { preference: recordFromRows(updated, null), event: eventFromRow(event) };
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function withPreferenceRegistryAttemptAuthority<T>(
|
|
977
|
+
db: Database,
|
|
978
|
+
input: PreferenceRegistryAttemptClaims,
|
|
979
|
+
fn: (db: Database, authority: PreferenceRegistryAttemptAuthority) => Promise<T>,
|
|
980
|
+
): Promise<T> {
|
|
981
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
|
|
982
|
+
const rows = (await scopedDb.execute(sql`
|
|
983
|
+
WITH locked_workspace AS MATERIALIZED (
|
|
984
|
+
SELECT workspace.id, workspace.account_id
|
|
985
|
+
FROM workspaces workspace
|
|
986
|
+
WHERE workspace.id = ${input.workspaceId}::uuid
|
|
987
|
+
AND workspace.account_id = ${input.accountId}::uuid
|
|
988
|
+
FOR KEY SHARE OF workspace
|
|
989
|
+
), locked_session AS MATERIALIZED (
|
|
990
|
+
SELECT session.id, session.account_id, session.workspace_id, session.active_turn_id
|
|
991
|
+
FROM sessions session
|
|
992
|
+
JOIN locked_workspace workspace
|
|
993
|
+
ON workspace.id = session.workspace_id
|
|
994
|
+
AND workspace.account_id = session.account_id
|
|
995
|
+
WHERE session.id = ${input.sessionId}::uuid
|
|
996
|
+
AND session.active_turn_id = ${input.turnId}::uuid
|
|
997
|
+
FOR SHARE OF session
|
|
998
|
+
), locked_turn AS MATERIALIZED (
|
|
999
|
+
SELECT turn.id, turn.account_id, turn.workspace_id, turn.session_id,
|
|
1000
|
+
turn.active_attempt_id, turn.execution_generation,
|
|
1001
|
+
turn.initiator_kind, turn.initiator_subject_id
|
|
1002
|
+
FROM session_turns turn
|
|
1003
|
+
JOIN locked_session session
|
|
1004
|
+
ON session.id = turn.session_id
|
|
1005
|
+
AND session.workspace_id = turn.workspace_id
|
|
1006
|
+
AND session.account_id = turn.account_id
|
|
1007
|
+
WHERE turn.id = ${input.turnId}::uuid
|
|
1008
|
+
AND turn.active_attempt_id = ${input.attemptId}::uuid
|
|
1009
|
+
AND turn.execution_generation = ${input.executionGeneration}
|
|
1010
|
+
AND turn.status IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
|
|
1011
|
+
AND turn.initiator_kind = 'subject'
|
|
1012
|
+
AND length(btrim(turn.initiator_subject_id)) BETWEEN 1 AND 1024
|
|
1013
|
+
FOR SHARE OF turn
|
|
1014
|
+
), locked_attempt AS MATERIALIZED (
|
|
1015
|
+
SELECT attempt.id, attempt.account_id, attempt.workspace_id,
|
|
1016
|
+
attempt.session_id, attempt.turn_id, attempt.execution_generation
|
|
1017
|
+
FROM session_turn_attempts attempt
|
|
1018
|
+
JOIN locked_turn turn
|
|
1019
|
+
ON turn.id = attempt.turn_id
|
|
1020
|
+
AND turn.session_id = attempt.session_id
|
|
1021
|
+
AND turn.workspace_id = attempt.workspace_id
|
|
1022
|
+
AND turn.account_id = attempt.account_id
|
|
1023
|
+
WHERE attempt.id = ${input.attemptId}::uuid
|
|
1024
|
+
AND attempt.execution_generation = ${input.executionGeneration}
|
|
1025
|
+
AND attempt.state IN ('claimed', 'running')
|
|
1026
|
+
AND NOT EXISTS (
|
|
1027
|
+
SELECT 1
|
|
1028
|
+
FROM session_attempt_interruptions interruption
|
|
1029
|
+
WHERE interruption.workspace_id = attempt.workspace_id
|
|
1030
|
+
AND interruption.attempt_id = attempt.id
|
|
1031
|
+
AND interruption.state IN ('pending', 'delivered', 'acknowledged')
|
|
1032
|
+
)
|
|
1033
|
+
FOR SHARE OF attempt
|
|
1034
|
+
)
|
|
1035
|
+
SELECT turn.initiator_subject_id
|
|
1036
|
+
FROM locked_workspace workspace
|
|
1037
|
+
JOIN locked_session session ON true
|
|
1038
|
+
JOIN locked_turn turn ON true
|
|
1039
|
+
JOIN locked_attempt attempt ON true
|
|
1040
|
+
WHERE workspace.account_id = attempt.account_id
|
|
1041
|
+
AND workspace.id = attempt.workspace_id
|
|
1042
|
+
AND session.id = attempt.session_id
|
|
1043
|
+
AND turn.id = attempt.turn_id
|
|
1044
|
+
`)) as unknown as Array<{ initiator_subject_id: string }>;
|
|
1045
|
+
const initiatingHumanSubjectId = rows[0]?.initiator_subject_id;
|
|
1046
|
+
if (!initiatingHumanSubjectId) {
|
|
1047
|
+
throw new PreferenceRegistryInitiatorError(
|
|
1048
|
+
"Preference retrieval requires the exact current attempt, generation, and immutable human initiator",
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const authority = { ...input, initiatingHumanSubjectId };
|
|
1052
|
+
await setSubjectRlsContext(scopedDb, initiatingHumanSubjectId);
|
|
1053
|
+
return await fn(scopedDb, authority);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export function boundPreferenceRegistryDescriptors(
|
|
1058
|
+
descriptors: readonly PreferenceRegistryDescriptor[],
|
|
1059
|
+
): { descriptors: PreferenceRegistryDescriptor[]; truncated: boolean } {
|
|
1060
|
+
const scopeOrder: Record<PreferenceRegistryScope, number> = {
|
|
1061
|
+
organization: 0,
|
|
1062
|
+
workspace: 1,
|
|
1063
|
+
user: 2,
|
|
1064
|
+
};
|
|
1065
|
+
const compareText = (left: string, right: string): number =>
|
|
1066
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
1067
|
+
const ordered = [...descriptors].sort(
|
|
1068
|
+
(left, right) =>
|
|
1069
|
+
scopeOrder[left.scope] - scopeOrder[right.scope] ||
|
|
1070
|
+
right.precedence.rank - left.precedence.rank ||
|
|
1071
|
+
compareText(left.stableKey, right.stableKey) ||
|
|
1072
|
+
compareText(left.id, right.id),
|
|
1073
|
+
);
|
|
1074
|
+
const selected: PreferenceRegistryDescriptor[] = [];
|
|
1075
|
+
for (const descriptor of ordered) {
|
|
1076
|
+
if (selected.length >= PREFERENCE_REGISTRY_DESCRIPTOR_MAX_COUNT) break;
|
|
1077
|
+
const candidate = [...selected, descriptor];
|
|
1078
|
+
if (postgresJsonbTextUtf8Bytes(candidate) > PREFERENCE_REGISTRY_DESCRIPTOR_MAX_UTF8_BYTES) {
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
selected.push(descriptor);
|
|
1082
|
+
}
|
|
1083
|
+
return { descriptors: selected, truncated: selected.length < ordered.length };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function postgresJsonbTextUtf8Bytes(value: unknown): number {
|
|
1087
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8") + postgresJsonbSpacingBytes(value);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function postgresJsonbSpacingBytes(value: unknown): number {
|
|
1091
|
+
if (Array.isArray(value)) {
|
|
1092
|
+
return (
|
|
1093
|
+
Math.max(0, value.length - 1) +
|
|
1094
|
+
value.reduce<number>((total, entry) => total + postgresJsonbSpacingBytes(entry), 0)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (value !== null && typeof value === "object") {
|
|
1098
|
+
const entries = Object.values(value);
|
|
1099
|
+
return (
|
|
1100
|
+
entries.length +
|
|
1101
|
+
Math.max(0, entries.length - 1) +
|
|
1102
|
+
entries.reduce<number>((total, entry) => total + postgresJsonbSpacingBytes(entry), 0)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return 0;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export async function getOrCreatePreferenceRegistrySnapshot(
|
|
1109
|
+
db: Database,
|
|
1110
|
+
claims: PreferenceRegistryAttemptClaims,
|
|
1111
|
+
) {
|
|
1112
|
+
try {
|
|
1113
|
+
return await withWorkspaceRls(db, claims.workspaceId, async (scopedDb) => {
|
|
1114
|
+
const rows = (await scopedDb.execute(sql`
|
|
1115
|
+
SELECT
|
|
1116
|
+
snapshot.id,
|
|
1117
|
+
snapshot.workspace_id AS "workspaceId",
|
|
1118
|
+
snapshot.session_id AS "sessionId",
|
|
1119
|
+
snapshot.turn_id AS "turnId",
|
|
1120
|
+
snapshot.attempt_id AS "attemptId",
|
|
1121
|
+
snapshot.execution_generation AS "executionGeneration",
|
|
1122
|
+
snapshot.initiating_human_subject_id AS "initiatingHumanSubjectId",
|
|
1123
|
+
snapshot.descriptor_hash AS "descriptorHash",
|
|
1124
|
+
snapshot.descriptors,
|
|
1125
|
+
snapshot.truncated,
|
|
1126
|
+
snapshot.created_at AS "createdAt"
|
|
1127
|
+
FROM preference_registry_get_or_create_snapshot(
|
|
1128
|
+
${claims.accountId}::uuid,
|
|
1129
|
+
${claims.workspaceId}::uuid,
|
|
1130
|
+
${claims.sessionId}::uuid,
|
|
1131
|
+
${claims.turnId}::uuid,
|
|
1132
|
+
${claims.attemptId}::uuid,
|
|
1133
|
+
${claims.executionGeneration}
|
|
1134
|
+
) snapshot
|
|
1135
|
+
`)) as unknown as Array<{
|
|
1136
|
+
id: string;
|
|
1137
|
+
workspaceId: string;
|
|
1138
|
+
sessionId: string;
|
|
1139
|
+
turnId: string;
|
|
1140
|
+
attemptId: string;
|
|
1141
|
+
executionGeneration: number;
|
|
1142
|
+
initiatingHumanSubjectId: string;
|
|
1143
|
+
descriptorHash: string;
|
|
1144
|
+
descriptors: unknown;
|
|
1145
|
+
truncated: boolean;
|
|
1146
|
+
createdAt: Date | string;
|
|
1147
|
+
}>;
|
|
1148
|
+
const snapshot = rows[0];
|
|
1149
|
+
if (!snapshot) {
|
|
1150
|
+
throw new PreferenceRegistryInitiatorError(
|
|
1151
|
+
"Preference snapshot authority conflicts with the accepted human attempt",
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
return PreferenceRegistrySnapshot.parse({
|
|
1155
|
+
...snapshot,
|
|
1156
|
+
createdAt: iso(snapshot.createdAt),
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (["40001", "42501"].includes(nestedPostgresSqlState(error) ?? "")) {
|
|
1161
|
+
throw new PreferenceRegistryInitiatorError(
|
|
1162
|
+
"Preference snapshot requires the exact current attempt, generation, and immutable human initiator",
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export async function getPreferenceRegistryFullContent(
|
|
1170
|
+
db: Database,
|
|
1171
|
+
claims: PreferenceRegistryAttemptClaims,
|
|
1172
|
+
handle: string,
|
|
1173
|
+
) {
|
|
1174
|
+
return await withPreferenceRegistryAttemptAuthority(db, claims, async (scopedDb, authority) => {
|
|
1175
|
+
const [snapshot] = await scopedDb
|
|
1176
|
+
.select()
|
|
1177
|
+
.from(schema.preferenceRegistrySnapshots)
|
|
1178
|
+
.where(
|
|
1179
|
+
and(
|
|
1180
|
+
eq(schema.preferenceRegistrySnapshots.accountId, authority.accountId),
|
|
1181
|
+
eq(schema.preferenceRegistrySnapshots.workspaceId, authority.workspaceId),
|
|
1182
|
+
eq(schema.preferenceRegistrySnapshots.sessionId, authority.sessionId),
|
|
1183
|
+
eq(schema.preferenceRegistrySnapshots.turnId, authority.turnId),
|
|
1184
|
+
eq(schema.preferenceRegistrySnapshots.attemptId, authority.attemptId),
|
|
1185
|
+
eq(schema.preferenceRegistrySnapshots.executionGeneration, authority.executionGeneration),
|
|
1186
|
+
eq(
|
|
1187
|
+
schema.preferenceRegistrySnapshots.initiatingHumanSubjectId,
|
|
1188
|
+
authority.initiatingHumanSubjectId,
|
|
1189
|
+
),
|
|
1190
|
+
),
|
|
1191
|
+
)
|
|
1192
|
+
.limit(1);
|
|
1193
|
+
if (!snapshot) {
|
|
1194
|
+
throw new PreferenceRegistryInvalidOperationError(
|
|
1195
|
+
"Preference summary must be snapshotted before full-content retrieval",
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
const descriptor = snapshot.descriptors.find(
|
|
1199
|
+
(candidate) => candidate.retrievalHandle === handle,
|
|
1200
|
+
);
|
|
1201
|
+
if (!descriptor)
|
|
1202
|
+
throw new PreferenceRegistryNotFoundError(
|
|
1203
|
+
"Preference retrieval handle is not in this attempt snapshot",
|
|
1204
|
+
);
|
|
1205
|
+
const [revision] = await scopedDb
|
|
1206
|
+
.select()
|
|
1207
|
+
.from(schema.preferenceRegistryRevisions)
|
|
1208
|
+
.where(
|
|
1209
|
+
and(
|
|
1210
|
+
eq(schema.preferenceRegistryRevisions.accountId, authority.accountId),
|
|
1211
|
+
eq(schema.preferenceRegistryRevisions.id, descriptor.revisionId),
|
|
1212
|
+
eq(schema.preferenceRegistryRevisions.preferenceId, descriptor.id),
|
|
1213
|
+
eq(schema.preferenceRegistryRevisions.contentHash, descriptor.contentHash),
|
|
1214
|
+
),
|
|
1215
|
+
)
|
|
1216
|
+
.limit(1);
|
|
1217
|
+
if (!revision) throw new PreferenceRegistryNotFoundError("Preference revision was not found");
|
|
1218
|
+
return PreferenceRegistryFullContent.parse({ descriptor, content: revision.content });
|
|
1219
|
+
});
|
|
1220
|
+
}
|