@tearleads/client-sdk 0.1.112 → 0.1.113

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 (34) hide show
  1. package/data/persistence/keyingCheckpoints/keyingCheckpointAdvancePersistence.js +16 -11
  2. package/data/persistence/keyingCheckpoints/keyingCheckpointAdvancePersistence.js.map +1 -1
  3. package/data/persistence/keyingCheckpoints/keyingCheckpointPersistence.js +150 -2
  4. package/data/persistence/keyingCheckpoints/keyingCheckpointPersistence.js.map +1 -1
  5. package/data/persistence/keyingCheckpoints/principalAuthorizationCheckpoints.js +3 -2
  6. package/data/persistence/keyingCheckpoints/principalAuthorizationCheckpoints.js.map +1 -1
  7. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.d.ts +14 -1
  8. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.js +128 -34
  9. package/data/persistence/principalHistory/principalHistoryEvidencePersistence.js.map +1 -1
  10. package/data/persistence/principalHistory/principalHistoryNodeRetention.js +64 -24
  11. package/data/persistence/principalHistory/principalHistoryNodeRetention.js.map +1 -1
  12. package/data/persistence/principalHistory/principalHistoryPrefixPersistence.js +6 -1
  13. package/data/persistence/principalHistory/principalHistoryPrefixPersistence.js.map +1 -1
  14. package/data/persistence/principalHistory/principalHistoryRootOwnership.d.ts +45 -0
  15. package/data/persistence/principalHistory/principalHistoryRootOwnership.js +41 -4
  16. package/data/persistence/principalHistory/principalHistoryRootOwnership.js.map +1 -1
  17. package/data/persistence/principalHistory/principalHistoryStagePersistence.js +12 -8
  18. package/data/persistence/principalHistory/principalHistoryStagePersistence.js.map +1 -1
  19. package/data/persistence/principalKeys/principalGrantRetirementPersistence.js +35 -1
  20. package/data/persistence/principalKeys/principalGrantRetirementPersistence.js.map +1 -1
  21. package/data/persistence/principalPolicy/principalPolicyOrganizationPersistence.js +62 -1
  22. package/data/persistence/principalPolicy/principalPolicyOrganizationPersistence.js.map +1 -1
  23. package/data/persistence/trustedUserIdentityPinPersistence.js +46 -23
  24. package/data/persistence/trustedUserIdentityPinPersistence.js.map +1 -1
  25. package/data/persistence/verifiedManifestBundlePersistence.js +27 -10
  26. package/data/persistence/verifiedManifestBundlePersistence.js.map +1 -1
  27. package/data/sqlite/schema/principalHistoryNodeRetentionSchema.d.ts +222 -0
  28. package/package.json +1 -1
  29. package/workflows/principals/historyRecovery/loadRecoveredPrincipalHistoryPage.js +13 -12
  30. package/workflows/principals/historyRecovery/loadRecoveredPrincipalHistoryPage.js.map +1 -1
  31. package/workflows/principals/historyRecovery/principalHistoryRecoveryReferences.js +17 -19
  32. package/workflows/principals/historyRecovery/principalHistoryRecoveryReferences.js.map +1 -1
  33. package/workflows/principals/publicHistory/selectPublicPrincipalHistory.js +8 -10
  34. package/workflows/principals/publicHistory/selectPublicPrincipalHistory.js.map +1 -1
@@ -31,8 +31,8 @@ export async function decrementPrincipalHistoryNodeReference(tx, scope, hash) {
31
31
  .where(and(eq(principalHistoryNodeReferences.scopeId, scope.scopeId), eq(principalHistoryNodeReferences.organizationId, scope.organizationId), eq(principalHistoryNodeReferences.hash, hash)))
32
32
  .run();
33
33
  }
34
- /** Caller commits ownership together with the exact saved progress it describes. */
35
- export async function retainPrincipalHistoryRoot(tx, input) {
34
+ /** Null when the owner already holds this exact root, which changes nothing. */
35
+ export async function loadPrincipalHistoryRootRetention(tx, input) {
36
36
  const [previous] = await tx
37
37
  .select()
38
38
  .from(principalHistoryRootOwners)
@@ -41,8 +41,41 @@ export async function retainPrincipalHistoryRoot(tx, input) {
41
41
  if (previous?.scopeId === input.scopeId &&
42
42
  previous.organizationId === input.organizationId &&
43
43
  previous.rootHash === input.rootHash)
44
+ return null;
45
+ return { input: { ...input }, previous };
46
+ }
47
+ function sameScope(left, right) {
48
+ return (left.scopeId === right.scopeId &&
49
+ left.organizationId === right.organizationId);
50
+ }
51
+ /**
52
+ * The edges a write of `page`'s nodes can apply for this retention without
53
+ * reordering them: the increment only when the owner is in the page's scope,
54
+ * and the previous root's decrement only after it and in the same scope. A
55
+ * page without nodes writes no liveness rows, so nothing folds into it.
56
+ */
57
+ export function foldablePrincipalHistoryRootEdges(retention, page) {
58
+ if (!retention ||
59
+ page.nodes.length === 0 ||
60
+ !sameScope(retention.input, page))
61
+ return undefined;
62
+ const { previous } = retention;
63
+ return {
64
+ increment: retention.input.rootHash,
65
+ decrement: previous && sameScope(previous, page) ? previous.rootHash : null,
66
+ };
67
+ }
68
+ /**
69
+ * Records the owner and applies the root edges `folded` did not already
70
+ * write. Caller commits ownership together with the exact saved progress it
71
+ * describes.
72
+ */
73
+ export async function writePrincipalHistoryRootRetention(tx, retention, folded) {
74
+ if (!retention)
44
75
  return;
45
- await incrementPrincipalHistoryNodeReference(tx, input, input.rootHash);
76
+ const { input, previous } = retention;
77
+ if (!folded)
78
+ await incrementPrincipalHistoryNodeReference(tx, input, input.rootHash);
46
79
  await tx
47
80
  .insert(principalHistoryRootOwners)
48
81
  .values(input)
@@ -51,9 +84,13 @@ export async function retainPrincipalHistoryRoot(tx, input) {
51
84
  set: input,
52
85
  })
53
86
  .run();
54
- if (previous)
87
+ if (previous && !folded?.decrement)
55
88
  await decrementPrincipalHistoryNodeReference(tx, previous, previous.rootHash);
56
89
  }
90
+ /** Caller commits ownership together with the exact saved progress it describes. */
91
+ export async function retainPrincipalHistoryRoot(tx, input) {
92
+ await writePrincipalHistoryRootRetention(tx, await loadPrincipalHistoryRootRetention(tx, input));
93
+ }
57
94
  export async function releasePrincipalHistoryRoot(tx, id) {
58
95
  const [previous] = await tx
59
96
  .select()
@@ -1 +1 @@
1
- {"version":3,"file":"principalHistoryRootOwnership.js","sourceRoot":"","sources":["principalHistoryRootOwnership.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport {\n principalHistoryNodeReferences,\n principalHistoryRootOwners,\n} from \"../../sqlite/schema/principalHistoryNodeRetentionSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface PrincipalHistoryNodeScope {\n /** Evidence scope hashes include organizationId; one scope belongs to one organization. */\n readonly scopeId: string;\n readonly organizationId: string;\n}\n\nexport async function incrementPrincipalHistoryNodeReference(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hash: string,\n) {\n await tx\n .insert(principalHistoryNodeReferences)\n .values({\n ...scope,\n hash,\n referenceCount: 1,\n managed: false,\n leftHash: null,\n rightHash: null,\n })\n .onConflictDoUpdate({\n target: [\n principalHistoryNodeReferences.scopeId,\n principalHistoryNodeReferences.hash,\n ],\n set: {\n referenceCount: sql`${principalHistoryNodeReferences.referenceCount} + 1`,\n },\n })\n .run();\n}\n\nexport async function decrementPrincipalHistoryNodeReference(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hash: string,\n) {\n await tx\n .update(principalHistoryNodeReferences)\n .set({\n referenceCount: sql`max(${principalHistoryNodeReferences.referenceCount} - 1, 0)`,\n })\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(principalHistoryNodeReferences.organizationId, scope.organizationId),\n eq(principalHistoryNodeReferences.hash, hash),\n ),\n )\n .run();\n}\n\n/** Caller commits ownership together with the exact saved progress it describes. */\nexport async function retainPrincipalHistoryRoot(\n tx: ClientSQLiteTransactionScope,\n input: PrincipalHistoryNodeScope & {\n readonly id: string;\n readonly rootHash: string;\n },\n) {\n const [previous] = await tx\n .select()\n .from(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, input.id))\n .limit(1);\n if (\n previous?.scopeId === input.scopeId &&\n previous.organizationId === input.organizationId &&\n previous.rootHash === input.rootHash\n )\n return;\n await incrementPrincipalHistoryNodeReference(tx, input, input.rootHash);\n await tx\n .insert(principalHistoryRootOwners)\n .values(input)\n .onConflictDoUpdate({\n target: principalHistoryRootOwners.id,\n set: input,\n })\n .run();\n if (previous)\n await decrementPrincipalHistoryNodeReference(\n tx,\n previous,\n previous.rootHash,\n );\n}\n\nexport async function releasePrincipalHistoryRoot(\n tx: ClientSQLiteTransactionScope,\n id: string,\n) {\n const [previous] = await tx\n .select()\n .from(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, id))\n .limit(1);\n if (!previous) return;\n await tx\n .delete(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, id))\n .run();\n await decrementPrincipalHistoryNodeReference(tx, previous, previous.rootHash);\n}\n"]}
1
+ {"version":3,"file":"principalHistoryRootOwnership.js","sourceRoot":"","sources":["principalHistoryRootOwnership.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, sql } from \"drizzle-orm\";\nimport {\n principalHistoryNodeReferences,\n principalHistoryRootOwners,\n} from \"../../sqlite/schema/principalHistoryNodeRetentionSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface PrincipalHistoryNodeScope {\n /** Evidence scope hashes include organizationId; one scope belongs to one organization. */\n readonly scopeId: string;\n readonly organizationId: string;\n}\n\nexport async function incrementPrincipalHistoryNodeReference(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hash: string,\n) {\n await tx\n .insert(principalHistoryNodeReferences)\n .values({\n ...scope,\n hash,\n referenceCount: 1,\n managed: false,\n leftHash: null,\n rightHash: null,\n })\n .onConflictDoUpdate({\n target: [\n principalHistoryNodeReferences.scopeId,\n principalHistoryNodeReferences.hash,\n ],\n set: {\n referenceCount: sql`${principalHistoryNodeReferences.referenceCount} + 1`,\n },\n })\n .run();\n}\n\nexport async function decrementPrincipalHistoryNodeReference(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hash: string,\n) {\n await tx\n .update(principalHistoryNodeReferences)\n .set({\n referenceCount: sql`max(${principalHistoryNodeReferences.referenceCount} - 1, 0)`,\n })\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(principalHistoryNodeReferences.organizationId, scope.organizationId),\n eq(principalHistoryNodeReferences.hash, hash),\n ),\n )\n .run();\n}\n\ntype PrincipalHistoryRootOwner = typeof principalHistoryRootOwners.$inferSelect;\n\ntype PrincipalHistoryRootInput = PrincipalHistoryNodeScope & {\n readonly id: string;\n readonly rootHash: string;\n};\n\n/** An owner whose root changes: the new root gains an edge, the old one loses it. */\nexport interface PrincipalHistoryRootRetention {\n readonly input: PrincipalHistoryRootInput;\n readonly previous: PrincipalHistoryRootOwner | undefined;\n}\n\n/** The root edges a node write replays itself, after its nodes and in this order. */\nexport interface PrincipalHistoryRootEdges {\n readonly increment: string;\n readonly decrement: string | null;\n}\n\n/** Null when the owner already holds this exact root, which changes nothing. */\nexport async function loadPrincipalHistoryRootRetention(\n tx: ClientSQLiteTransactionScope,\n input: PrincipalHistoryRootInput,\n): Promise<PrincipalHistoryRootRetention | null> {\n const [previous] = await tx\n .select()\n .from(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, input.id))\n .limit(1);\n if (\n previous?.scopeId === input.scopeId &&\n previous.organizationId === input.organizationId &&\n previous.rootHash === input.rootHash\n )\n return null;\n return { input: { ...input }, previous };\n}\n\nfunction sameScope(\n left: PrincipalHistoryNodeScope,\n right: PrincipalHistoryNodeScope,\n): boolean {\n return (\n left.scopeId === right.scopeId &&\n left.organizationId === right.organizationId\n );\n}\n\n/**\n * The edges a write of `page`'s nodes can apply for this retention without\n * reordering them: the increment only when the owner is in the page's scope,\n * and the previous root's decrement only after it and in the same scope. A\n * page without nodes writes no liveness rows, so nothing folds into it.\n */\nexport function foldablePrincipalHistoryRootEdges(\n retention: PrincipalHistoryRootRetention | null,\n page: PrincipalHistoryNodeScope & { readonly nodes: readonly unknown[] },\n): PrincipalHistoryRootEdges | undefined {\n if (\n !retention ||\n page.nodes.length === 0 ||\n !sameScope(retention.input, page)\n )\n return undefined;\n const { previous } = retention;\n return {\n increment: retention.input.rootHash,\n decrement: previous && sameScope(previous, page) ? previous.rootHash : null,\n };\n}\n\n/**\n * Records the owner and applies the root edges `folded` did not already\n * write. Caller commits ownership together with the exact saved progress it\n * describes.\n */\nexport async function writePrincipalHistoryRootRetention(\n tx: ClientSQLiteTransactionScope,\n retention: PrincipalHistoryRootRetention | null,\n folded?: PrincipalHistoryRootEdges | undefined,\n) {\n if (!retention) return;\n const { input, previous } = retention;\n if (!folded)\n await incrementPrincipalHistoryNodeReference(tx, input, input.rootHash);\n await tx\n .insert(principalHistoryRootOwners)\n .values(input)\n .onConflictDoUpdate({\n target: principalHistoryRootOwners.id,\n set: input,\n })\n .run();\n if (previous && !folded?.decrement)\n await decrementPrincipalHistoryNodeReference(\n tx,\n previous,\n previous.rootHash,\n );\n}\n\n/** Caller commits ownership together with the exact saved progress it describes. */\nexport async function retainPrincipalHistoryRoot(\n tx: ClientSQLiteTransactionScope,\n input: PrincipalHistoryRootInput,\n) {\n await writePrincipalHistoryRootRetention(\n tx,\n await loadPrincipalHistoryRootRetention(tx, input),\n );\n}\n\nexport async function releasePrincipalHistoryRoot(\n tx: ClientSQLiteTransactionScope,\n id: string,\n) {\n const [previous] = await tx\n .select()\n .from(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, id))\n .limit(1);\n if (!previous) return;\n await tx\n .delete(principalHistoryRootOwners)\n .where(eq(principalHistoryRootOwners.id, id))\n .run();\n await decrementPrincipalHistoryNodeReference(tx, previous, previous.rootHash);\n}\n"]}
@@ -8,7 +8,7 @@ import { ensureSqlTables } from "../../sqlite/sqlSchema.js";
8
8
  import { writePrincipalHistoryEvidencePage, } from "./principalHistoryEvidencePersistence.js";
9
9
  import { reclaimIncompletePrincipalHistoryStages } from "./principalHistoryIncompleteRetention.js";
10
10
  import { reclaimPrincipalHistoryNodes } from "./principalHistoryNodeRetention.js";
11
- import { releasePrincipalHistoryRoot, retainPrincipalHistoryRoot, } from "./principalHistoryRootOwnership.js";
11
+ import { foldablePrincipalHistoryRootEdges, loadPrincipalHistoryRootRetention, releasePrincipalHistoryRoot, writePrincipalHistoryRootRetention, } from "./principalHistoryRootOwnership.js";
12
12
  import { reclaimUnpublishedPrincipalHistoryStages } from "./principalHistoryUnpublishedRetention.js";
13
13
  const principalHistoryStageColumns = {
14
14
  id: principalHistoryStages.id,
@@ -56,19 +56,23 @@ export async function savePrincipalHistoryStage(input) {
56
56
  .limit(1);
57
57
  if ((previous?.progress ?? null) !== input.previousProgress)
58
58
  throw new PrincipalHistoryStageChangedError();
59
- await writePrincipalHistoryEvidencePage(tx, evidence);
59
+ // The node write replays the root's edges after the page's own, as
60
+ // separate statements after it would.
61
+ const retention = await loadPrincipalHistoryRootRetention(tx, {
62
+ id: `stage:${stage.id}`,
63
+ scopeId: evidence.scopeId,
64
+ organizationId: stage.organizationId,
65
+ rootHash: evidence.indexRootHash,
66
+ });
67
+ const rootEdges = foldablePrincipalHistoryRootEdges(retention, evidence);
68
+ await writePrincipalHistoryEvidencePage(tx, evidence, rootEdges);
60
69
  const row = { ...stage, touchedAt: Date.now() };
61
70
  await tx
62
71
  .insert(principalHistoryStages)
63
72
  .values(row)
64
73
  .onConflictDoUpdate({ target: principalHistoryStages.id, set: row })
65
74
  .run();
66
- await retainPrincipalHistoryRoot(tx, {
67
- id: `stage:${stage.id}`,
68
- scopeId: evidence.scopeId,
69
- organizationId: stage.organizationId,
70
- rootHash: evidence.indexRootHash,
71
- });
75
+ await writePrincipalHistoryRootRetention(tx, retention, rootEdges);
72
76
  await reclaimIncompletePrincipalHistoryStages(tx, stage);
73
77
  await reclaimUnpublishedPrincipalHistoryStages(tx, stage);
74
78
  await reclaimPrincipalHistoryNodes(tx, evidence);
@@ -1 +1 @@
1
- {"version":3,"file":"principalHistoryStagePersistence.js","sourceRoot":"","sources":["principalHistoryStagePersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,mCAAmC,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAAE,8BAA8B,EAAE,MAAM,oDAAoD,CAAC;AACpG,OAAO,EAAE,mCAAmC,EAAE,MAAM,yDAAyD,CAAC;AAC9G,OAAO,EACL,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,iCAAiC,EAAE,MAAM,uCAAuC,CAAC;AAC1F,OAAO,EAAgB,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAEL,iCAAiC,GAClC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,uCAAuC,EAAE,MAAM,uCAAuC,CAAC;AAChG,OAAO,EAAE,4BAA4B,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,wCAAwC,EAAE,MAAM,wCAAwC,CAAC;AAQlG,MAAM,4BAA4B,GAAG;IACnC,EAAE,EAAE,sBAAsB,CAAC,EAAE;IAC7B,cAAc,EAAE,sBAAsB,CAAC,cAAc;IACrD,WAAW,EAAE,sBAAsB,CAAC,WAAW;IAC/C,YAAY,EAAE,sBAAsB,CAAC,YAAY;IACjD,QAAQ,EAAE,sBAAsB,CAAC,QAAQ;IACzC,QAAQ,EAAE,sBAAsB,CAAC,QAAQ;CAC1C,CAAC;AAEF,MAAM,iCAAkC,SAAQ,KAAK;IAC1C,IAAI,GAAG,iCAAiC,CAAC;IAClD;QACE,KAAK,CACH,sFAAsF,CACvF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,mCAAmC,CAAC;IAClD,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,UAAU,mCAAmC,CAAC,KAAc;IAChE,OAAO,KAAK,YAAY,iCAAiC,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,OAAgB,EAChB,EAAU;IAEV,MAAM,eAAe,CAAC,OAAO,EAAE,2BAA2B,CAAC,CAAC;IAC5D,MAAM,EAAE,EAAE,EAAE,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,CAAC,4BAA4B,CAAC;SACpC,IAAI,CAAC,sBAAsB,CAAC;SAC5B,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;SACxC,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,OAAO,GAAG,IAAI,IAAI,CAAC;AACrB,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,KAM/C;IACC,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC5D,MAAM,eAAe,CAAC,KAAK,CAAC,OAAO,EAAE;QACnC,GAAG,2BAA2B;QAC9B,GAAG,8BAA8B;KAClC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,iCAAiC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,kBAAkB,CAC5C,KAAK,EAAE,EAAE,EAAE,EAAE;QACX,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE;aACxB,MAAM,CAAC,EAAE,QAAQ,EAAE,sBAAsB,CAAC,QAAQ,EAAE,CAAC;aACrD,IAAI,CAAC,sBAAsB,CAAC;aAC5B,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAC;QACZ,IAAI,CAAC,QAAQ,EAAE,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,gBAAgB;YACzD,MAAM,IAAI,iCAAiC,EAAE,CAAC;QAChD,MAAM,iCAAiC,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACtD,MAAM,GAAG,GAAG,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAChD,MAAM,EAAE;aACL,MAAM,CAAC,sBAAsB,CAAC;aAC9B,MAAM,CAAC,GAAG,CAAC;aACX,kBAAkB,CAAC,EAAE,MAAM,EAAE,sBAAsB,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;aACnE,GAAG,EAAE,CAAC;QACT,MAAM,0BAA0B,CAAC,EAAE,EAAE;YACnC,EAAE,EAAE,SAAS,KAAK,CAAC,EAAE,EAAE;YACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,QAAQ,EAAE,QAAQ,CAAC,aAAa;SACjC,CAAC,CAAC;QACH,MAAM,uCAAuC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACzD,MAAM,wCAAwC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,4BAA4B,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC,EACD,KAAK,CAAC,YAAY,EAClB,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;IACF,mCAAmC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAgB,EAChB,KAA4B,EAC5B,YAA2B;IAE3B,MAAM,eAAe,CAAC,OAAO,EAAE;QAC7B,GAAG,2BAA2B;QAC9B,GAAG,mCAAmC;KACvC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC;IAC3D,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,kBAAkB,CAChD,KAAK,EAAE,EAAE,EAAE,EAAE;QACX,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE;aACvB,MAAM,CAAC,EAAE,QAAQ,EAAE,sBAAsB,CAAC,QAAQ,EAAE,CAAC;aACrD,IAAI,CAAC,sBAAsB,CAAC;aAC5B,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAC;QACZ,IAAI,OAAO,EAAE,QAAQ,KAAK,KAAK,CAAC,QAAQ;YAAE,OAAO;QACjD,MAAM,2BAA2B,CAAC,EAAE,EAAE,SAAS,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,EAAE;aACL,MAAM,CAAC,sBAAsB,CAAC;aAC9B,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,EACvC,EAAE,CAAC,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CACpD,CACF;aACA,GAAG,EAAE,CAAC;IACX,CAAC,EACD,YAAY,EACZ,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;IACF,mCAAmC,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;AACjE,CAAC","sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport { assertProjectionVerificationCurrent } from \"../../keyingProjectionVerification/types\";\nimport { principalHistoryEvidenceTables } from \"../../sqlite/schema/principalHistoryEvidenceSchema\";\nimport { principalHistoryNodeRetentionTables } from \"../../sqlite/schema/principalHistoryNodeRetentionSchema\";\nimport {\n principalHistoryStages,\n principalHistoryStageTables,\n} from \"../../sqlite/schema/principalHistoryStageSchema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n type PrincipalHistoryEvidencePage,\n writePrincipalHistoryEvidencePage,\n} from \"./principalHistoryEvidencePersistence\";\nimport { reclaimIncompletePrincipalHistoryStages } from \"./principalHistoryIncompleteRetention\";\nimport { reclaimPrincipalHistoryNodes } from \"./principalHistoryNodeRetention\";\nimport {\n releasePrincipalHistoryRoot,\n retainPrincipalHistoryRoot,\n} from \"./principalHistoryRootOwnership\";\nimport { reclaimUnpublishedPrincipalHistoryStages } from \"./principalHistoryUnpublishedRetention\";\n\n/** A stage's verification work; its retention hints are written by persistence. */\nexport type PrincipalHistoryStage = Omit<\n typeof principalHistoryStages.$inferSelect,\n \"scopeId\" | \"touchedAt\"\n>;\n\nconst principalHistoryStageColumns = {\n id: principalHistoryStages.id,\n organizationId: principalHistoryStages.organizationId,\n currentJson: principalHistoryStages.currentJson,\n afterVersion: principalHistoryStages.afterVersion,\n complete: principalHistoryStages.complete,\n progress: principalHistoryStages.progress,\n};\n\nclass PrincipalHistoryStageChangedError extends Error {\n readonly code = \"principal_history_stage_changed\";\n constructor() {\n super(\n \"Another operation changed the saved principal history; resume from its current state\",\n );\n this.name = \"PrincipalHistoryStageChangedError\";\n }\n}\n\n/** Another writer replaced this stage's progress between load and save. */\nexport function isPrincipalHistoryStageChangedError(error: unknown): boolean {\n return error instanceof PrincipalHistoryStageChangedError;\n}\n\nexport async function loadPrincipalHistoryStage(\n execSql: ExecSql,\n id: string,\n): Promise<PrincipalHistoryStage | null> {\n await ensureSqlTables(execSql, principalHistoryStageTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [row] = await db\n .select(principalHistoryStageColumns)\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, id))\n .limit(1);\n return row ?? null;\n}\n\n/** Publish accepted entries and index nodes atomically with authenticated progress. */\nexport async function savePrincipalHistoryStage(input: {\n readonly execSql: ExecSql;\n readonly stage: PrincipalHistoryStage;\n readonly evidence: PrincipalHistoryEvidencePage;\n readonly previousProgress: string | null;\n readonly stillCurrent: () => boolean;\n}): Promise<void> {\n const evidence = structuredClone(input.evidence);\n const stage = { ...input.stage, scopeId: evidence.scopeId };\n await ensureSqlTables(input.execSql, [\n ...principalHistoryStageTables,\n ...principalHistoryEvidenceTables,\n ]);\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n const saved = await runtime.guardedTransaction(\n async (tx) => {\n const [previous] = await tx\n .select({ progress: principalHistoryStages.progress })\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, stage.id))\n .limit(1);\n if ((previous?.progress ?? null) !== input.previousProgress)\n throw new PrincipalHistoryStageChangedError();\n await writePrincipalHistoryEvidencePage(tx, evidence);\n const row = { ...stage, touchedAt: Date.now() };\n await tx\n .insert(principalHistoryStages)\n .values(row)\n .onConflictDoUpdate({ target: principalHistoryStages.id, set: row })\n .run();\n await retainPrincipalHistoryRoot(tx, {\n id: `stage:${stage.id}`,\n scopeId: evidence.scopeId,\n organizationId: stage.organizationId,\n rootHash: evidence.indexRootHash,\n });\n await reclaimIncompletePrincipalHistoryStages(tx, stage);\n await reclaimUnpublishedPrincipalHistoryStages(tx, stage);\n await reclaimPrincipalHistoryNodes(tx, evidence);\n },\n input.stillCurrent,\n { behavior: \"immediate\" },\n );\n assertProjectionVerificationCurrent(() => saved.committed);\n}\n\nexport async function discardPrincipalHistoryStage(\n execSql: ExecSql,\n stage: PrincipalHistoryStage,\n stillCurrent: () => boolean,\n) {\n await ensureSqlTables(execSql, [\n ...principalHistoryStageTables,\n ...principalHistoryNodeRetentionTables,\n ]);\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const discarded = await runtime.guardedTransaction(\n async (db) => {\n const [current] = await db\n .select({ progress: principalHistoryStages.progress })\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, stage.id))\n .limit(1);\n if (current?.progress !== stage.progress) return;\n await releasePrincipalHistoryRoot(db, `stage:${stage.id}`);\n await db\n .delete(principalHistoryStages)\n .where(\n and(\n eq(principalHistoryStages.id, stage.id),\n eq(principalHistoryStages.progress, stage.progress),\n ),\n )\n .run();\n },\n stillCurrent,\n { behavior: \"immediate\" },\n );\n assertProjectionVerificationCurrent(() => discarded.committed);\n}\n"]}
1
+ {"version":3,"file":"principalHistoryStagePersistence.js","sourceRoot":"","sources":["principalHistoryStagePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport { assertProjectionVerificationCurrent } from \"../../keyingProjectionVerification/types\";\nimport { principalHistoryEvidenceTables } from \"../../sqlite/schema/principalHistoryEvidenceSchema\";\nimport { principalHistoryNodeRetentionTables } from \"../../sqlite/schema/principalHistoryNodeRetentionSchema\";\nimport {\n principalHistoryStages,\n principalHistoryStageTables,\n} from \"../../sqlite/schema/principalHistoryStageSchema\";\nimport { getClientSQLitePersistenceRuntime } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../../sqlite/sqlSchema\";\nimport {\n type PrincipalHistoryEvidencePage,\n writePrincipalHistoryEvidencePage,\n} from \"./principalHistoryEvidencePersistence\";\nimport { reclaimIncompletePrincipalHistoryStages } from \"./principalHistoryIncompleteRetention\";\nimport { reclaimPrincipalHistoryNodes } from \"./principalHistoryNodeRetention\";\nimport {\n foldablePrincipalHistoryRootEdges,\n loadPrincipalHistoryRootRetention,\n releasePrincipalHistoryRoot,\n writePrincipalHistoryRootRetention,\n} from \"./principalHistoryRootOwnership\";\nimport { reclaimUnpublishedPrincipalHistoryStages } from \"./principalHistoryUnpublishedRetention\";\n\n/** A stage's verification work; its retention hints are written by persistence. */\nexport type PrincipalHistoryStage = Omit<\n typeof principalHistoryStages.$inferSelect,\n \"scopeId\" | \"touchedAt\"\n>;\n\nconst principalHistoryStageColumns = {\n id: principalHistoryStages.id,\n organizationId: principalHistoryStages.organizationId,\n currentJson: principalHistoryStages.currentJson,\n afterVersion: principalHistoryStages.afterVersion,\n complete: principalHistoryStages.complete,\n progress: principalHistoryStages.progress,\n};\n\nclass PrincipalHistoryStageChangedError extends Error {\n readonly code = \"principal_history_stage_changed\";\n constructor() {\n super(\n \"Another operation changed the saved principal history; resume from its current state\",\n );\n this.name = \"PrincipalHistoryStageChangedError\";\n }\n}\n\n/** Another writer replaced this stage's progress between load and save. */\nexport function isPrincipalHistoryStageChangedError(error: unknown): boolean {\n return error instanceof PrincipalHistoryStageChangedError;\n}\n\nexport async function loadPrincipalHistoryStage(\n execSql: ExecSql,\n id: string,\n): Promise<PrincipalHistoryStage | null> {\n await ensureSqlTables(execSql, principalHistoryStageTables);\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const [row] = await db\n .select(principalHistoryStageColumns)\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, id))\n .limit(1);\n return row ?? null;\n}\n\n/** Publish accepted entries and index nodes atomically with authenticated progress. */\nexport async function savePrincipalHistoryStage(input: {\n readonly execSql: ExecSql;\n readonly stage: PrincipalHistoryStage;\n readonly evidence: PrincipalHistoryEvidencePage;\n readonly previousProgress: string | null;\n readonly stillCurrent: () => boolean;\n}): Promise<void> {\n const evidence = structuredClone(input.evidence);\n const stage = { ...input.stage, scopeId: evidence.scopeId };\n await ensureSqlTables(input.execSql, [\n ...principalHistoryStageTables,\n ...principalHistoryEvidenceTables,\n ]);\n const runtime = getClientSQLitePersistenceRuntime(input.execSql);\n const saved = await runtime.guardedTransaction(\n async (tx) => {\n const [previous] = await tx\n .select({ progress: principalHistoryStages.progress })\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, stage.id))\n .limit(1);\n if ((previous?.progress ?? null) !== input.previousProgress)\n throw new PrincipalHistoryStageChangedError();\n // The node write replays the root's edges after the page's own, as\n // separate statements after it would.\n const retention = await loadPrincipalHistoryRootRetention(tx, {\n id: `stage:${stage.id}`,\n scopeId: evidence.scopeId,\n organizationId: stage.organizationId,\n rootHash: evidence.indexRootHash,\n });\n const rootEdges = foldablePrincipalHistoryRootEdges(retention, evidence);\n await writePrincipalHistoryEvidencePage(tx, evidence, rootEdges);\n const row = { ...stage, touchedAt: Date.now() };\n await tx\n .insert(principalHistoryStages)\n .values(row)\n .onConflictDoUpdate({ target: principalHistoryStages.id, set: row })\n .run();\n await writePrincipalHistoryRootRetention(tx, retention, rootEdges);\n await reclaimIncompletePrincipalHistoryStages(tx, stage);\n await reclaimUnpublishedPrincipalHistoryStages(tx, stage);\n await reclaimPrincipalHistoryNodes(tx, evidence);\n },\n input.stillCurrent,\n { behavior: \"immediate\" },\n );\n assertProjectionVerificationCurrent(() => saved.committed);\n}\n\nexport async function discardPrincipalHistoryStage(\n execSql: ExecSql,\n stage: PrincipalHistoryStage,\n stillCurrent: () => boolean,\n) {\n await ensureSqlTables(execSql, [\n ...principalHistoryStageTables,\n ...principalHistoryNodeRetentionTables,\n ]);\n const runtime = getClientSQLitePersistenceRuntime(execSql);\n const discarded = await runtime.guardedTransaction(\n async (db) => {\n const [current] = await db\n .select({ progress: principalHistoryStages.progress })\n .from(principalHistoryStages)\n .where(eq(principalHistoryStages.id, stage.id))\n .limit(1);\n if (current?.progress !== stage.progress) return;\n await releasePrincipalHistoryRoot(db, `stage:${stage.id}`);\n await db\n .delete(principalHistoryStages)\n .where(\n and(\n eq(principalHistoryStages.id, stage.id),\n eq(principalHistoryStages.progress, stage.progress),\n ),\n )\n .run();\n },\n stillCurrent,\n { behavior: \"immediate\" },\n );\n assertProjectionVerificationCurrent(() => discarded.committed);\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { KeyingVerificationError, } from "../../../internal/crypto/index.js";
2
- import { and, eq } from "drizzle-orm";
2
+ import { and, eq, inArray } from "drizzle-orm";
3
3
  import { principalGrantRetirements } from "../../sqlite/schema/principalGrantRetirementSchema.js";
4
+ import { sqliteRowsPerStatement } from "../../sqlite/sqliteBindBudget.js";
4
5
  export async function storePrincipalGrantRetirements(tx, retirement, policies, organizationId) {
5
6
  if (!policies.some((policy) => policy.principalId === retirement.policy.principalId &&
6
7
  policy.principalType === retirement.policy.principalType &&
@@ -40,4 +41,37 @@ export async function assertContainerNotRetired(tx, checkpoint) {
40
41
  if (retired)
41
42
  throw new KeyingVerificationError("equivocation", `Container ${checkpoint.objectId} was retired during principal rotation`);
42
43
  }
44
+ // A lookup binds its organization once.
45
+ const RETIREMENT_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);
46
+ function retiredContainerKey(organizationId, containerId) {
47
+ return JSON.stringify([organizationId, containerId]);
48
+ }
49
+ /**
50
+ * The set form of `assertContainerNotRetired` for a batch of checkpoints: one
51
+ * read per organization. The returned check throws the same error for each
52
+ * checkpoint, so a caller replaying its batch in order fails at the same one.
53
+ */
54
+ export async function loadContainerRetirementCheck(tx, checkpoints) {
55
+ const idsByOrganization = new Map();
56
+ for (const checkpoint of checkpoints)
57
+ if (checkpoint.objectKind === "container")
58
+ idsByOrganization.set(checkpoint.organizationId, (idsByOrganization.get(checkpoint.organizationId) ?? new Set()).add(checkpoint.objectId));
59
+ const retired = new Set();
60
+ for (const [organizationId, ids] of idsByOrganization) {
61
+ const all = [...ids];
62
+ for (let index = 0; index < all.length; index += RETIREMENT_LOOKUP_BATCH_SIZE) {
63
+ const rows = await tx
64
+ .select({ containerId: principalGrantRetirements.containerId })
65
+ .from(principalGrantRetirements)
66
+ .where(and(eq(principalGrantRetirements.organizationId, organizationId), inArray(principalGrantRetirements.containerId, all.slice(index, index + RETIREMENT_LOOKUP_BATCH_SIZE))));
67
+ for (const row of rows)
68
+ retired.add(retiredContainerKey(organizationId, row.containerId));
69
+ }
70
+ }
71
+ return (checkpoint) => {
72
+ if (checkpoint.objectKind === "container" &&
73
+ retired.has(retiredContainerKey(checkpoint.organizationId, checkpoint.objectId)))
74
+ throw new KeyingVerificationError("equivocation", `Container ${checkpoint.objectId} was retired during principal rotation`);
75
+ };
76
+ }
43
77
  //# sourceMappingURL=principalGrantRetirementPersistence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"principalGrantRetirementPersistence.js","sourceRoot":"","sources":["principalGrantRetirementPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AccessManifestCheckpoint,\n KeyingVerificationError,\n type VerifiedPrincipalPolicy,\n type VerifiedPrincipalPolicyCurrent,\n} from \"@tearleads/crypto\";\nimport { and, eq } from \"drizzle-orm\";\nimport { principalGrantRetirements } from \"../../sqlite/schema/principalGrantRetirementSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface AcknowledgedPrincipalGrantRetirements {\n readonly containerIds: readonly string[];\n readonly organizationId: string;\n readonly policy: VerifiedPrincipalPolicy | VerifiedPrincipalPolicyCurrent;\n}\n\nexport async function storePrincipalGrantRetirements(\n tx: ClientSQLiteTransactionScope,\n retirement: AcknowledgedPrincipalGrantRetirements,\n policies: readonly (\n | VerifiedPrincipalPolicy\n | VerifiedPrincipalPolicyCurrent\n )[],\n organizationId: string | undefined,\n): Promise<void> {\n if (\n !policies.some(\n (policy) =>\n policy.principalId === retirement.policy.principalId &&\n policy.principalType === retirement.policy.principalType &&\n policy.stateHash === retirement.policy.stateHash,\n )\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Retirement must accompany its acknowledged policy\",\n );\n }\n const history =\n \"retainedHistory\" in retirement.policy\n ? retirement.policy.retainedHistory\n : retirement.policy.history;\n const previous = history?.find(\n (entry) => entry.state.stateHash === retirement.policy.state.prevStateHash,\n );\n const granted = new Set(\n [...retirement.policy.grants, ...(previous?.grants ?? [])].map(\n (grant) => grant.containerId,\n ),\n );\n if (\n retirement.organizationId !== organizationId ||\n retirement.containerIds.some((id) => !granted.has(id))\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Retirement must match the acknowledged organization and signed grants\",\n );\n }\n for (const containerId of new Set(retirement.containerIds)) {\n await tx\n .insert(principalGrantRetirements)\n .values({\n organizationId: retirement.organizationId,\n containerId,\n principalId: retirement.policy.principalId,\n policyStateHash: retirement.policy.stateHash,\n })\n .onConflictDoNothing()\n .run();\n }\n}\n\nexport async function assertContainerNotRetired(\n tx: ClientSQLiteTransactionScope,\n checkpoint: AccessManifestCheckpoint,\n): Promise<void> {\n if (checkpoint.objectKind !== \"container\") return;\n const [retired] = await tx\n .select({ containerId: principalGrantRetirements.containerId })\n .from(principalGrantRetirements)\n .where(\n and(\n eq(principalGrantRetirements.organizationId, checkpoint.organizationId),\n eq(principalGrantRetirements.containerId, checkpoint.objectId),\n ),\n )\n .limit(1);\n if (retired)\n throw new KeyingVerificationError(\n \"equivocation\",\n `Container ${checkpoint.objectId} was retired during principal rotation`,\n );\n}\n"]}
1
+ {"version":3,"file":"principalGrantRetirementPersistence.js","sourceRoot":"","sources":["principalGrantRetirementPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AccessManifestCheckpoint,\n KeyingVerificationError,\n type VerifiedPrincipalPolicy,\n type VerifiedPrincipalPolicyCurrent,\n} from \"@tearleads/crypto\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { principalGrantRetirements } from \"../../sqlite/schema/principalGrantRetirementSchema\";\nimport { sqliteRowsPerStatement } from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\nexport interface AcknowledgedPrincipalGrantRetirements {\n readonly containerIds: readonly string[];\n readonly organizationId: string;\n readonly policy: VerifiedPrincipalPolicy | VerifiedPrincipalPolicyCurrent;\n}\n\nexport async function storePrincipalGrantRetirements(\n tx: ClientSQLiteTransactionScope,\n retirement: AcknowledgedPrincipalGrantRetirements,\n policies: readonly (\n | VerifiedPrincipalPolicy\n | VerifiedPrincipalPolicyCurrent\n )[],\n organizationId: string | undefined,\n): Promise<void> {\n if (\n !policies.some(\n (policy) =>\n policy.principalId === retirement.policy.principalId &&\n policy.principalType === retirement.policy.principalType &&\n policy.stateHash === retirement.policy.stateHash,\n )\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Retirement must accompany its acknowledged policy\",\n );\n }\n const history =\n \"retainedHistory\" in retirement.policy\n ? retirement.policy.retainedHistory\n : retirement.policy.history;\n const previous = history?.find(\n (entry) => entry.state.stateHash === retirement.policy.state.prevStateHash,\n );\n const granted = new Set(\n [...retirement.policy.grants, ...(previous?.grants ?? [])].map(\n (grant) => grant.containerId,\n ),\n );\n if (\n retirement.organizationId !== organizationId ||\n retirement.containerIds.some((id) => !granted.has(id))\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Retirement must match the acknowledged organization and signed grants\",\n );\n }\n for (const containerId of new Set(retirement.containerIds)) {\n await tx\n .insert(principalGrantRetirements)\n .values({\n organizationId: retirement.organizationId,\n containerId,\n principalId: retirement.policy.principalId,\n policyStateHash: retirement.policy.stateHash,\n })\n .onConflictDoNothing()\n .run();\n }\n}\n\nexport async function assertContainerNotRetired(\n tx: ClientSQLiteTransactionScope,\n checkpoint: AccessManifestCheckpoint,\n): Promise<void> {\n if (checkpoint.objectKind !== \"container\") return;\n const [retired] = await tx\n .select({ containerId: principalGrantRetirements.containerId })\n .from(principalGrantRetirements)\n .where(\n and(\n eq(principalGrantRetirements.organizationId, checkpoint.organizationId),\n eq(principalGrantRetirements.containerId, checkpoint.objectId),\n ),\n )\n .limit(1);\n if (retired)\n throw new KeyingVerificationError(\n \"equivocation\",\n `Container ${checkpoint.objectId} was retired during principal rotation`,\n );\n}\n\n// A lookup binds its organization once.\nconst RETIREMENT_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);\n\nfunction retiredContainerKey(organizationId: string, containerId: string) {\n return JSON.stringify([organizationId, containerId]);\n}\n\n/**\n * The set form of `assertContainerNotRetired` for a batch of checkpoints: one\n * read per organization. The returned check throws the same error for each\n * checkpoint, so a caller replaying its batch in order fails at the same one.\n */\nexport async function loadContainerRetirementCheck(\n tx: ClientSQLiteTransactionScope,\n checkpoints: readonly AccessManifestCheckpoint[],\n): Promise<(checkpoint: AccessManifestCheckpoint) => void> {\n const idsByOrganization = new Map<string, Set<string>>();\n for (const checkpoint of checkpoints)\n if (checkpoint.objectKind === \"container\")\n idsByOrganization.set(\n checkpoint.organizationId,\n (idsByOrganization.get(checkpoint.organizationId) ?? new Set()).add(\n checkpoint.objectId,\n ),\n );\n const retired = new Set<string>();\n for (const [organizationId, ids] of idsByOrganization) {\n const all = [...ids];\n for (\n let index = 0;\n index < all.length;\n index += RETIREMENT_LOOKUP_BATCH_SIZE\n ) {\n const rows = await tx\n .select({ containerId: principalGrantRetirements.containerId })\n .from(principalGrantRetirements)\n .where(\n and(\n eq(principalGrantRetirements.organizationId, organizationId),\n inArray(\n principalGrantRetirements.containerId,\n all.slice(index, index + RETIREMENT_LOOKUP_BATCH_SIZE),\n ),\n ),\n );\n for (const row of rows)\n retired.add(retiredContainerKey(organizationId, row.containerId));\n }\n }\n return (checkpoint) => {\n if (\n checkpoint.objectKind === \"container\" &&\n retired.has(\n retiredContainerKey(checkpoint.organizationId, checkpoint.objectId),\n )\n )\n throw new KeyingVerificationError(\n \"equivocation\",\n `Container ${checkpoint.objectId} was retired during principal rotation`,\n );\n };\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { KeyingVerificationError } from "../../../internal/crypto/index.js";
2
- import { and, eq } from "drizzle-orm";
2
+ import { and, eq, inArray } from "drizzle-orm";
3
3
  import { principalPolicyOrganizations } from "../../sqlite/schema/index.js";
4
+ import { sqliteInsertBatchSize, sqliteRowsPerStatement, } from "../../sqlite/sqliteBindBudget.js";
4
5
  function resolveOrganizationId(input) {
5
6
  if (input.principalType === "organization") {
6
7
  if (input.organizationId !== undefined &&
@@ -32,6 +33,66 @@ export async function recordPrincipalPolicyOrganizationInTransaction(tx, input)
32
33
  .onConflictDoNothing()
33
34
  .run();
34
35
  }
36
+ // A lookup binds its principal type once.
37
+ const OWNER_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);
38
+ const OWNER_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalPolicyOrganizations);
39
+ function slices(values, size) {
40
+ const result = [];
41
+ for (let index = 0; index < values.length; index += size)
42
+ result.push(values.slice(index, index + size));
43
+ return result;
44
+ }
45
+ async function loadPrincipalPolicyOrganizations(tx, inputs) {
46
+ const owners = new Map();
47
+ const idsByType = new Map();
48
+ for (const input of inputs)
49
+ idsByType.set(input.principalType, (idsByType.get(input.principalType) ?? new Set()).add(input.principalId));
50
+ for (const [principalType, ids] of idsByType)
51
+ for (const batch of slices([...ids], OWNER_LOOKUP_BATCH_SIZE)) {
52
+ const rows = await tx
53
+ .select({
54
+ principalId: principalPolicyOrganizations.principalId,
55
+ organizationId: principalPolicyOrganizations.organizationId,
56
+ })
57
+ .from(principalPolicyOrganizations)
58
+ .where(and(eq(principalPolicyOrganizations.principalType, principalType), inArray(principalPolicyOrganizations.principalId, batch)));
59
+ for (const row of rows)
60
+ owners.set(JSON.stringify([principalType, row.principalId]), row.organizationId);
61
+ }
62
+ return owners;
63
+ }
64
+ /**
65
+ * The set form of `recordPrincipalPolicyOrganizationInTransaction`: one read
66
+ * per principal type, the same checks replayed in order (so the first failing
67
+ * input throws the same error), then one multi-row insert of the new owners.
68
+ */
69
+ export async function recordPrincipalPolicyOrganizationsInTransaction(tx, inputs) {
70
+ if (inputs.length === 0)
71
+ return;
72
+ const owners = await loadPrincipalPolicyOrganizations(tx, inputs);
73
+ const inserted = [];
74
+ for (const input of inputs) {
75
+ const organizationId = resolveOrganizationId(input);
76
+ const key = JSON.stringify([input.principalType, input.principalId]);
77
+ const stored = owners.get(key);
78
+ if (stored !== undefined && stored !== organizationId)
79
+ throw new KeyingVerificationError("object_mismatch", "principal policy cache is already owned by another organization");
80
+ if (stored !== undefined)
81
+ continue;
82
+ owners.set(key, organizationId);
83
+ inserted.push({
84
+ principalType: input.principalType,
85
+ principalId: input.principalId,
86
+ organizationId,
87
+ });
88
+ }
89
+ for (const batch of slices(inserted, OWNER_WRITE_BATCH_SIZE))
90
+ await tx
91
+ .insert(principalPolicyOrganizations)
92
+ .values(batch)
93
+ .onConflictDoNothing()
94
+ .run();
95
+ }
35
96
  export function recordPrincipalPolicyBundleOrganizationInTransaction(tx, bundle, organizationId) {
36
97
  return recordPrincipalPolicyOrganizationInTransaction(tx, {
37
98
  organizationId,
@@ -1 +1 @@
1
- {"version":3,"file":"principalPolicyOrganizationPersistence.js","sourceRoot":"","sources":["principalPolicyOrganizationPersistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AASnE,SAAS,qBAAqB,CAC5B,KAAuC;IAEvC,IAAI,KAAK,CAAC,aAAa,KAAK,cAAc,EAAE,CAAC;QAC3C,IACE,KAAK,CAAC,cAAc,KAAK,SAAS;YAClC,KAAK,CAAC,cAAc,KAAK,KAAK,CAAC,WAAW,EAC1C,CAAC;YACD,MAAM,IAAI,uBAAuB,CAC/B,iBAAiB,EACjB,kEAAkE,CACnE,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC,WAAW,CAAC;IAC3B,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,KAAK,CAAC,cAAc,CAAC;AAC9B,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,8CAA8C,CAClE,EAAgC,EAChC,KAAuC;IAEvC,MAAM,cAAc,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,cAAc,GAAG,GAAG,CACxB,EAAE,CAAC,4BAA4B,CAAC,aAAa,EAAE,KAAK,CAAC,aAAa,CAAC,EACnE,EAAE,CAAC,4BAA4B,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAChE,CAAC;IACF,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE;SACtB,MAAM,CAAC,EAAE,cAAc,EAAE,4BAA4B,CAAC,cAAc,EAAE,CAAC;SACvE,IAAI,CAAC,4BAA4B,CAAC;SAClC,KAAK,CAAC,cAAc,CAAC;SACrB,KAAK,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI,MAAM,IAAI,MAAM,CAAC,cAAc,KAAK,cAAc,EAAE,CAAC;QACvD,MAAM,IAAI,uBAAuB,CAC/B,iBAAiB,EACjB,iEAAiE,CAClE,CAAC;IACJ,CAAC;IACD,MAAM,EAAE;SACL,MAAM,CAAC,4BAA4B,CAAC;SACpC,MAAM,CAAC,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,CAAC;SACpC,mBAAmB,EAAE;SACrB,GAAG,EAAE,CAAC;AACX,CAAC;AAED,MAAM,UAAU,oDAAoD,CAClE,EAAgC,EAChC,MAAyD,EACzD,cAAmC;IAEnC,OAAO,8CAA8C,CAAC,EAAE,EAAE;QACxD,cAAc;QACd,WAAW,EAAE,MAAM,CAAC,YAAY,CAAC,WAAW;QAC5C,aAAa,EAAE,MAAM,CAAC,YAAY,CAAC,aAAa;KACjD,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { KeyingVerificationError } from \"@tearleads/crypto\";\nimport type { PrincipalStateResponse } from \"@tearleads/validators/response\";\nimport { and, eq } from \"drizzle-orm\";\nimport { principalPolicyOrganizations } from \"../../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\ninterface PrincipalPolicyOrganizationInput {\n readonly organizationId?: string | undefined;\n readonly principalId: string;\n readonly principalType: PrincipalStateResponse[\"principalType\"];\n}\n\nfunction resolveOrganizationId(\n input: PrincipalPolicyOrganizationInput,\n): string {\n if (input.principalType === \"organization\") {\n if (\n input.organizationId !== undefined &&\n input.organizationId !== input.principalId\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"organization policy cache ownership targets another organization\",\n );\n }\n return input.principalId;\n }\n if (!input.organizationId) {\n throw new Error(\"Group policy cache writes require an organization ID\");\n }\n return input.organizationId;\n}\n\n/** Pins cache ownership in the same transaction as the cached policy state. */\nexport async function recordPrincipalPolicyOrganizationInTransaction(\n tx: ClientSQLiteTransactionScope,\n input: PrincipalPolicyOrganizationInput,\n): Promise<void> {\n const organizationId = resolveOrganizationId(input);\n const wherePrincipal = and(\n eq(principalPolicyOrganizations.principalType, input.principalType),\n eq(principalPolicyOrganizations.principalId, input.principalId),\n );\n const [stored] = await tx\n .select({ organizationId: principalPolicyOrganizations.organizationId })\n .from(principalPolicyOrganizations)\n .where(wherePrincipal)\n .limit(1);\n if (stored && stored.organizationId !== organizationId) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"principal policy cache is already owned by another organization\",\n );\n }\n await tx\n .insert(principalPolicyOrganizations)\n .values({ ...input, organizationId })\n .onConflictDoNothing()\n .run();\n}\n\nexport function recordPrincipalPolicyBundleOrganizationInTransaction(\n tx: ClientSQLiteTransactionScope,\n bundle: { readonly currentState: PrincipalStateResponse },\n organizationId?: string | undefined,\n): Promise<void> {\n return recordPrincipalPolicyOrganizationInTransaction(tx, {\n organizationId,\n principalId: bundle.currentState.principalId,\n principalType: bundle.currentState.principalType,\n });\n}\n"]}
1
+ {"version":3,"file":"principalPolicyOrganizationPersistence.js","sourceRoot":"","sources":["principalPolicyOrganizationPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { KeyingVerificationError } from \"@tearleads/crypto\";\nimport type { PrincipalStateResponse } from \"@tearleads/validators/response\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { principalPolicyOrganizations } from \"../../sqlite/schema\";\nimport {\n sqliteInsertBatchSize,\n sqliteRowsPerStatement,\n} from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\n\ninterface PrincipalPolicyOrganizationInput {\n readonly organizationId?: string | undefined;\n readonly principalId: string;\n readonly principalType: PrincipalStateResponse[\"principalType\"];\n}\n\nfunction resolveOrganizationId(\n input: PrincipalPolicyOrganizationInput,\n): string {\n if (input.principalType === \"organization\") {\n if (\n input.organizationId !== undefined &&\n input.organizationId !== input.principalId\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"organization policy cache ownership targets another organization\",\n );\n }\n return input.principalId;\n }\n if (!input.organizationId) {\n throw new Error(\"Group policy cache writes require an organization ID\");\n }\n return input.organizationId;\n}\n\n/** Pins cache ownership in the same transaction as the cached policy state. */\nexport async function recordPrincipalPolicyOrganizationInTransaction(\n tx: ClientSQLiteTransactionScope,\n input: PrincipalPolicyOrganizationInput,\n): Promise<void> {\n const organizationId = resolveOrganizationId(input);\n const wherePrincipal = and(\n eq(principalPolicyOrganizations.principalType, input.principalType),\n eq(principalPolicyOrganizations.principalId, input.principalId),\n );\n const [stored] = await tx\n .select({ organizationId: principalPolicyOrganizations.organizationId })\n .from(principalPolicyOrganizations)\n .where(wherePrincipal)\n .limit(1);\n if (stored && stored.organizationId !== organizationId) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"principal policy cache is already owned by another organization\",\n );\n }\n await tx\n .insert(principalPolicyOrganizations)\n .values({ ...input, organizationId })\n .onConflictDoNothing()\n .run();\n}\n\n// A lookup binds its principal type once.\nconst OWNER_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);\nconst OWNER_WRITE_BATCH_SIZE = sqliteInsertBatchSize(\n principalPolicyOrganizations,\n);\n\nfunction slices<T>(values: readonly T[], size: number): T[][] {\n const result: T[][] = [];\n for (let index = 0; index < values.length; index += size)\n result.push(values.slice(index, index + size));\n return result;\n}\n\nasync function loadPrincipalPolicyOrganizations(\n tx: ClientSQLiteTransactionScope,\n inputs: readonly PrincipalPolicyOrganizationInput[],\n): Promise<Map<string, string>> {\n const owners = new Map<string, string>();\n const idsByType = new Map<\n PrincipalPolicyOrganizationInput[\"principalType\"],\n Set<string>\n >();\n for (const input of inputs)\n idsByType.set(\n input.principalType,\n (idsByType.get(input.principalType) ?? new Set()).add(input.principalId),\n );\n for (const [principalType, ids] of idsByType)\n for (const batch of slices([...ids], OWNER_LOOKUP_BATCH_SIZE)) {\n const rows = await tx\n .select({\n principalId: principalPolicyOrganizations.principalId,\n organizationId: principalPolicyOrganizations.organizationId,\n })\n .from(principalPolicyOrganizations)\n .where(\n and(\n eq(principalPolicyOrganizations.principalType, principalType),\n inArray(principalPolicyOrganizations.principalId, batch),\n ),\n );\n for (const row of rows)\n owners.set(\n JSON.stringify([principalType, row.principalId]),\n row.organizationId,\n );\n }\n return owners;\n}\n\n/**\n * The set form of `recordPrincipalPolicyOrganizationInTransaction`: one read\n * per principal type, the same checks replayed in order (so the first failing\n * input throws the same error), then one multi-row insert of the new owners.\n */\nexport async function recordPrincipalPolicyOrganizationsInTransaction(\n tx: ClientSQLiteTransactionScope,\n inputs: readonly PrincipalPolicyOrganizationInput[],\n): Promise<void> {\n if (inputs.length === 0) return;\n const owners = await loadPrincipalPolicyOrganizations(tx, inputs);\n const inserted: (PrincipalPolicyOrganizationInput & {\n readonly organizationId: string;\n })[] = [];\n for (const input of inputs) {\n const organizationId = resolveOrganizationId(input);\n const key = JSON.stringify([input.principalType, input.principalId]);\n const stored = owners.get(key);\n if (stored !== undefined && stored !== organizationId)\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"principal policy cache is already owned by another organization\",\n );\n if (stored !== undefined) continue;\n owners.set(key, organizationId);\n inserted.push({\n principalType: input.principalType,\n principalId: input.principalId,\n organizationId,\n });\n }\n for (const batch of slices(inserted, OWNER_WRITE_BATCH_SIZE))\n await tx\n .insert(principalPolicyOrganizations)\n .values(batch)\n .onConflictDoNothing()\n .run();\n}\n\nexport function recordPrincipalPolicyBundleOrganizationInTransaction(\n tx: ClientSQLiteTransactionScope,\n bundle: { readonly currentState: PrincipalStateResponse },\n organizationId?: string | undefined,\n): Promise<void> {\n return recordPrincipalPolicyOrganizationInTransaction(tx, {\n organizationId,\n principalId: bundle.currentState.principalId,\n principalType: bundle.currentState.principalType,\n });\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, or } from "drizzle-orm";
2
2
  import { trustedUserIdentityPins, trustedUserIdentityPinTables, } from "../sqlite/schema/index.js";
3
3
  import { getClientSQLitePersistenceRuntime, } from "../sqlite/sqlitePersistenceRuntime.js";
4
4
  import { ensureSqlTables } from "../sqlite/sqlSchema.js";
@@ -213,6 +213,27 @@ export async function loadTrustedUserIdForSigningKey(input) {
213
213
  .limit(1);
214
214
  return rows[0]?.userId ?? null;
215
215
  }
216
+ /**
217
+ * One read answers an exact repeat of a pin that alone holds its
218
+ * key and ranks at or above the candidate; anything else returns null for the
219
+ * write transaction. Pins are never deleted, keys never change, and
220
+ * provenance only rises, so a match is what the transaction would return.
221
+ */
222
+ const pins = trustedUserIdentityPins;
223
+ async function matchingPin(db, pin) {
224
+ const [row, other] = await db
225
+ .select()
226
+ .from(pins)
227
+ .where(and(eq(pins.identityTrustDomain, pin.identityTrustDomain), or(eq(pins.userId, pin.userId), eq(pins.signingKeyFingerprint, pin.signingKeyFingerprint))))
228
+ .limit(2);
229
+ if (other || row?.userId !== pin.userId || validationFailure(row))
230
+ return null;
231
+ const stored = parseStoredPin(row, pin);
232
+ const rank = provenanceRank;
233
+ if (differingPinFields(stored, pin).length > 0)
234
+ return null;
235
+ return rank[pin.provenance] <= rank[stored.provenance] ? stored : null;
236
+ }
216
237
  /**
217
238
  * Atomically establishes or verifies a durable identity pin.
218
239
  *
@@ -224,54 +245,56 @@ export async function loadTrustedUserIdForSigningKey(input) {
224
245
  * provenance is raised; it is never lowered.
225
246
  */
226
247
  export async function compareOrInsertTrustedUserIdentityPin(input) {
227
- assertCandidatePin(input.pin);
248
+ const { pin } = input;
249
+ assertCandidatePin(pin);
228
250
  const runtime = getClientSQLitePersistenceRuntime(input.execSql);
229
251
  return runTrustedUserIdentityPinTransaction(async () => {
230
252
  await ensureSqlTables(input.execSql, trustedUserIdentityPinTables);
253
+ // Like the write path's BEGIN, this read's BEGIN retries while any adapter
254
+ // holds the connection's transaction, so it never sees uncommitted pins.
255
+ const pinned = await runtime.transaction((tx) => matchingPin(tx, pin));
256
+ if (pinned)
257
+ return pinned;
231
258
  return runtime.transaction(async (tx) => {
232
259
  const aliases = await tx
233
260
  .select()
234
- .from(trustedUserIdentityPins)
235
- .where(and(eq(trustedUserIdentityPins.identityTrustDomain, input.pin.identityTrustDomain), eq(trustedUserIdentityPins.signingKeyFingerprint, input.pin.signingKeyFingerprint)))
261
+ .from(pins)
262
+ .where(and(eq(pins.identityTrustDomain, pin.identityTrustDomain), eq(pins.signingKeyFingerprint, pin.signingKeyFingerprint)))
236
263
  .limit(1);
237
264
  const alias = aliases[0];
238
- if (alias && alias.userId !== input.pin.userId) {
265
+ if (alias && alias.userId !== pin.userId) {
239
266
  throw new TrustedUserIdentityPinMismatchError(parseStoredPin(alias, {
240
- identityTrustDomain: input.pin.identityTrustDomain,
267
+ identityTrustDomain: pin.identityTrustDomain,
241
268
  userId: alias.userId,
242
- }), input.pin, ["userId"]);
269
+ }), pin, ["userId"]);
243
270
  }
244
271
  // Targets the primary key only: re-pinning the same user is a no-op,
245
272
  // but a write that would bind this key to a second user must fail at
246
273
  // the unique index rather than be silently skipped.
247
274
  await tx
248
- .insert(trustedUserIdentityPins)
249
- .values(input.pin)
275
+ .insert(pins)
276
+ .values(pin)
250
277
  .onConflictDoNothing({
251
- target: [
252
- trustedUserIdentityPins.identityTrustDomain,
253
- trustedUserIdentityPins.userId,
254
- ],
278
+ target: [pins.identityTrustDomain, pins.userId],
255
279
  })
256
280
  .run();
257
- const stored = await selectStoredPin(tx, input.pin);
281
+ const stored = await selectStoredPin(tx, pin);
258
282
  if (!stored) {
259
- throw new TrustedUserIdentityPinCorruptError(input.pin, "row is missing after compare-or-insert");
283
+ throw new TrustedUserIdentityPinCorruptError(pin, "row is missing after compare-or-insert");
260
284
  }
261
- const differingFields = differingPinFields(stored, input.pin);
285
+ const differingFields = differingPinFields(stored, pin);
262
286
  if (differingFields.length > 0) {
263
- throw new TrustedUserIdentityPinMismatchError(stored, input.pin, differingFields);
287
+ throw new TrustedUserIdentityPinMismatchError(stored, pin, differingFields);
264
288
  }
265
- if (provenanceRank[input.pin.provenance] <=
266
- provenanceRank[stored.provenance]) {
289
+ if (provenanceRank[pin.provenance] <= provenanceRank[stored.provenance]) {
267
290
  return stored;
268
291
  }
269
292
  await tx
270
- .update(trustedUserIdentityPins)
271
- .set({ provenance: input.pin.provenance })
272
- .where(and(eq(trustedUserIdentityPins.identityTrustDomain, input.pin.identityTrustDomain), eq(trustedUserIdentityPins.userId, input.pin.userId)))
293
+ .update(pins)
294
+ .set({ provenance: pin.provenance })
295
+ .where(and(eq(pins.identityTrustDomain, pin.identityTrustDomain), eq(pins.userId, pin.userId)))
273
296
  .run();
274
- return { ...stored, provenance: input.pin.provenance };
297
+ return { ...stored, provenance: pin.provenance };
275
298
  }, { behavior: "immediate" });
276
299
  });
277
300
  }