@tearleads/client-sdk 0.1.46 → 0.1.48

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.
@@ -1,9 +1,8 @@
1
- import { and, eq } from "drizzle-orm";
1
+ import { and, eq, lte } from "drizzle-orm";
2
2
  import { normalizeEffectiveAccessLevel } from "../../../accessLevel.js";
3
3
  import { DEFAULT_DOCUMENT_KIND } from "../../../documents/documentConstants.js";
4
4
  import { deriveStoredDocumentTitle } from "../../../documents/documentKinds.js";
5
5
  import { serializeDocumentSyncPullContinuation } from "../../../documents/shared/pullContinuation.js";
6
- import { documentRecordSelection, mapSelectedDocumentRecord, } from "../../../sqlite/documentPersistence.js";
7
6
  import { documentProjection, documentProjectionText, documents, } from "../../../sqlite/schema.js";
8
7
  import { getClientSQLitePersistenceRuntime, } from "../../../sqlite/sqlitePersistenceRuntime.js";
9
8
  import { DOCUMENTS_APP_KIND } from "./constants.js";
@@ -23,15 +22,6 @@ export async function hasDocumentRow(execSql, localId) {
23
22
  .limit(1);
24
23
  return rows.length > 0;
25
24
  }
26
- async function loadDocumentRecordInTransaction(input) {
27
- const { localId, tx } = input;
28
- const rows = await tx
29
- .select(documentRecordSelection)
30
- .from(documents)
31
- .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), eq(documents.localId, localId)))
32
- .limit(1);
33
- return rows[0] ? mapSelectedDocumentRecord(rows[0]) : null;
34
- }
35
25
  function toDocumentRecordRow(input) {
36
26
  const { document, updatedAt } = input;
37
27
  return {
@@ -113,23 +103,22 @@ export async function createDocumentRowsIfAbsent(input) {
113
103
  }
114
104
  export async function saveDocumentRows(input) {
115
105
  const { document, tx, updatedAt } = input;
116
- const existing = await loadDocumentRecordInTransaction({
117
- localId: document.id,
118
- tx,
119
- });
120
- if (existing &&
121
- (existing.recoveryGeneration ?? 0) > (document.recoveryGeneration ?? 0)) {
122
- return false;
123
- }
124
106
  const documentRow = toDocumentRecordRow({ document, updatedAt });
125
- await tx
107
+ // The recovery-generation fence rides on the upsert: a stored row from a
108
+ // newer recovery generation is left untouched and returns no row, so a
109
+ // stale writer skips the projection writes without a separate read.
110
+ const saved = await tx
126
111
  .insert(documents)
127
112
  .values(documentRow)
128
113
  .onConflictDoUpdate({
129
114
  target: [documents.appKind, documents.localId],
130
115
  set: documentRow,
116
+ setWhere: lte(documents.recoveryGeneration, documentRow.recoveryGeneration),
131
117
  })
132
- .run();
118
+ .returning({ localId: documents.localId });
119
+ if (saved.length === 0) {
120
+ return false;
121
+ }
133
122
  await saveDocumentProjectionRows(input);
134
123
  return true;
135
124
  }
@@ -145,29 +134,36 @@ export async function resolveDocumentSaveTimestamp(input) {
145
134
  if (options?.updatedAt) {
146
135
  return options.updatedAt;
147
136
  }
148
- const [existingRecord, projectionRows] = await Promise.all([
149
- loadDocumentRecordInTransaction({
150
- localId: document.id,
151
- tx,
152
- }),
153
- tx
154
- .select({
155
- documentKind: documentProjection.documentKind,
156
- text: documentProjectionText.text,
157
- title: documentProjection.title,
158
- updatedAt: documentProjection.updatedAt,
159
- })
160
- .from(documentProjection)
161
- .leftJoin(documentProjectionText, eq(documentProjectionText.localId, documentProjection.localId))
162
- .where(eq(documentProjection.localId, document.id))
163
- .limit(1),
164
- ]);
165
- const existingProjection = projectionRows[0];
137
+ // One read for the stored row and its projection; the projection columns
138
+ // are not nullable, so they are null exactly when the projection is absent.
139
+ const [existing] = await tx
140
+ .select({
141
+ snapshotEndVersion: documents.snapshotEndVersion,
142
+ documentKind: documentProjection.documentKind,
143
+ text: documentProjectionText.text,
144
+ title: documentProjection.title,
145
+ updatedAt: documentProjection.updatedAt,
146
+ })
147
+ .from(documents)
148
+ .leftJoin(documentProjection, eq(documentProjection.localId, documents.localId))
149
+ .leftJoin(documentProjectionText, eq(documentProjectionText.localId, documentProjection.localId))
150
+ .where(and(eq(documents.appKind, DOCUMENTS_APP_KIND), eq(documents.localId, document.id)))
151
+ .limit(1);
152
+ const existingProjection = existing?.documentKind != null &&
153
+ existing.title != null &&
154
+ existing.updatedAt != null
155
+ ? {
156
+ documentKind: existing.documentKind,
157
+ text: existing.text,
158
+ title: existing.title,
159
+ updatedAt: existing.updatedAt,
160
+ }
161
+ : undefined;
166
162
  const now = new Date().toISOString();
167
- return didStoredDocumentContentChange(existingRecord
163
+ return didStoredDocumentContentChange(existing
168
164
  ? {
169
165
  documentKind: getProjectionDocumentKind(existingProjection),
170
- snapshotEndVersion: existingRecord.snapshotEndVersion,
166
+ snapshotEndVersion: existing.snapshotEndVersion,
171
167
  text: getProjectionText(existingProjection),
172
168
  title: getProjectionTitle(existingProjection),
173
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"documentRows.js","sourceRoot":"","sources":["documentRows.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport { normalizeEffectiveAccessLevel } from \"../../../accessLevel\";\nimport { DEFAULT_DOCUMENT_KIND } from \"../../../documents/documentConstants\";\nimport { deriveStoredDocumentTitle } from \"../../../documents/documentKinds\";\nimport { serializeDocumentSyncPullContinuation } from \"../../../documents/shared/pullContinuation\";\nimport {\n type DocumentRecord as BaseDocumentRecord,\n type DocumentScope,\n documentRecordSelection,\n mapSelectedDocumentRecord,\n} from \"../../../sqlite/documentPersistence\";\nimport {\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { DOCUMENTS_APP_KIND } from \"./constants\";\nimport {\n getProjectionDocumentKind,\n getProjectionText,\n getProjectionTitle,\n getProjectionUpdatedAt,\n} from \"./documentProjectionRows\";\n\nexport function getDocumentScope(localId: string): DocumentScope {\n return {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n };\n}\n\nexport async function hasDocumentRow(\n execSql: ExecSql,\n localId: string,\n): Promise<boolean> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.localId, localId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\nasync function loadDocumentRecordInTransaction(input: {\n localId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<BaseDocumentRecord | null> {\n const { localId, tx } = input;\n const rows = await tx\n .select(documentRecordSelection)\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.localId, localId),\n ),\n )\n .limit(1);\n\n return rows[0] ? mapSelectedDocumentRecord(rows[0]) : null;\n}\n\nfunction toDocumentRecordRow(input: {\n document: StoredDocumentRecord;\n updatedAt: string;\n}) {\n const { document, updatedAt } = input;\n return {\n appKind: DOCUMENTS_APP_KIND,\n localId: document.id,\n documentId: document.documentId,\n recoveryGeneration: document.recoveryGeneration ?? 0,\n recoveryDocumentId: document.documentId\n ? null\n : (document.recoveryDocumentId ?? null),\n snapshotEndVersion: document.snapshotEndVersion,\n accessEpoch: document.accessEpoch,\n accessStateHash: document.accessStateHash ?? null,\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n document.effectiveAccessLevel,\n ),\n lastCommitLsn: document.lastCommitLsn ?? null,\n documentManifestBundle: document.documentManifestBundle ?? null,\n contentKeyBundle: document.contentKeyBundle ?? null,\n documentKekTargets: document.documentKekTargets ?? null,\n // Only touch the outgoing-delta marker when the caller manages it. Callers\n // that leave it undefined (e.g. discovery upserts, registration bootstrap)\n // must not clobber a marker a device-first deferRemoteSync write persisted.\n ...(document.pendingBaseVersion === undefined\n ? {}\n : { pendingBaseVersion: document.pendingBaseVersion }),\n ...(document.pullContinuation === undefined\n ? {}\n : {\n pullContinuation: serializeDocumentSyncPullContinuation(\n document.pullContinuation,\n ),\n }),\n updatedAt,\n };\n}\n\nasync function saveDocumentProjectionRows(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const { document, tx, updatedAt } = input;\n const projectionRow = {\n localId: document.id,\n documentId: document.documentId,\n containerId: document.containerId,\n documentKind: document.documentKind ?? DEFAULT_DOCUMENT_KIND,\n title: document.title ?? deriveStoredDocumentTitle(document.text),\n updatedAt,\n };\n await tx\n .insert(documentProjection)\n .values(projectionRow)\n .onConflictDoUpdate({\n target: documentProjection.localId,\n set: projectionRow,\n })\n .run();\n\n const projectionTextRow = {\n localId: document.id,\n text: document.text,\n };\n await tx\n .insert(documentProjectionText)\n .values(projectionTextRow)\n .onConflictDoUpdate({\n target: documentProjectionText.localId,\n set: projectionTextRow,\n })\n .run();\n}\n\nexport async function createDocumentRowsIfAbsent(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<boolean> {\n const { document, tx, updatedAt } = input;\n const documentRow = toDocumentRecordRow({ document, updatedAt });\n const inserted = await tx\n .insert(documents)\n .values(documentRow)\n .onConflictDoNothing({\n target: [documents.appKind, documents.localId],\n })\n .returning({ localId: documents.localId });\n if (inserted.length === 0) {\n return false;\n }\n\n await saveDocumentProjectionRows(input);\n return true;\n}\n\nexport async function saveDocumentRows(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<boolean> {\n const { document, tx, updatedAt } = input;\n const existing = await loadDocumentRecordInTransaction({\n localId: document.id,\n tx,\n });\n if (\n existing &&\n (existing.recoveryGeneration ?? 0) > (document.recoveryGeneration ?? 0)\n ) {\n return false;\n }\n const documentRow = toDocumentRecordRow({ document, updatedAt });\n await tx\n .insert(documents)\n .values(documentRow)\n .onConflictDoUpdate({\n target: [documents.appKind, documents.localId],\n set: documentRow,\n })\n .run();\n await saveDocumentProjectionRows(input);\n return true;\n}\n\nfunction didStoredDocumentContentChange(\n existing: Pick<\n StoredDocumentRecord,\n \"documentKind\" | \"snapshotEndVersion\" | \"text\" | \"title\"\n > | null,\n next: Pick<\n StoredDocumentRecord,\n \"documentKind\" | \"snapshotEndVersion\" | \"text\" | \"title\"\n >,\n): boolean {\n return (\n existing === null ||\n existing.documentKind !== (next.documentKind ?? DEFAULT_DOCUMENT_KIND) ||\n existing.snapshotEndVersion !== next.snapshotEndVersion ||\n existing.text !== next.text ||\n existing.title !== (next.title ?? deriveStoredDocumentTitle(next.text))\n );\n}\n\nexport async function resolveDocumentSaveTimestamp(input: {\n document: StoredDocumentRecord;\n options?: { updatedAt?: string } | undefined;\n tx: ClientSQLiteTransactionScope;\n}): Promise<string> {\n const { document, options, tx } = input;\n if (options?.updatedAt) {\n return options.updatedAt;\n }\n\n const [existingRecord, projectionRows] = await Promise.all([\n loadDocumentRecordInTransaction({\n localId: document.id,\n tx,\n }),\n tx\n .select({\n documentKind: documentProjection.documentKind,\n text: documentProjectionText.text,\n title: documentProjection.title,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documentProjection)\n .leftJoin(\n documentProjectionText,\n eq(documentProjectionText.localId, documentProjection.localId),\n )\n .where(eq(documentProjection.localId, document.id))\n .limit(1),\n ]);\n const existingProjection = projectionRows[0];\n const now = new Date().toISOString();\n return didStoredDocumentContentChange(\n existingRecord\n ? {\n documentKind: getProjectionDocumentKind(existingProjection),\n snapshotEndVersion: existingRecord.snapshotEndVersion,\n text: getProjectionText(existingProjection),\n title: getProjectionTitle(existingProjection),\n }\n : null,\n document,\n )\n ? now\n : getProjectionUpdatedAt(existingProjection) || now;\n}\n"]}
1
+ {"version":3,"file":"documentRows.js","sourceRoot":"","sources":["documentRows.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, lte } from \"drizzle-orm\";\nimport { normalizeEffectiveAccessLevel } from \"../../../accessLevel\";\nimport { DEFAULT_DOCUMENT_KIND } from \"../../../documents/documentConstants\";\nimport { deriveStoredDocumentTitle } from \"../../../documents/documentKinds\";\nimport { serializeDocumentSyncPullContinuation } from \"../../../documents/shared/pullContinuation\";\nimport type { DocumentScope } from \"../../../sqlite/documentPersistence\";\nimport {\n documentProjection,\n documentProjectionText,\n documents,\n} from \"../../../sqlite/schema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport type { StoredDocumentRecord } from \"../types\";\nimport { DOCUMENTS_APP_KIND } from \"./constants\";\nimport {\n getProjectionDocumentKind,\n getProjectionText,\n getProjectionTitle,\n getProjectionUpdatedAt,\n} from \"./documentProjectionRows\";\n\nexport function getDocumentScope(localId: string): DocumentScope {\n return {\n appKind: DOCUMENTS_APP_KIND,\n localId,\n };\n}\n\nexport async function hasDocumentRow(\n execSql: ExecSql,\n localId: string,\n): Promise<boolean> {\n const { db } = getClientSQLitePersistenceRuntime(execSql);\n const rows = await db\n .select({ localId: documents.localId })\n .from(documents)\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.localId, localId),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\nfunction toDocumentRecordRow(input: {\n document: StoredDocumentRecord;\n updatedAt: string;\n}) {\n const { document, updatedAt } = input;\n return {\n appKind: DOCUMENTS_APP_KIND,\n localId: document.id,\n documentId: document.documentId,\n recoveryGeneration: document.recoveryGeneration ?? 0,\n recoveryDocumentId: document.documentId\n ? null\n : (document.recoveryDocumentId ?? null),\n snapshotEndVersion: document.snapshotEndVersion,\n accessEpoch: document.accessEpoch,\n accessStateHash: document.accessStateHash ?? null,\n effectiveAccessLevel: normalizeEffectiveAccessLevel(\n document.effectiveAccessLevel,\n ),\n lastCommitLsn: document.lastCommitLsn ?? null,\n documentManifestBundle: document.documentManifestBundle ?? null,\n contentKeyBundle: document.contentKeyBundle ?? null,\n documentKekTargets: document.documentKekTargets ?? null,\n // Only touch the outgoing-delta marker when the caller manages it. Callers\n // that leave it undefined (e.g. discovery upserts, registration bootstrap)\n // must not clobber a marker a device-first deferRemoteSync write persisted.\n ...(document.pendingBaseVersion === undefined\n ? {}\n : { pendingBaseVersion: document.pendingBaseVersion }),\n ...(document.pullContinuation === undefined\n ? {}\n : {\n pullContinuation: serializeDocumentSyncPullContinuation(\n document.pullContinuation,\n ),\n }),\n updatedAt,\n };\n}\n\nasync function saveDocumentProjectionRows(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<void> {\n const { document, tx, updatedAt } = input;\n const projectionRow = {\n localId: document.id,\n documentId: document.documentId,\n containerId: document.containerId,\n documentKind: document.documentKind ?? DEFAULT_DOCUMENT_KIND,\n title: document.title ?? deriveStoredDocumentTitle(document.text),\n updatedAt,\n };\n await tx\n .insert(documentProjection)\n .values(projectionRow)\n .onConflictDoUpdate({\n target: documentProjection.localId,\n set: projectionRow,\n })\n .run();\n\n const projectionTextRow = {\n localId: document.id,\n text: document.text,\n };\n await tx\n .insert(documentProjectionText)\n .values(projectionTextRow)\n .onConflictDoUpdate({\n target: documentProjectionText.localId,\n set: projectionTextRow,\n })\n .run();\n}\n\nexport async function createDocumentRowsIfAbsent(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<boolean> {\n const { document, tx, updatedAt } = input;\n const documentRow = toDocumentRecordRow({ document, updatedAt });\n const inserted = await tx\n .insert(documents)\n .values(documentRow)\n .onConflictDoNothing({\n target: [documents.appKind, documents.localId],\n })\n .returning({ localId: documents.localId });\n if (inserted.length === 0) {\n return false;\n }\n\n await saveDocumentProjectionRows(input);\n return true;\n}\n\nexport async function saveDocumentRows(input: {\n document: StoredDocumentRecord;\n tx: ClientSQLiteTransactionScope;\n updatedAt: string;\n}): Promise<boolean> {\n const { document, tx, updatedAt } = input;\n const documentRow = toDocumentRecordRow({ document, updatedAt });\n // The recovery-generation fence rides on the upsert: a stored row from a\n // newer recovery generation is left untouched and returns no row, so a\n // stale writer skips the projection writes without a separate read.\n const saved = await tx\n .insert(documents)\n .values(documentRow)\n .onConflictDoUpdate({\n target: [documents.appKind, documents.localId],\n set: documentRow,\n setWhere: lte(\n documents.recoveryGeneration,\n documentRow.recoveryGeneration,\n ),\n })\n .returning({ localId: documents.localId });\n if (saved.length === 0) {\n return false;\n }\n await saveDocumentProjectionRows(input);\n return true;\n}\n\nfunction didStoredDocumentContentChange(\n existing: Pick<\n StoredDocumentRecord,\n \"documentKind\" | \"snapshotEndVersion\" | \"text\" | \"title\"\n > | null,\n next: Pick<\n StoredDocumentRecord,\n \"documentKind\" | \"snapshotEndVersion\" | \"text\" | \"title\"\n >,\n): boolean {\n return (\n existing === null ||\n existing.documentKind !== (next.documentKind ?? DEFAULT_DOCUMENT_KIND) ||\n existing.snapshotEndVersion !== next.snapshotEndVersion ||\n existing.text !== next.text ||\n existing.title !== (next.title ?? deriveStoredDocumentTitle(next.text))\n );\n}\n\nexport async function resolveDocumentSaveTimestamp(input: {\n document: StoredDocumentRecord;\n options?: { updatedAt?: string } | undefined;\n tx: ClientSQLiteTransactionScope;\n}): Promise<string> {\n const { document, options, tx } = input;\n if (options?.updatedAt) {\n return options.updatedAt;\n }\n\n // One read for the stored row and its projection; the projection columns\n // are not nullable, so they are null exactly when the projection is absent.\n const [existing] = await tx\n .select({\n snapshotEndVersion: documents.snapshotEndVersion,\n documentKind: documentProjection.documentKind,\n text: documentProjectionText.text,\n title: documentProjection.title,\n updatedAt: documentProjection.updatedAt,\n })\n .from(documents)\n .leftJoin(\n documentProjection,\n eq(documentProjection.localId, documents.localId),\n )\n .leftJoin(\n documentProjectionText,\n eq(documentProjectionText.localId, documentProjection.localId),\n )\n .where(\n and(\n eq(documents.appKind, DOCUMENTS_APP_KIND),\n eq(documents.localId, document.id),\n ),\n )\n .limit(1);\n const existingProjection =\n existing?.documentKind != null &&\n existing.title != null &&\n existing.updatedAt != null\n ? {\n documentKind: existing.documentKind,\n text: existing.text,\n title: existing.title,\n updatedAt: existing.updatedAt,\n }\n : undefined;\n const now = new Date().toISOString();\n return didStoredDocumentContentChange(\n existing\n ? {\n documentKind: getProjectionDocumentKind(existingProjection),\n snapshotEndVersion: existing.snapshotEndVersion,\n text: getProjectionText(existingProjection),\n title: getProjectionTitle(existingProjection),\n }\n : null,\n document,\n )\n ? now\n : getProjectionUpdatedAt(existingProjection) || now;\n}\n"]}
@@ -2,9 +2,12 @@ import { compareCanonicalStrings, KeyingVerificationError, PRINCIPAL_HISTORY_PAG
2
2
  import { isPrincipalPolicyStateChainEntryResponse, } from "../../internal/validators/response/index.js";
3
3
  import { and, eq } from "drizzle-orm";
4
4
  import { principalHistoryEntries, principalHistoryEvidenceTables, principalHistoryNodes, } from "../sqlite/principalHistoryEvidenceSchema.js";
5
+ import { sqliteInsertBatchSize } from "../sqlite/sqliteBindBudget.js";
5
6
  import { getClientSQLitePersistenceRuntime, } from "../sqlite/sqlitePersistenceRuntime.js";
7
+ import { excludedValue } from "../sqlite/sqliteUpsert.js";
6
8
  import { ensureSqlTables } from "../sqlite/sqlSchema.js";
7
- import { recordPrincipalHistoryNodeEdges } from "./principalHistoryNodeRetention.js";
9
+ import { writePrincipalHistoryNodes } from "./principalHistoryNodeRetention.js";
10
+ const ENTRY_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalHistoryEntries);
8
11
  /** Prepare only a page accepted by the signature/authorization verifier. */
9
12
  export async function preparePrincipalHistoryEvidencePage(input) {
10
13
  if (input.entries.length > PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT ||
@@ -22,32 +25,28 @@ export async function preparePrincipalHistoryEvidencePage(input) {
22
25
  /** Publish in the same guarded transaction as the accepted page's progress. */
23
26
  export async function writePrincipalHistoryEvidencePage(tx, page) {
24
27
  const scope = { scopeId: page.scopeId, organizationId: page.organizationId };
25
- for (const entry of [...page.entries].sort((a, b) => compareCanonicalStrings(a.leafHash, b.leafHash))) {
26
- const row = { ...scope, ...entry };
28
+ // A repeated leaf keeps its last entry, as sequential upserts would.
29
+ const entries = new Map(page.entries.map((entry) => [
30
+ entry.leafHash,
31
+ { ...scope, leafHash: entry.leafHash, entryJson: entry.entryJson },
32
+ ]));
33
+ const rows = [...entries.values()].sort((a, b) => compareCanonicalStrings(a.leafHash, b.leafHash));
34
+ for (let index = 0; index < rows.length; index += ENTRY_WRITE_BATCH_SIZE)
27
35
  await tx
28
36
  .insert(principalHistoryEntries)
29
- .values(row)
37
+ .values(rows.slice(index, index + ENTRY_WRITE_BATCH_SIZE))
30
38
  .onConflictDoUpdate({
31
39
  target: [
32
40
  principalHistoryEntries.scopeId,
33
41
  principalHistoryEntries.leafHash,
34
42
  ],
35
- set: row,
43
+ set: {
44
+ organizationId: excludedValue(principalHistoryEntries.organizationId),
45
+ entryJson: excludedValue(principalHistoryEntries.entryJson),
46
+ },
36
47
  })
37
48
  .run();
38
- }
39
- for (const node of [...page.nodes].sort((a, b) => compareCanonicalStrings(a.hash, b.hash))) {
40
- await recordPrincipalHistoryNodeEdges(tx, scope, node);
41
- const row = { ...scope, ...node };
42
- await tx
43
- .insert(principalHistoryNodes)
44
- .values(row)
45
- .onConflictDoUpdate({
46
- target: [principalHistoryNodes.scopeId, principalHistoryNodes.hash],
47
- set: row,
48
- })
49
- .run();
50
- }
49
+ await writePrincipalHistoryNodes(tx, scope, [...page.nodes].sort((a, b) => compareCanonicalStrings(a.hash, b.hash)));
51
50
  }
52
51
  /** Returns untrusted material. The caller must verify it against the issued root. */
53
52
  export async function loadPrincipalHistoryReference(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"principalHistoryEvidencePersistence.js","sourceRoot":"","sources":["principalHistoryEvidencePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n compareCanonicalStrings,\n KeyingVerificationError,\n PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT,\n type PrincipalHistoryIndexNode,\n type PrincipalPolicyHistoryReferenceProof,\n principalHistoryIndexLeaf,\n type ReferencedPrincipalHead,\n resolvePrincipalHistoryIndexProof,\n type VerifiedPrincipalPolicyHistory,\n} from \"@tearleads/crypto\";\nimport {\n isPrincipalPolicyStateChainEntryResponse,\n type PrincipalPolicyStateChainEntryResponse,\n} from \"@tearleads/validators/response\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n principalHistoryEntries,\n principalHistoryEvidenceTables,\n principalHistoryNodes,\n} from \"../sqlite/principalHistoryEvidenceSchema\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../sqlite/sqlitePersistenceRuntime\";\nimport { type ExecSql, ensureSqlTables } from \"../sqlite/sqlSchema\";\nimport { recordPrincipalHistoryNodeEdges } from \"./principalHistoryNodeRetention\";\n\nexport interface PrincipalHistoryEvidencePage {\n readonly indexRootHash: string;\n readonly scopeId: string;\n readonly organizationId: string;\n readonly entries: readonly {\n readonly leafHash: string;\n readonly entryJson: string;\n }[];\n readonly nodes: readonly PrincipalHistoryIndexNode[];\n}\n\n/** Prepare only a page accepted by the signature/authorization verifier. */\nexport async function preparePrincipalHistoryEvidencePage(input: {\n readonly indexRootHash: string;\n readonly scopeId: string;\n readonly organizationId: string;\n readonly entries: readonly PrincipalPolicyStateChainEntryResponse[];\n readonly nodes: readonly PrincipalHistoryIndexNode[];\n}): Promise<PrincipalHistoryEvidencePage> {\n if (\n input.entries.length > PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT ||\n input.nodes.length > 2 * PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT\n )\n throw new KeyingVerificationError(\n \"invalid_shape\",\n \"Principal evidence page exceeds its batch budget\",\n );\n const owned = structuredClone(input);\n return {\n ...owned,\n entries: await Promise.all(\n owned.entries.map(async (entry) => ({\n leafHash: await principalHistoryIndexLeaf(entry.state),\n entryJson: JSON.stringify(entry),\n })),\n ),\n };\n}\n\n/** Publish in the same guarded transaction as the accepted page's progress. */\nexport async function writePrincipalHistoryEvidencePage(\n tx: ClientSQLiteTransactionScope,\n page: PrincipalHistoryEvidencePage,\n): Promise<void> {\n const scope = { scopeId: page.scopeId, organizationId: page.organizationId };\n for (const entry of [...page.entries].sort((a, b) =>\n compareCanonicalStrings(a.leafHash, b.leafHash),\n )) {\n const row = { ...scope, ...entry };\n await tx\n .insert(principalHistoryEntries)\n .values(row)\n .onConflictDoUpdate({\n target: [\n principalHistoryEntries.scopeId,\n principalHistoryEntries.leafHash,\n ],\n set: row,\n })\n .run();\n }\n for (const node of [...page.nodes].sort((a, b) =>\n compareCanonicalStrings(a.hash, b.hash),\n )) {\n await recordPrincipalHistoryNodeEdges(tx, scope, node);\n const row = { ...scope, ...node };\n await tx\n .insert(principalHistoryNodes)\n .values(row)\n .onConflictDoUpdate({\n target: [principalHistoryNodes.scopeId, principalHistoryNodes.hash],\n set: row,\n })\n .run();\n }\n}\n\n/** Returns untrusted material. The caller must verify it against the issued root. */\nexport async function loadPrincipalHistoryReference(input: {\n readonly execSql: ExecSql;\n readonly scopeId: string;\n readonly history: VerifiedPrincipalPolicyHistory;\n readonly version: number;\n readonly expectedReference?: ReferencedPrincipalHead | undefined;\n}): Promise<PrincipalPolicyHistoryReferenceProof> {\n await ensureSqlTables(input.execSql, principalHistoryEvidenceTables);\n const { db } = getClientSQLitePersistenceRuntime(input.execSql);\n const resolved = await resolvePrincipalHistoryIndexProof({\n rootHash: input.history.indexRootHash,\n treeSize: input.history.currentEntry.state.version,\n version: input.version,\n readNode: async (hash) => {\n const [row] = await db\n .select()\n .from(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, input.scopeId),\n eq(principalHistoryNodes.hash, hash),\n ),\n )\n .limit(1);\n return row ?? null;\n },\n });\n const [row] = await db\n .select()\n .from(principalHistoryEntries)\n .where(\n and(\n eq(principalHistoryEntries.scopeId, input.scopeId),\n eq(principalHistoryEntries.leafHash, resolved.leafHash),\n ),\n )\n .limit(1);\n let entry: unknown;\n try {\n entry = row ? JSON.parse(row.entryJson) : null;\n } catch {\n entry = null;\n }\n if (!isPrincipalPolicyStateChainEntryResponse(entry))\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Principal history entry is missing or corrupt\",\n );\n const reference = input.expectedReference ?? {\n principalType: entry.state.principalType,\n principalId: entry.state.principalId,\n version: entry.state.version,\n keyEpoch: entry.state.keyEpoch,\n stateHash: entry.state.stateHash,\n keyFingerprint: entry.state.keyFingerprint,\n };\n return { reference, entry, proof: resolved.proof };\n}\n"]}
1
+ {"version":3,"file":"principalHistoryEvidencePersistence.js","sourceRoot":"","sources":["principalHistoryEvidencePersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n compareCanonicalStrings,\n KeyingVerificationError,\n PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT,\n type PrincipalHistoryIndexNode,\n type PrincipalPolicyHistoryReferenceProof,\n principalHistoryIndexLeaf,\n type ReferencedPrincipalHead,\n resolvePrincipalHistoryIndexProof,\n type VerifiedPrincipalPolicyHistory,\n} from \"@tearleads/crypto\";\nimport {\n isPrincipalPolicyStateChainEntryResponse,\n type PrincipalPolicyStateChainEntryResponse,\n} from \"@tearleads/validators/response\";\nimport { and, eq } from \"drizzle-orm\";\nimport {\n principalHistoryEntries,\n principalHistoryEvidenceTables,\n principalHistoryNodes,\n} from \"../sqlite/principalHistoryEvidenceSchema\";\nimport { sqliteInsertBatchSize } from \"../sqlite/sqliteBindBudget\";\nimport {\n type ClientSQLiteTransactionScope,\n getClientSQLitePersistenceRuntime,\n} from \"../sqlite/sqlitePersistenceRuntime\";\nimport { excludedValue } from \"../sqlite/sqliteUpsert\";\nimport { type ExecSql, ensureSqlTables } from \"../sqlite/sqlSchema\";\nimport { writePrincipalHistoryNodes } from \"./principalHistoryNodeRetention\";\n\nconst ENTRY_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalHistoryEntries);\n\nexport interface PrincipalHistoryEvidencePage {\n readonly indexRootHash: string;\n readonly scopeId: string;\n readonly organizationId: string;\n readonly entries: readonly {\n readonly leafHash: string;\n readonly entryJson: string;\n }[];\n readonly nodes: readonly PrincipalHistoryIndexNode[];\n}\n\n/** Prepare only a page accepted by the signature/authorization verifier. */\nexport async function preparePrincipalHistoryEvidencePage(input: {\n readonly indexRootHash: string;\n readonly scopeId: string;\n readonly organizationId: string;\n readonly entries: readonly PrincipalPolicyStateChainEntryResponse[];\n readonly nodes: readonly PrincipalHistoryIndexNode[];\n}): Promise<PrincipalHistoryEvidencePage> {\n if (\n input.entries.length > PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT ||\n input.nodes.length > 2 * PRINCIPAL_HISTORY_PAGE_ENTRY_LIMIT\n )\n throw new KeyingVerificationError(\n \"invalid_shape\",\n \"Principal evidence page exceeds its batch budget\",\n );\n const owned = structuredClone(input);\n return {\n ...owned,\n entries: await Promise.all(\n owned.entries.map(async (entry) => ({\n leafHash: await principalHistoryIndexLeaf(entry.state),\n entryJson: JSON.stringify(entry),\n })),\n ),\n };\n}\n\n/** Publish in the same guarded transaction as the accepted page's progress. */\nexport async function writePrincipalHistoryEvidencePage(\n tx: ClientSQLiteTransactionScope,\n page: PrincipalHistoryEvidencePage,\n): Promise<void> {\n const scope = { scopeId: page.scopeId, organizationId: page.organizationId };\n // A repeated leaf keeps its last entry, as sequential upserts would.\n const entries = new Map(\n page.entries.map((entry) => [\n entry.leafHash,\n { ...scope, leafHash: entry.leafHash, entryJson: entry.entryJson },\n ]),\n );\n const rows = [...entries.values()].sort((a, b) =>\n compareCanonicalStrings(a.leafHash, b.leafHash),\n );\n for (let index = 0; index < rows.length; index += ENTRY_WRITE_BATCH_SIZE)\n await tx\n .insert(principalHistoryEntries)\n .values(rows.slice(index, index + ENTRY_WRITE_BATCH_SIZE))\n .onConflictDoUpdate({\n target: [\n principalHistoryEntries.scopeId,\n principalHistoryEntries.leafHash,\n ],\n set: {\n organizationId: excludedValue(principalHistoryEntries.organizationId),\n entryJson: excludedValue(principalHistoryEntries.entryJson),\n },\n })\n .run();\n await writePrincipalHistoryNodes(\n tx,\n scope,\n [...page.nodes].sort((a, b) => compareCanonicalStrings(a.hash, b.hash)),\n );\n}\n\n/** Returns untrusted material. The caller must verify it against the issued root. */\nexport async function loadPrincipalHistoryReference(input: {\n readonly execSql: ExecSql;\n readonly scopeId: string;\n readonly history: VerifiedPrincipalPolicyHistory;\n readonly version: number;\n readonly expectedReference?: ReferencedPrincipalHead | undefined;\n}): Promise<PrincipalPolicyHistoryReferenceProof> {\n await ensureSqlTables(input.execSql, principalHistoryEvidenceTables);\n const { db } = getClientSQLitePersistenceRuntime(input.execSql);\n const resolved = await resolvePrincipalHistoryIndexProof({\n rootHash: input.history.indexRootHash,\n treeSize: input.history.currentEntry.state.version,\n version: input.version,\n readNode: async (hash) => {\n const [row] = await db\n .select()\n .from(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, input.scopeId),\n eq(principalHistoryNodes.hash, hash),\n ),\n )\n .limit(1);\n return row ?? null;\n },\n });\n const [row] = await db\n .select()\n .from(principalHistoryEntries)\n .where(\n and(\n eq(principalHistoryEntries.scopeId, input.scopeId),\n eq(principalHistoryEntries.leafHash, resolved.leafHash),\n ),\n )\n .limit(1);\n let entry: unknown;\n try {\n entry = row ? JSON.parse(row.entryJson) : null;\n } catch {\n entry = null;\n }\n if (!isPrincipalPolicyStateChainEntryResponse(entry))\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Principal history entry is missing or corrupt\",\n );\n const reference = input.expectedReference ?? {\n principalType: entry.state.principalType,\n principalId: entry.state.principalId,\n version: entry.state.version,\n keyEpoch: entry.state.keyEpoch,\n stateHash: entry.state.stateHash,\n keyFingerprint: entry.state.keyFingerprint,\n };\n return { reference, entry, proof: resolved.proof };\n}\n"]}
@@ -1,48 +1,173 @@
1
- import { and, asc, eq } from "drizzle-orm";
1
+ import { and, asc, eq, inArray } from "drizzle-orm";
2
2
  import { principalHistoryNodes } from "../sqlite/principalHistoryEvidenceSchema.js";
3
3
  import { principalHistoryNodeReferences } from "../sqlite/principalHistoryNodeRetentionSchema.js";
4
- import { decrementPrincipalHistoryNodeReference, incrementPrincipalHistoryNodeReference, } from "./principalHistoryRootOwnership.js";
5
- /** Register edges before inserting their proof node, preserving pre-index evidence. */
6
- export async function recordPrincipalHistoryNodeEdges(tx, scope, node) {
7
- const key = and(eq(principalHistoryNodeReferences.scopeId, scope.scopeId), eq(principalHistoryNodeReferences.hash, node.hash));
8
- const [previous] = await tx
9
- .select()
10
- .from(principalHistoryNodeReferences)
11
- .where(key)
12
- .limit(1);
13
- if (previous?.leftHash === node.leftHash &&
14
- previous.rightHash === node.rightHash)
4
+ import { sqliteInsertBatchSize, sqliteRowsPerStatement, } from "../sqlite/sqliteBindBudget.js";
5
+ import { excludedValue } from "../sqlite/sqliteUpsert.js";
6
+ import { decrementPrincipalHistoryNodeReference, } from "./principalHistoryRootOwnership.js";
7
+ const REFERENCE_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalHistoryNodeReferences);
8
+ const NODE_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalHistoryNodes);
9
+ // The scope id binds once per lookup.
10
+ const HASH_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);
11
+ function batches(items, size) {
12
+ const result = [];
13
+ for (let index = 0; index < items.length; index += size)
14
+ result.push(items.slice(index, index + size));
15
+ return result;
16
+ }
17
+ async function loadNodeReferences(tx, scope, hashes, into) {
18
+ const missing = [...new Set(hashes)].filter((hash) => !into.has(hash));
19
+ for (const batch of batches(missing, HASH_LOOKUP_BATCH_SIZE)) {
20
+ for (const hash of batch)
21
+ into.set(hash, null);
22
+ const rows = await tx
23
+ .select()
24
+ .from(principalHistoryNodeReferences)
25
+ .where(and(eq(principalHistoryNodeReferences.scopeId, scope.scopeId), inArray(principalHistoryNodeReferences.hash, batch)));
26
+ for (const row of rows)
27
+ into.set(row.hash, row);
28
+ }
29
+ }
30
+ async function loadStoredNodeHashes(tx, scope, hashes) {
31
+ const stored = new Set();
32
+ for (const batch of batches([...new Set(hashes)], HASH_LOOKUP_BATCH_SIZE)) {
33
+ const rows = await tx
34
+ .select({ hash: principalHistoryNodes.hash })
35
+ .from(principalHistoryNodes)
36
+ .where(and(eq(principalHistoryNodes.scopeId, scope.scopeId), inArray(principalHistoryNodes.hash, batch)));
37
+ for (const row of rows)
38
+ stored.add(row.hash);
39
+ }
40
+ return stored;
41
+ }
42
+ /**
43
+ * Replays the per-node edge bookkeeping in memory: increment inserts or adds
44
+ * one, decrement floors at zero within the scope's organization, and an edge
45
+ * change keeps the count while marking nodes first stored here as managed.
46
+ */
47
+ function replayNodeEdges(scope, nodes, loaded, storedNodeHashes) {
48
+ const references = new Map(loaded);
49
+ const read = (hash) => {
50
+ const reference = references.get(hash);
51
+ if (reference === undefined)
52
+ throw new Error("Principal history node reference was not loaded");
53
+ return reference;
54
+ };
55
+ const increment = (hash) => {
56
+ const reference = read(hash);
57
+ references.set(hash, reference
58
+ ? { ...reference, referenceCount: reference.referenceCount + 1 }
59
+ : {
60
+ ...scope,
61
+ hash,
62
+ referenceCount: 1,
63
+ managed: false,
64
+ leftHash: null,
65
+ rightHash: null,
66
+ });
67
+ };
68
+ const decrement = (hash) => {
69
+ const reference = read(hash);
70
+ if (reference?.organizationId === scope.organizationId)
71
+ references.set(hash, {
72
+ ...reference,
73
+ referenceCount: Math.max(reference.referenceCount - 1, 0),
74
+ });
75
+ };
76
+ for (const node of nodes) {
77
+ const previous = read(node.hash);
78
+ if (previous?.leftHash !== node.leftHash ||
79
+ previous.rightHash !== node.rightHash) {
80
+ const managed = previous?.managed || !storedNodeHashes.has(node.hash);
81
+ increment(node.leftHash);
82
+ increment(node.rightHash);
83
+ const current = read(node.hash);
84
+ references.set(node.hash, {
85
+ ...(current ?? {
86
+ ...scope,
87
+ hash: node.hash,
88
+ referenceCount: previous?.referenceCount ?? 0,
89
+ }),
90
+ leftHash: node.leftHash,
91
+ rightHash: node.rightHash,
92
+ managed,
93
+ });
94
+ for (const hash of [previous?.leftHash, previous?.rightHash])
95
+ if (hash)
96
+ decrement(hash);
97
+ }
98
+ storedNodeHashes.add(node.hash);
99
+ }
100
+ return references;
101
+ }
102
+ function isSameNodeReference(left, right) {
103
+ return (left.organizationId === right.organizationId &&
104
+ left.referenceCount === right.referenceCount &&
105
+ left.managed === right.managed &&
106
+ left.leftHash === right.leftHash &&
107
+ left.rightHash === right.rightHash);
108
+ }
109
+ /**
110
+ * Register edges and store proof nodes in the given order, with the same
111
+ * result as recording each node's edges and then upserting it. Reads the
112
+ * touched liveness rows once, replays the bookkeeping, and writes the changed
113
+ * rows and the nodes as multi-row upserts.
114
+ */
115
+ export async function writePrincipalHistoryNodes(tx, scope, nodes) {
116
+ if (nodes.length === 0)
15
117
  return;
16
- const [existing] = await tx
17
- .select({ hash: principalHistoryNodes.hash })
18
- .from(principalHistoryNodes)
19
- .where(and(eq(principalHistoryNodes.scopeId, scope.scopeId), eq(principalHistoryNodes.hash, node.hash)))
20
- .limit(1);
21
- for (const hash of [node.leftHash, node.rightHash])
22
- await incrementPrincipalHistoryNodeReference(tx, scope, hash);
23
- await tx
24
- .insert(principalHistoryNodeReferences)
25
- .values({
26
- ...scope,
27
- ...node,
28
- referenceCount: previous?.referenceCount ?? 0,
29
- managed: previous?.managed || !existing,
30
- })
31
- .onConflictDoUpdate({
32
- target: [
33
- principalHistoryNodeReferences.scopeId,
34
- principalHistoryNodeReferences.hash,
35
- ],
36
- set: {
118
+ const loaded = new Map();
119
+ await loadNodeReferences(tx, scope, nodes.flatMap((node) => [node.hash, node.leftHash, node.rightHash]), loaded);
120
+ // An edge change releases the stored row's previous children.
121
+ await loadNodeReferences(tx, scope, nodes.flatMap((node) => {
122
+ const stored = loaded.get(node.hash);
123
+ return [stored?.leftHash, stored?.rightHash].filter((hash) => Boolean(hash));
124
+ }), loaded);
125
+ const replayed = replayNodeEdges(scope, nodes, loaded, await loadStoredNodeHashes(tx, scope, nodes.map((node) => node.hash)));
126
+ const changed = [...replayed].flatMap(([hash, reference]) => {
127
+ const before = loaded.get(hash);
128
+ return reference && !(before && isSameNodeReference(before, reference))
129
+ ? [reference]
130
+ : [];
131
+ });
132
+ for (const batch of batches(changed, REFERENCE_WRITE_BATCH_SIZE))
133
+ await tx
134
+ .insert(principalHistoryNodeReferences)
135
+ .values(batch)
136
+ .onConflictDoUpdate({
137
+ target: [
138
+ principalHistoryNodeReferences.scopeId,
139
+ principalHistoryNodeReferences.hash,
140
+ ],
141
+ set: {
142
+ referenceCount: excludedValue(principalHistoryNodeReferences.referenceCount),
143
+ managed: excludedValue(principalHistoryNodeReferences.managed),
144
+ leftHash: excludedValue(principalHistoryNodeReferences.leftHash),
145
+ rightHash: excludedValue(principalHistoryNodeReferences.rightHash),
146
+ },
147
+ })
148
+ .run();
149
+ const nodeRows = new Map(nodes.map((node) => [
150
+ node.hash,
151
+ {
152
+ ...scope,
153
+ hash: node.hash,
37
154
  leftHash: node.leftHash,
38
155
  rightHash: node.rightHash,
39
- managed: previous?.managed || !existing,
40
156
  },
41
- })
42
- .run();
43
- for (const hash of [previous?.leftHash, previous?.rightHash])
44
- if (hash)
45
- await decrementPrincipalHistoryNodeReference(tx, scope, hash);
157
+ ]));
158
+ for (const batch of batches([...nodeRows.values()], NODE_WRITE_BATCH_SIZE))
159
+ await tx
160
+ .insert(principalHistoryNodes)
161
+ .values(batch)
162
+ .onConflictDoUpdate({
163
+ target: [principalHistoryNodes.scopeId, principalHistoryNodes.hash],
164
+ set: {
165
+ organizationId: excludedValue(principalHistoryNodes.organizationId),
166
+ leftHash: excludedValue(principalHistoryNodes.leftHash),
167
+ rightHash: excludedValue(principalHistoryNodes.rightHash),
168
+ },
169
+ })
170
+ .run();
46
171
  }
47
172
  /** A bounded cascade releases only managed proof nodes with no live incoming edge. */
48
173
  export async function reclaimPrincipalHistoryNodes(tx, scope) {
@@ -1 +1 @@
1
- {"version":3,"file":"principalHistoryNodeRetention.js","sourceRoot":"","sources":["principalHistoryNodeRetention.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PrincipalHistoryIndexNode } from \"@tearleads/crypto\";\nimport { and, asc, eq } from \"drizzle-orm\";\nimport { principalHistoryNodes } from \"../sqlite/principalHistoryEvidenceSchema\";\nimport { principalHistoryNodeReferences } from \"../sqlite/principalHistoryNodeRetentionSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../sqlite/sqlitePersistenceRuntime\";\nimport {\n decrementPrincipalHistoryNodeReference,\n incrementPrincipalHistoryNodeReference,\n type PrincipalHistoryNodeScope,\n} from \"./principalHistoryRootOwnership\";\n\n/** Register edges before inserting their proof node, preserving pre-index evidence. */\nexport async function recordPrincipalHistoryNodeEdges(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n node: PrincipalHistoryIndexNode,\n) {\n const key = and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(principalHistoryNodeReferences.hash, node.hash),\n );\n const [previous] = await tx\n .select()\n .from(principalHistoryNodeReferences)\n .where(key)\n .limit(1);\n if (\n previous?.leftHash === node.leftHash &&\n previous.rightHash === node.rightHash\n )\n return;\n const [existing] = await tx\n .select({ hash: principalHistoryNodes.hash })\n .from(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, scope.scopeId),\n eq(principalHistoryNodes.hash, node.hash),\n ),\n )\n .limit(1);\n for (const hash of [node.leftHash, node.rightHash])\n await incrementPrincipalHistoryNodeReference(tx, scope, hash);\n await tx\n .insert(principalHistoryNodeReferences)\n .values({\n ...scope,\n ...node,\n referenceCount: previous?.referenceCount ?? 0,\n managed: previous?.managed || !existing,\n })\n .onConflictDoUpdate({\n target: [\n principalHistoryNodeReferences.scopeId,\n principalHistoryNodeReferences.hash,\n ],\n set: {\n leftHash: node.leftHash,\n rightHash: node.rightHash,\n managed: previous?.managed || !existing,\n },\n })\n .run();\n for (const hash of [previous?.leftHash, previous?.rightHash])\n if (hash) await decrementPrincipalHistoryNodeReference(tx, scope, hash);\n}\n\n/** A bounded cascade releases only managed proof nodes with no live incoming edge. */\nexport async function reclaimPrincipalHistoryNodes(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n): Promise<number> {\n let reclaimed = 0;\n while (reclaimed < 64) {\n const obsolete = await tx\n .select()\n .from(principalHistoryNodeReferences)\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(\n principalHistoryNodeReferences.organizationId,\n scope.organizationId,\n ),\n eq(principalHistoryNodeReferences.managed, true),\n eq(principalHistoryNodeReferences.referenceCount, 0),\n ),\n )\n .orderBy(asc(principalHistoryNodeReferences.hash))\n .limit(64 - reclaimed);\n if (obsolete.length === 0) break;\n for (const node of obsolete) {\n await tx\n .delete(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, scope.scopeId),\n eq(principalHistoryNodes.organizationId, scope.organizationId),\n eq(principalHistoryNodes.hash, node.hash),\n ),\n )\n .run();\n await tx\n .delete(principalHistoryNodeReferences)\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(principalHistoryNodeReferences.hash, node.hash),\n ),\n )\n .run();\n for (const hash of [node.leftHash, node.rightHash])\n if (hash) await decrementPrincipalHistoryNodeReference(tx, scope, hash);\n reclaimed += 1;\n }\n }\n return reclaimed;\n}\n"]}
1
+ {"version":3,"file":"principalHistoryNodeRetention.js","sourceRoot":"","sources":["principalHistoryNodeRetention.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PrincipalHistoryIndexNode } from \"@tearleads/crypto\";\nimport { and, asc, eq, inArray } from \"drizzle-orm\";\nimport { principalHistoryNodes } from \"../sqlite/principalHistoryEvidenceSchema\";\nimport { principalHistoryNodeReferences } from \"../sqlite/principalHistoryNodeRetentionSchema\";\nimport {\n sqliteInsertBatchSize,\n sqliteRowsPerStatement,\n} from \"../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../sqlite/sqlitePersistenceRuntime\";\nimport { excludedValue } from \"../sqlite/sqliteUpsert\";\nimport {\n decrementPrincipalHistoryNodeReference,\n type PrincipalHistoryNodeScope,\n} from \"./principalHistoryRootOwnership\";\n\ntype PrincipalHistoryNodeReference =\n typeof principalHistoryNodeReferences.$inferSelect;\n\nconst REFERENCE_WRITE_BATCH_SIZE = sqliteInsertBatchSize(\n principalHistoryNodeReferences,\n);\nconst NODE_WRITE_BATCH_SIZE = sqliteInsertBatchSize(principalHistoryNodes);\n// The scope id binds once per lookup.\nconst HASH_LOOKUP_BATCH_SIZE = sqliteRowsPerStatement(1, 1);\n\nfunction batches<T>(items: readonly T[], size: number): T[][] {\n const result: T[][] = [];\n for (let index = 0; index < items.length; index += size)\n result.push(items.slice(index, index + size));\n return result;\n}\n\nasync function loadNodeReferences(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hashes: readonly string[],\n into: Map<string, PrincipalHistoryNodeReference | null>,\n): Promise<void> {\n const missing = [...new Set(hashes)].filter((hash) => !into.has(hash));\n for (const batch of batches(missing, HASH_LOOKUP_BATCH_SIZE)) {\n for (const hash of batch) into.set(hash, null);\n const rows = await tx\n .select()\n .from(principalHistoryNodeReferences)\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n inArray(principalHistoryNodeReferences.hash, batch),\n ),\n );\n for (const row of rows) into.set(row.hash, row);\n }\n}\n\nasync function loadStoredNodeHashes(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n hashes: readonly string[],\n): Promise<Set<string>> {\n const stored = new Set<string>();\n for (const batch of batches([...new Set(hashes)], HASH_LOOKUP_BATCH_SIZE)) {\n const rows = await tx\n .select({ hash: principalHistoryNodes.hash })\n .from(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, scope.scopeId),\n inArray(principalHistoryNodes.hash, batch),\n ),\n );\n for (const row of rows) stored.add(row.hash);\n }\n return stored;\n}\n\n/**\n * Replays the per-node edge bookkeeping in memory: increment inserts or adds\n * one, decrement floors at zero within the scope's organization, and an edge\n * change keeps the count while marking nodes first stored here as managed.\n */\nfunction replayNodeEdges(\n scope: PrincipalHistoryNodeScope,\n nodes: readonly PrincipalHistoryIndexNode[],\n loaded: ReadonlyMap<string, PrincipalHistoryNodeReference | null>,\n storedNodeHashes: Set<string>,\n): Map<string, PrincipalHistoryNodeReference | null> {\n const references = new Map(loaded);\n const read = (hash: string) => {\n const reference = references.get(hash);\n if (reference === undefined)\n throw new Error(\"Principal history node reference was not loaded\");\n return reference;\n };\n const increment = (hash: string) => {\n const reference = read(hash);\n references.set(\n hash,\n reference\n ? { ...reference, referenceCount: reference.referenceCount + 1 }\n : {\n ...scope,\n hash,\n referenceCount: 1,\n managed: false,\n leftHash: null,\n rightHash: null,\n },\n );\n };\n const decrement = (hash: string) => {\n const reference = read(hash);\n if (reference?.organizationId === scope.organizationId)\n references.set(hash, {\n ...reference,\n referenceCount: Math.max(reference.referenceCount - 1, 0),\n });\n };\n for (const node of nodes) {\n const previous = read(node.hash);\n if (\n previous?.leftHash !== node.leftHash ||\n previous.rightHash !== node.rightHash\n ) {\n const managed = previous?.managed || !storedNodeHashes.has(node.hash);\n increment(node.leftHash);\n increment(node.rightHash);\n const current = read(node.hash);\n references.set(node.hash, {\n ...(current ?? {\n ...scope,\n hash: node.hash,\n referenceCount: previous?.referenceCount ?? 0,\n }),\n leftHash: node.leftHash,\n rightHash: node.rightHash,\n managed,\n });\n for (const hash of [previous?.leftHash, previous?.rightHash])\n if (hash) decrement(hash);\n }\n storedNodeHashes.add(node.hash);\n }\n return references;\n}\n\nfunction isSameNodeReference(\n left: PrincipalHistoryNodeReference,\n right: PrincipalHistoryNodeReference,\n): boolean {\n return (\n left.organizationId === right.organizationId &&\n left.referenceCount === right.referenceCount &&\n left.managed === right.managed &&\n left.leftHash === right.leftHash &&\n left.rightHash === right.rightHash\n );\n}\n\n/**\n * Register edges and store proof nodes in the given order, with the same\n * result as recording each node's edges and then upserting it. Reads the\n * touched liveness rows once, replays the bookkeeping, and writes the changed\n * rows and the nodes as multi-row upserts.\n */\nexport async function writePrincipalHistoryNodes(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n nodes: readonly PrincipalHistoryIndexNode[],\n): Promise<void> {\n if (nodes.length === 0) return;\n const loaded = new Map<string, PrincipalHistoryNodeReference | null>();\n await loadNodeReferences(\n tx,\n scope,\n nodes.flatMap((node) => [node.hash, node.leftHash, node.rightHash]),\n loaded,\n );\n // An edge change releases the stored row's previous children.\n await loadNodeReferences(\n tx,\n scope,\n nodes.flatMap((node) => {\n const stored = loaded.get(node.hash);\n return [stored?.leftHash, stored?.rightHash].filter(\n (hash): hash is string => Boolean(hash),\n );\n }),\n loaded,\n );\n const replayed = replayNodeEdges(\n scope,\n nodes,\n loaded,\n await loadStoredNodeHashes(\n tx,\n scope,\n nodes.map((node) => node.hash),\n ),\n );\n const changed = [...replayed].flatMap(([hash, reference]) => {\n const before = loaded.get(hash);\n return reference && !(before && isSameNodeReference(before, reference))\n ? [reference]\n : [];\n });\n for (const batch of batches(changed, REFERENCE_WRITE_BATCH_SIZE))\n await tx\n .insert(principalHistoryNodeReferences)\n .values(batch)\n .onConflictDoUpdate({\n target: [\n principalHistoryNodeReferences.scopeId,\n principalHistoryNodeReferences.hash,\n ],\n set: {\n referenceCount: excludedValue(\n principalHistoryNodeReferences.referenceCount,\n ),\n managed: excludedValue(principalHistoryNodeReferences.managed),\n leftHash: excludedValue(principalHistoryNodeReferences.leftHash),\n rightHash: excludedValue(principalHistoryNodeReferences.rightHash),\n },\n })\n .run();\n const nodeRows = new Map(\n nodes.map((node) => [\n node.hash,\n {\n ...scope,\n hash: node.hash,\n leftHash: node.leftHash,\n rightHash: node.rightHash,\n },\n ]),\n );\n for (const batch of batches([...nodeRows.values()], NODE_WRITE_BATCH_SIZE))\n await tx\n .insert(principalHistoryNodes)\n .values(batch)\n .onConflictDoUpdate({\n target: [principalHistoryNodes.scopeId, principalHistoryNodes.hash],\n set: {\n organizationId: excludedValue(principalHistoryNodes.organizationId),\n leftHash: excludedValue(principalHistoryNodes.leftHash),\n rightHash: excludedValue(principalHistoryNodes.rightHash),\n },\n })\n .run();\n}\n\n/** A bounded cascade releases only managed proof nodes with no live incoming edge. */\nexport async function reclaimPrincipalHistoryNodes(\n tx: ClientSQLiteTransactionScope,\n scope: PrincipalHistoryNodeScope,\n): Promise<number> {\n let reclaimed = 0;\n while (reclaimed < 64) {\n const obsolete = await tx\n .select()\n .from(principalHistoryNodeReferences)\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(\n principalHistoryNodeReferences.organizationId,\n scope.organizationId,\n ),\n eq(principalHistoryNodeReferences.managed, true),\n eq(principalHistoryNodeReferences.referenceCount, 0),\n ),\n )\n .orderBy(asc(principalHistoryNodeReferences.hash))\n .limit(64 - reclaimed);\n if (obsolete.length === 0) break;\n for (const node of obsolete) {\n await tx\n .delete(principalHistoryNodes)\n .where(\n and(\n eq(principalHistoryNodes.scopeId, scope.scopeId),\n eq(principalHistoryNodes.organizationId, scope.organizationId),\n eq(principalHistoryNodes.hash, node.hash),\n ),\n )\n .run();\n await tx\n .delete(principalHistoryNodeReferences)\n .where(\n and(\n eq(principalHistoryNodeReferences.scopeId, scope.scopeId),\n eq(principalHistoryNodeReferences.hash, node.hash),\n ),\n )\n .run();\n for (const hash of [node.leftHash, node.rightHash])\n if (hash) await decrementPrincipalHistoryNodeReference(tx, scope, hash);\n reclaimed += 1;\n }\n }\n return reclaimed;\n}\n"]}
@@ -1,52 +1,115 @@
1
1
  import { KeyingVerificationError } from "../../internal/crypto/index.js";
2
- import { and, eq } from "drizzle-orm";
2
+ import { and, eq, inArray, or } from "drizzle-orm";
3
3
  import { principalPolicyBundleStates } from "../principalPolicyStates.js";
4
4
  import { principalPolicyBundleReferences } from "../sqlite/schema.js";
5
- function exactReferenceWhere(row) {
6
- return and(eq(principalPolicyBundleReferences.principalType, row.principalType), eq(principalPolicyBundleReferences.principalId, row.principalId), eq(principalPolicyBundleReferences.version, row.version), eq(principalPolicyBundleReferences.stateHash, row.stateHash), eq(principalPolicyBundleReferences.keyEpoch, row.keyEpoch), eq(principalPolicyBundleReferences.keyFingerprint, row.keyFingerprint));
5
+ import { sqliteInsertBatchSize, sqliteRowsPerStatement, } from "../sqlite/sqliteBindBudget.js";
6
+ const REFERENCE_INSERT_BATCH_SIZE = sqliteInsertBatchSize(principalPolicyBundleReferences);
7
+ // Principal type and id plus the bundle version and state hash bind once.
8
+ const REFERENCE_LOOKUP_VERSIONS = sqliteRowsPerStatement(1, 4);
9
+ /** The exact six-field match; the lookup already fixes the principal. */
10
+ function isSameReference(stored, row) {
11
+ return (stored.version === row.version &&
12
+ stored.stateHash === row.stateHash &&
13
+ stored.keyEpoch === row.keyEpoch &&
14
+ stored.keyFingerprint === row.keyFingerprint);
7
15
  }
8
- async function assertNoIndexedConflict(tx, row) {
9
- const conflicts = await tx
10
- .select({
11
- bundleStateHash: principalPolicyBundleReferences.bundleStateHash,
12
- })
13
- .from(principalPolicyBundleReferences)
14
- .where(and(exactReferenceWhere(row), eq(principalPolicyBundleReferences.bundleVersion, row.bundleVersion)));
15
- if (conflicts.some(({ bundleStateHash }) => bundleStateHash !== row.bundleStateHash)) {
16
- throw new KeyingVerificationError("equivocation", "principal policy reference has conflicting bundle heads at one version");
16
+ /** The table's primary key within one principal. */
17
+ function referenceKey(row) {
18
+ return JSON.stringify([
19
+ row.version,
20
+ row.stateHash,
21
+ row.keyEpoch,
22
+ row.keyFingerprint,
23
+ row.bundleStateHash,
24
+ ]);
25
+ }
26
+ /** Rows either equivocation check can match, read before this bundle's writes. */
27
+ async function loadStoredReferences(tx, head, versions) {
28
+ const stored = [];
29
+ for (let index = 0; index < versions.length; index += REFERENCE_LOOKUP_VERSIONS) {
30
+ stored.push(...(await tx
31
+ .select({
32
+ version: principalPolicyBundleReferences.version,
33
+ stateHash: principalPolicyBundleReferences.stateHash,
34
+ keyEpoch: principalPolicyBundleReferences.keyEpoch,
35
+ keyFingerprint: principalPolicyBundleReferences.keyFingerprint,
36
+ bundleVersion: principalPolicyBundleReferences.bundleVersion,
37
+ bundleStateHash: principalPolicyBundleReferences.bundleStateHash,
38
+ })
39
+ .from(principalPolicyBundleReferences)
40
+ .where(and(eq(principalPolicyBundleReferences.principalType, head.principalType), eq(principalPolicyBundleReferences.principalId, head.principalId), inArray(principalPolicyBundleReferences.version, versions.slice(index, index + REFERENCE_LOOKUP_VERSIONS)), or(eq(principalPolicyBundleReferences.bundleVersion, head.version), eq(principalPolicyBundleReferences.bundleStateHash, head.stateHash))))));
17
41
  }
42
+ return stored;
18
43
  }
19
- export async function indexPrincipalPolicyBundleInTransaction(tx, bundle) {
20
- const head = bundle.currentState;
21
- for (const state of principalPolicyBundleStates(bundle)) {
22
- if (state.principalType !== head.principalType ||
23
- state.principalId !== head.principalId) {
24
- throw new KeyingVerificationError("object_mismatch", "principal policy bundle reference belongs to another principal");
25
- }
26
- const row = {
27
- principalType: state.principalType,
28
- principalId: state.principalId,
29
- version: state.version,
30
- stateHash: state.stateHash,
31
- keyEpoch: state.keyEpoch,
32
- keyFingerprint: state.keyFingerprint,
33
- bundleVersion: head.version,
34
- bundleStateHash: head.stateHash,
35
- };
36
- await assertNoIndexedConflict(tx, row);
44
+ /**
45
+ * Rows written earlier in the same bundle all carry its head, so neither
46
+ * check can match them: each state's outcome depends only on stored rows.
47
+ */
48
+ function referenceConflict(row, stored) {
49
+ const sameReference = stored.filter((candidate) => isSameReference(candidate, row));
50
+ if (sameReference.some((candidate) => candidate.bundleVersion === row.bundleVersion &&
51
+ candidate.bundleStateHash !== row.bundleStateHash))
52
+ return new KeyingVerificationError("equivocation", "principal policy reference has conflicting bundle heads at one version");
53
+ const indexed = sameReference.find((candidate) => candidate.bundleStateHash === row.bundleStateHash);
54
+ if (indexed && indexed.bundleVersion !== row.bundleVersion)
55
+ return new KeyingVerificationError("equivocation", "principal policy reference index conflicts with the bundle head");
56
+ return null;
57
+ }
58
+ async function insertMissingReferences(tx, rows, stored) {
59
+ const present = new Set(stored.map(referenceKey));
60
+ const missing = new Map();
61
+ for (const row of rows) {
62
+ const key = referenceKey(row);
63
+ if (!present.has(key) && !missing.has(key))
64
+ missing.set(key, row);
65
+ }
66
+ const values = [...missing.values()];
67
+ for (let index = 0; index < values.length; index += REFERENCE_INSERT_BATCH_SIZE) {
37
68
  await tx
38
69
  .insert(principalPolicyBundleReferences)
39
- .values(row)
70
+ .values(values.slice(index, index + REFERENCE_INSERT_BATCH_SIZE))
40
71
  .onConflictDoNothing()
41
72
  .run();
42
- const [stored] = await tx
43
- .select({ bundleVersion: principalPolicyBundleReferences.bundleVersion })
44
- .from(principalPolicyBundleReferences)
45
- .where(and(exactReferenceWhere(row), eq(principalPolicyBundleReferences.bundleStateHash, row.bundleStateHash)))
46
- .limit(1);
47
- if (stored?.bundleVersion !== row.bundleVersion) {
48
- throw new KeyingVerificationError("equivocation", "principal policy reference index conflicts with the bundle head");
73
+ }
74
+ }
75
+ /**
76
+ * Index every state of a verified bundle under its head. Fails in state order
77
+ * with the per-state checks' errors, keeping the states indexed before the
78
+ * first failure.
79
+ */
80
+ export async function indexPrincipalPolicyBundleInTransaction(tx, bundle) {
81
+ const head = bundle.currentState;
82
+ const states = principalPolicyBundleStates(bundle);
83
+ const foreignIndex = states.findIndex((state) => state.principalType !== head.principalType ||
84
+ state.principalId !== head.principalId);
85
+ const rows = (foreignIndex === -1 ? states : states.slice(0, foreignIndex)).map((state) => ({
86
+ principalType: state.principalType,
87
+ principalId: state.principalId,
88
+ version: state.version,
89
+ stateHash: state.stateHash,
90
+ keyEpoch: state.keyEpoch,
91
+ keyFingerprint: state.keyFingerprint,
92
+ bundleVersion: head.version,
93
+ bundleStateHash: head.stateHash,
94
+ }));
95
+ const stored = rows.length === 0
96
+ ? []
97
+ : await loadStoredReferences(tx, head, [
98
+ ...new Set(rows.map((row) => row.version)),
99
+ ]);
100
+ let failure = null;
101
+ let accepted = rows.length;
102
+ for (const [index, row] of rows.entries()) {
103
+ failure = referenceConflict(row, stored);
104
+ if (failure) {
105
+ accepted = index;
106
+ break;
49
107
  }
50
108
  }
109
+ if (!failure && foreignIndex !== -1)
110
+ failure = new KeyingVerificationError("object_mismatch", "principal policy bundle reference belongs to another principal");
111
+ await insertMissingReferences(tx, rows.slice(0, accepted), stored);
112
+ if (failure)
113
+ throw failure;
51
114
  }
52
115
  //# sourceMappingURL=principalPolicyReferenceIndexPersistence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"principalPolicyReferenceIndexPersistence.js","sourceRoot":"","sources":["principalPolicyReferenceIndexPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { KeyingVerificationError } from \"@tearleads/crypto\";\nimport type { PrincipalPolicyBundleResponse } from \"@tearleads/validators/response\";\nimport { and, eq } from \"drizzle-orm\";\nimport { principalPolicyBundleStates } from \"../principalPolicyStates\";\nimport { principalPolicyBundleReferences } from \"../sqlite/schema\";\nimport type { ClientSQLiteTransactionScope } from \"../sqlite/sqlitePersistenceRuntime\";\n\ntype PrincipalPolicyReferenceRow =\n typeof principalPolicyBundleReferences.$inferInsert;\n\nfunction exactReferenceWhere(row: PrincipalPolicyReferenceRow) {\n return and(\n eq(principalPolicyBundleReferences.principalType, row.principalType),\n eq(principalPolicyBundleReferences.principalId, row.principalId),\n eq(principalPolicyBundleReferences.version, row.version),\n eq(principalPolicyBundleReferences.stateHash, row.stateHash),\n eq(principalPolicyBundleReferences.keyEpoch, row.keyEpoch),\n eq(principalPolicyBundleReferences.keyFingerprint, row.keyFingerprint),\n );\n}\n\nasync function assertNoIndexedConflict(\n tx: ClientSQLiteTransactionScope,\n row: PrincipalPolicyReferenceRow,\n): Promise<void> {\n const conflicts = await tx\n .select({\n bundleStateHash: principalPolicyBundleReferences.bundleStateHash,\n })\n .from(principalPolicyBundleReferences)\n .where(\n and(\n exactReferenceWhere(row),\n eq(principalPolicyBundleReferences.bundleVersion, row.bundleVersion),\n ),\n );\n if (\n conflicts.some(\n ({ bundleStateHash }) => bundleStateHash !== row.bundleStateHash,\n )\n ) {\n throw new KeyingVerificationError(\n \"equivocation\",\n \"principal policy reference has conflicting bundle heads at one version\",\n );\n }\n}\n\nexport async function indexPrincipalPolicyBundleInTransaction(\n tx: ClientSQLiteTransactionScope,\n bundle: PrincipalPolicyBundleResponse,\n): Promise<void> {\n const head = bundle.currentState;\n for (const state of principalPolicyBundleStates(bundle)) {\n if (\n state.principalType !== head.principalType ||\n state.principalId !== head.principalId\n ) {\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"principal policy bundle reference belongs to another principal\",\n );\n }\n const row: PrincipalPolicyReferenceRow = {\n principalType: state.principalType,\n principalId: state.principalId,\n version: state.version,\n stateHash: state.stateHash,\n keyEpoch: state.keyEpoch,\n keyFingerprint: state.keyFingerprint,\n bundleVersion: head.version,\n bundleStateHash: head.stateHash,\n };\n await assertNoIndexedConflict(tx, row);\n await tx\n .insert(principalPolicyBundleReferences)\n .values(row)\n .onConflictDoNothing()\n .run();\n const [stored] = await tx\n .select({ bundleVersion: principalPolicyBundleReferences.bundleVersion })\n .from(principalPolicyBundleReferences)\n .where(\n and(\n exactReferenceWhere(row),\n eq(\n principalPolicyBundleReferences.bundleStateHash,\n row.bundleStateHash,\n ),\n ),\n )\n .limit(1);\n if (stored?.bundleVersion !== row.bundleVersion) {\n throw new KeyingVerificationError(\n \"equivocation\",\n \"principal policy reference index conflicts with the bundle head\",\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"principalPolicyReferenceIndexPersistence.js","sourceRoot":"","sources":["principalPolicyReferenceIndexPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import { KeyingVerificationError } from \"@tearleads/crypto\";\nimport type { PrincipalPolicyBundleResponse } from \"@tearleads/validators/response\";\nimport { and, eq, inArray, or } from \"drizzle-orm\";\nimport { principalPolicyBundleStates } from \"../principalPolicyStates\";\nimport { principalPolicyBundleReferences } from \"../sqlite/schema\";\nimport {\n sqliteInsertBatchSize,\n sqliteRowsPerStatement,\n} from \"../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../sqlite/sqlitePersistenceRuntime\";\n\ntype PrincipalPolicyReferenceRow =\n typeof principalPolicyBundleReferences.$inferInsert;\n\ntype StoredPrincipalPolicyReference = Pick<\n typeof principalPolicyBundleReferences.$inferSelect,\n | \"version\"\n | \"stateHash\"\n | \"keyEpoch\"\n | \"keyFingerprint\"\n | \"bundleVersion\"\n | \"bundleStateHash\"\n>;\n\nconst REFERENCE_INSERT_BATCH_SIZE = sqliteInsertBatchSize(\n principalPolicyBundleReferences,\n);\n// Principal type and id plus the bundle version and state hash bind once.\nconst REFERENCE_LOOKUP_VERSIONS = sqliteRowsPerStatement(1, 4);\n\n/** The exact six-field match; the lookup already fixes the principal. */\nfunction isSameReference(\n stored: StoredPrincipalPolicyReference,\n row: PrincipalPolicyReferenceRow,\n): boolean {\n return (\n stored.version === row.version &&\n stored.stateHash === row.stateHash &&\n stored.keyEpoch === row.keyEpoch &&\n stored.keyFingerprint === row.keyFingerprint\n );\n}\n\n/** The table's primary key within one principal. */\nfunction referenceKey(\n row: Pick<\n StoredPrincipalPolicyReference,\n \"version\" | \"stateHash\" | \"keyEpoch\" | \"keyFingerprint\" | \"bundleStateHash\"\n >,\n): string {\n return JSON.stringify([\n row.version,\n row.stateHash,\n row.keyEpoch,\n row.keyFingerprint,\n row.bundleStateHash,\n ]);\n}\n\n/** Rows either equivocation check can match, read before this bundle's writes. */\nasync function loadStoredReferences(\n tx: ClientSQLiteTransactionScope,\n head: PrincipalPolicyBundleResponse[\"currentState\"],\n versions: readonly number[],\n): Promise<StoredPrincipalPolicyReference[]> {\n const stored: StoredPrincipalPolicyReference[] = [];\n for (\n let index = 0;\n index < versions.length;\n index += REFERENCE_LOOKUP_VERSIONS\n ) {\n stored.push(\n ...(await tx\n .select({\n version: principalPolicyBundleReferences.version,\n stateHash: principalPolicyBundleReferences.stateHash,\n keyEpoch: principalPolicyBundleReferences.keyEpoch,\n keyFingerprint: principalPolicyBundleReferences.keyFingerprint,\n bundleVersion: principalPolicyBundleReferences.bundleVersion,\n bundleStateHash: principalPolicyBundleReferences.bundleStateHash,\n })\n .from(principalPolicyBundleReferences)\n .where(\n and(\n eq(\n principalPolicyBundleReferences.principalType,\n head.principalType,\n ),\n eq(principalPolicyBundleReferences.principalId, head.principalId),\n inArray(\n principalPolicyBundleReferences.version,\n versions.slice(index, index + REFERENCE_LOOKUP_VERSIONS),\n ),\n or(\n eq(principalPolicyBundleReferences.bundleVersion, head.version),\n eq(\n principalPolicyBundleReferences.bundleStateHash,\n head.stateHash,\n ),\n ),\n ),\n )),\n );\n }\n return stored;\n}\n\n/**\n * Rows written earlier in the same bundle all carry its head, so neither\n * check can match them: each state's outcome depends only on stored rows.\n */\nfunction referenceConflict(\n row: PrincipalPolicyReferenceRow,\n stored: readonly StoredPrincipalPolicyReference[],\n): KeyingVerificationError | null {\n const sameReference = stored.filter((candidate) =>\n isSameReference(candidate, row),\n );\n if (\n sameReference.some(\n (candidate) =>\n candidate.bundleVersion === row.bundleVersion &&\n candidate.bundleStateHash !== row.bundleStateHash,\n )\n )\n return new KeyingVerificationError(\n \"equivocation\",\n \"principal policy reference has conflicting bundle heads at one version\",\n );\n const indexed = sameReference.find(\n (candidate) => candidate.bundleStateHash === row.bundleStateHash,\n );\n if (indexed && indexed.bundleVersion !== row.bundleVersion)\n return new KeyingVerificationError(\n \"equivocation\",\n \"principal policy reference index conflicts with the bundle head\",\n );\n return null;\n}\n\nasync function insertMissingReferences(\n tx: ClientSQLiteTransactionScope,\n rows: readonly PrincipalPolicyReferenceRow[],\n stored: readonly StoredPrincipalPolicyReference[],\n): Promise<void> {\n const present = new Set(stored.map(referenceKey));\n const missing = new Map<string, PrincipalPolicyReferenceRow>();\n for (const row of rows) {\n const key = referenceKey(row);\n if (!present.has(key) && !missing.has(key)) missing.set(key, row);\n }\n const values = [...missing.values()];\n for (\n let index = 0;\n index < values.length;\n index += REFERENCE_INSERT_BATCH_SIZE\n ) {\n await tx\n .insert(principalPolicyBundleReferences)\n .values(values.slice(index, index + REFERENCE_INSERT_BATCH_SIZE))\n .onConflictDoNothing()\n .run();\n }\n}\n\n/**\n * Index every state of a verified bundle under its head. Fails in state order\n * with the per-state checks' errors, keeping the states indexed before the\n * first failure.\n */\nexport async function indexPrincipalPolicyBundleInTransaction(\n tx: ClientSQLiteTransactionScope,\n bundle: PrincipalPolicyBundleResponse,\n): Promise<void> {\n const head = bundle.currentState;\n const states = principalPolicyBundleStates(bundle);\n const foreignIndex = states.findIndex(\n (state) =>\n state.principalType !== head.principalType ||\n state.principalId !== head.principalId,\n );\n const rows: PrincipalPolicyReferenceRow[] = (\n foreignIndex === -1 ? states : states.slice(0, foreignIndex)\n ).map((state) => ({\n principalType: state.principalType,\n principalId: state.principalId,\n version: state.version,\n stateHash: state.stateHash,\n keyEpoch: state.keyEpoch,\n keyFingerprint: state.keyFingerprint,\n bundleVersion: head.version,\n bundleStateHash: head.stateHash,\n }));\n const stored =\n rows.length === 0\n ? []\n : await loadStoredReferences(tx, head, [\n ...new Set(rows.map((row) => row.version)),\n ]);\n let failure: KeyingVerificationError | null = null;\n let accepted = rows.length;\n for (const [index, row] of rows.entries()) {\n failure = referenceConflict(row, stored);\n if (failure) {\n accepted = index;\n break;\n }\n }\n if (!failure && foreignIndex !== -1)\n failure = new KeyingVerificationError(\n \"object_mismatch\",\n \"principal policy bundle reference belongs to another principal\",\n );\n await insertMissingReferences(tx, rows.slice(0, accepted), stored);\n if (failure) throw failure;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { sql } from "drizzle-orm";
2
+ /**
3
+ * The value a multi-row upsert proposed for `column`. A `set` clause needs it
4
+ * to apply each conflicting row's own value rather than one shared value.
5
+ */
6
+ export function excludedValue(column) {
7
+ return sql `excluded.${sql.identifier(column.name)}`;
8
+ }
9
+ //# sourceMappingURL=sqliteUpsert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqliteUpsert.js","sourceRoot":"","sources":["sqliteUpsert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,GAAG,EAAE,MAAM,aAAa,CAAC;AAG5C;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,OAAO,GAAG,CAAA,YAAY,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { type SQL, sql } from \"drizzle-orm\";\nimport type { SQLiteColumn } from \"drizzle-orm/sqlite-core\";\n\n/**\n * The value a multi-row upsert proposed for `column`. A `set` clause needs it\n * to apply each conflicting row's own value rather than one shared value.\n */\nexport function excludedValue(column: SQLiteColumn): SQL {\n return sql`excluded.${sql.identifier(column.name)}`;\n}\n"]}
@@ -1816,7 +1816,6 @@ export declare class ApiClient {
1816
1816
  }[];
1817
1817
  } | null>;
1818
1818
  getOrganizationBillingManagementUrl(organizationId: string): Promise<{
1819
- canCancelDirectly: boolean;
1820
1819
  managementUrl: string | null;
1821
1820
  } | null>;
1822
1821
  getOrganizationNativePurchaseEligibility(organizationId: string, store: NativeSubscriptionStore): Promise<{
@@ -497,16 +497,12 @@ export declare const getOrganizationBillingManagementUrlOperation: Omit<{
497
497
  readonly path: "/organizations/{organizationId}/billing/management-url";
498
498
  readonly responses: {
499
499
  readonly 200: z.ZodType<{
500
- canCancelDirectly: boolean;
501
500
  managementUrl: string | null;
502
501
  }, {
503
- canCancelDirectly: boolean;
504
502
  managementUrl: string | null;
505
503
  }, z.core.$ZodTypeInternals<{
506
- canCancelDirectly: boolean;
507
504
  managementUrl: string | null;
508
505
  }, {
509
- canCancelDirectly: boolean;
510
506
  managementUrl: string | null;
511
507
  }>>;
512
508
  };
@@ -12,16 +12,12 @@ import { z } from "zod";
12
12
  * this response.
13
13
  */
14
14
  export declare const OrganizationBillingManagementUrlResponseSchema: z.ZodType<{
15
- canCancelDirectly: boolean;
16
15
  managementUrl: string | null;
17
16
  }, {
18
- canCancelDirectly: boolean;
19
17
  managementUrl: string | null;
20
18
  }, z.core.$ZodTypeInternals<{
21
- canCancelDirectly: boolean;
22
19
  managementUrl: string | null;
23
20
  }, {
24
- canCancelDirectly: boolean;
25
21
  managementUrl: string | null;
26
22
  }>>;
27
23
  export type OrganizationBillingManagementUrlResponse = z.infer<typeof OrganizationBillingManagementUrlResponseSchema>;
@@ -13,8 +13,6 @@ import { loosePlainObject } from "../schema.js";
13
13
  * this response.
14
14
  */
15
15
  export const OrganizationBillingManagementUrlResponseSchema = loosePlainObject({
16
- /** Retained for installed clients; current clients read this from the snapshot. */
17
- canCancelDirectly: z.boolean(),
18
16
  managementUrl: z.string().nullable(),
19
17
  });
20
18
  export function isOrganizationBillingManagementUrlResponse(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"organizationBillingManagementUrl.js","sourceRoot":"","sources":["organizationBillingManagementUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,8CAA8C,GAAG,gBAAgB,CAAC;IAC7E,mFAAmF;IACnF,iBAAiB,EAAE,CAAC,CAAC,OAAO,EAAE;IAC9B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAMH,MAAM,UAAU,0CAA0C,CACxD,KAAc;IAEd,OAAO,8CAA8C,CAAC,SAAS,CAAC,KAAK,CAAC;SACnE,OAAO,CAAC;AACb,CAAC","sourcesContent":["import { z } from \"zod\";\nimport { loosePlainObject } from \"../schema\";\n\n/**\n * The subscription-management URL for an organization's paid subscription. The\n * server resolves it from the billing provider (RevenueCat) using the org's\n * stored customer id, so any org admin — not just the buyer whose device holds\n * the subscription — can reach the manage/cancel page. Native management\n * remains available for lapsed subscriptions so the buyer can repair a payment\n * issue. `managementUrl` is null when the organization has no native\n * provider-managed subscription, the provider exposes no management link, or\n * the lookup could not be completed. Which purchase system owns the\n * subscription is part of the billing snapshot (`subscriptionSource`), not of\n * this response.\n */\nexport const OrganizationBillingManagementUrlResponseSchema = loosePlainObject({\n /** Retained for installed clients; current clients read this from the snapshot. */\n canCancelDirectly: z.boolean(),\n managementUrl: z.string().nullable(),\n});\n\nexport type OrganizationBillingManagementUrlResponse = z.infer<\n typeof OrganizationBillingManagementUrlResponseSchema\n>;\n\nexport function isOrganizationBillingManagementUrlResponse(\n value: unknown,\n): value is OrganizationBillingManagementUrlResponse {\n return OrganizationBillingManagementUrlResponseSchema.safeParse(value)\n .success;\n}\n"]}
1
+ {"version":3,"file":"organizationBillingManagementUrl.js","sourceRoot":"","sources":["organizationBillingManagementUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,8CAA8C,GAAG,gBAAgB,CAAC;IAC7E,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAMH,MAAM,UAAU,0CAA0C,CACxD,KAAc;IAEd,OAAO,8CAA8C,CAAC,SAAS,CAAC,KAAK,CAAC;SACnE,OAAO,CAAC;AACb,CAAC","sourcesContent":["import { z } from \"zod\";\nimport { loosePlainObject } from \"../schema\";\n\n/**\n * The subscription-management URL for an organization's paid subscription. The\n * server resolves it from the billing provider (RevenueCat) using the org's\n * stored customer id, so any org admin — not just the buyer whose device holds\n * the subscription — can reach the manage/cancel page. Native management\n * remains available for lapsed subscriptions so the buyer can repair a payment\n * issue. `managementUrl` is null when the organization has no native\n * provider-managed subscription, the provider exposes no management link, or\n * the lookup could not be completed. Which purchase system owns the\n * subscription is part of the billing snapshot (`subscriptionSource`), not of\n * this response.\n */\nexport const OrganizationBillingManagementUrlResponseSchema = loosePlainObject({\n managementUrl: z.string().nullable(),\n});\n\nexport type OrganizationBillingManagementUrlResponse = z.infer<\n typeof OrganizationBillingManagementUrlResponseSchema\n>;\n\nexport function isOrganizationBillingManagementUrlResponse(\n value: unknown,\n): value is OrganizationBillingManagementUrlResponse {\n return OrganizationBillingManagementUrlResponseSchema.safeParse(value)\n .success;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tearleads/client-sdk",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "React-free Tearleads client runtime: local SQLite persistence, keys, sync, encrypted blobs, and domain workflows.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -75,21 +75,30 @@ function buildPrincipalPolicyHistoryEntries(bundle) {
75
75
  });
76
76
  return entries.reverse();
77
77
  }
78
+ // Entries run newest first; the oldest one held is the cursor for older
79
+ // entries unless it is genesis.
80
+ function olderEntriesCursor(entries) {
81
+ const oldestVersion = entries.at(-1)?.version;
82
+ return oldestVersion !== undefined && oldestVersion > 1
83
+ ? oldestVersion
84
+ : null;
85
+ }
78
86
  export function buildOrganizationGroupPolicyHistory(bundle, organizationId) {
87
+ const entries = buildPrincipalPolicyHistoryEntries(bundle);
79
88
  return {
80
- entries: buildPrincipalPolicyHistoryEntries(bundle),
89
+ entries,
81
90
  groupId: bundle.currentState.principalId,
91
+ nextBeforeVersion: olderEntriesCursor(entries),
82
92
  organizationId,
83
93
  principalId: bundle.currentState.principalId,
84
94
  principalType: "group",
85
95
  };
86
96
  }
87
97
  export function buildOrganizationPolicyHistory(bundle) {
98
+ const entries = buildPrincipalPolicyHistoryEntries(bundle);
88
99
  return {
89
- entries: buildPrincipalPolicyHistoryEntries(bundle).map((entry) => ({
90
- ...entry,
91
- groupChanges: null,
92
- })),
100
+ entries: entries.map((entry) => ({ ...entry, groupChanges: null })),
101
+ nextBeforeVersion: olderEntriesCursor(entries),
93
102
  organizationId: bundle.currentState.principalId,
94
103
  principalId: bundle.currentState.principalId,
95
104
  principalType: "organization",
@@ -1 +1 @@
1
- {"version":3,"file":"policyHistoryReadModel.js","sourceRoot":"","sources":["policyHistoryReadModel.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n PrincipalPolicyBundleResponse,\n PrincipalProjectionMemberResponse,\n PrincipalStateResponse,\n} from \"@tearleads/validators/response\";\nimport type {\n OrganizationGroupPolicyHistory,\n OrganizationPolicyHistory,\n OrganizationPrincipalMemberChange,\n OrganizationPrincipalPolicyHistoryEntry,\n} from \"./policyHistoryTypes\";\n\nexport type {\n OrganizationGroupPolicyHistory,\n OrganizationGroupPolicyHistoryEntry,\n OrganizationPolicyGrantChange,\n OrganizationPolicyGroupChange,\n OrganizationPolicyHistory,\n OrganizationPolicyHistoryEntry,\n OrganizationPrincipalMemberChange,\n OrganizationPrincipalMemberChangeType,\n OrganizationPrincipalPolicyHistory,\n OrganizationPrincipalPolicyHistoryEntry,\n} from \"./policyHistoryTypes\";\n\ninterface PrincipalPolicyHistoryState {\n readonly projection: ReadonlyArray<PrincipalProjectionMemberResponse>;\n readonly state: PrincipalStateResponse;\n}\n\nfunction projectionMemberKey(\n member: Pick<PrincipalProjectionMemberResponse, \"userId\">,\n): string {\n return member.userId;\n}\n\nfunction comparePrincipalMemberChanges(\n left: OrganizationPrincipalMemberChange,\n right: OrganizationPrincipalMemberChange,\n): number {\n return (\n left.userId.localeCompare(right.userId) ||\n left.changeType.localeCompare(right.changeType)\n );\n}\n\nexport function diffPrincipalProjectionMembers(input: {\n readonly current: ReadonlyArray<PrincipalProjectionMemberResponse>;\n readonly previous: ReadonlyArray<PrincipalProjectionMemberResponse>;\n}): OrganizationPrincipalMemberChange[] {\n const previousMembersByKey = new Map(\n input.previous.map((member) => [projectionMemberKey(member), member]),\n );\n const currentMembersByKey = new Map(\n input.current.map((member) => [projectionMemberKey(member), member]),\n );\n const changes: OrganizationPrincipalMemberChange[] = [];\n\n for (const currentMember of input.current) {\n const previousMember = previousMembersByKey.get(\n projectionMemberKey(currentMember),\n );\n if (!previousMember) {\n changes.push({\n changeType: \"added\",\n userId: currentMember.userId,\n nextRole: currentMember.role,\n previousRole: null,\n });\n continue;\n }\n\n if (previousMember.role !== currentMember.role) {\n changes.push({\n changeType: \"role_changed\",\n userId: currentMember.userId,\n nextRole: currentMember.role,\n previousRole: previousMember.role,\n });\n }\n }\n\n for (const previousMember of input.previous) {\n if (currentMembersByKey.has(projectionMemberKey(previousMember))) {\n continue;\n }\n\n changes.push({\n changeType: \"removed\",\n userId: previousMember.userId,\n nextRole: null,\n previousRole: previousMember.role,\n });\n }\n\n return changes.sort(comparePrincipalMemberChanges);\n}\n\nfunction principalPolicyHistoryStates(\n bundle: PrincipalPolicyBundleResponse,\n): PrincipalPolicyHistoryState[] {\n return [\n ...bundle.previousStates.map((entry) => ({\n projection: entry.projection,\n state: entry.state,\n })),\n {\n projection: bundle.currentProjection,\n state: bundle.currentState,\n },\n ].sort((left, right) => left.state.version - right.state.version);\n}\n\nfunction buildPrincipalPolicyHistoryEntries(\n bundle: PrincipalPolicyBundleResponse,\n): OrganizationPrincipalPolicyHistoryEntry[] {\n const states = principalPolicyHistoryStates(bundle);\n const entries = states.map((entry, index) => {\n const previousProjection = states[index - 1]?.projection ?? [];\n\n return {\n changes: diffPrincipalProjectionMembers({\n current: entry.projection,\n previous: previousProjection,\n }),\n createdAt: entry.state.createdAt,\n keyEpoch: entry.state.keyEpoch,\n memberCount: entry.state.memberCount,\n signedAt: entry.state.signedAt,\n signerUserId: entry.state.signerUserId,\n signerUserKeyFingerprint: entry.state.signerUserKeyFingerprint,\n stateHash: entry.state.stateHash,\n version: entry.state.version,\n };\n });\n\n return entries.reverse();\n}\n\nexport function buildOrganizationGroupPolicyHistory(\n bundle: PrincipalPolicyBundleResponse,\n organizationId: string,\n): OrganizationGroupPolicyHistory {\n return {\n entries: buildPrincipalPolicyHistoryEntries(bundle),\n groupId: bundle.currentState.principalId,\n organizationId,\n principalId: bundle.currentState.principalId,\n principalType: \"group\",\n };\n}\n\nexport function buildOrganizationPolicyHistory(\n bundle: PrincipalPolicyBundleResponse,\n): OrganizationPolicyHistory {\n return {\n entries: buildPrincipalPolicyHistoryEntries(bundle).map((entry) => ({\n ...entry,\n groupChanges: null,\n })),\n organizationId: bundle.currentState.principalId,\n principalId: bundle.currentState.principalId,\n principalType: \"organization\",\n };\n}\n"]}
1
+ {"version":3,"file":"policyHistoryReadModel.js","sourceRoot":"","sources":["policyHistoryReadModel.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n PrincipalPolicyBundleResponse,\n PrincipalProjectionMemberResponse,\n PrincipalStateResponse,\n} from \"@tearleads/validators/response\";\nimport type {\n OrganizationGroupPolicyHistory,\n OrganizationPolicyHistory,\n OrganizationPrincipalMemberChange,\n OrganizationPrincipalPolicyHistoryEntry,\n} from \"./policyHistoryTypes\";\n\nexport type {\n OrganizationGroupPolicyHistory,\n OrganizationGroupPolicyHistoryEntry,\n OrganizationPolicyGrantChange,\n OrganizationPolicyGroupChange,\n OrganizationPolicyHistory,\n OrganizationPolicyHistoryEntry,\n OrganizationPrincipalMemberChange,\n OrganizationPrincipalMemberChangeType,\n OrganizationPrincipalPolicyHistory,\n OrganizationPrincipalPolicyHistoryEntry,\n} from \"./policyHistoryTypes\";\n\ninterface PrincipalPolicyHistoryState {\n readonly projection: ReadonlyArray<PrincipalProjectionMemberResponse>;\n readonly state: PrincipalStateResponse;\n}\n\nfunction projectionMemberKey(\n member: Pick<PrincipalProjectionMemberResponse, \"userId\">,\n): string {\n return member.userId;\n}\n\nfunction comparePrincipalMemberChanges(\n left: OrganizationPrincipalMemberChange,\n right: OrganizationPrincipalMemberChange,\n): number {\n return (\n left.userId.localeCompare(right.userId) ||\n left.changeType.localeCompare(right.changeType)\n );\n}\n\nexport function diffPrincipalProjectionMembers(input: {\n readonly current: ReadonlyArray<PrincipalProjectionMemberResponse>;\n readonly previous: ReadonlyArray<PrincipalProjectionMemberResponse>;\n}): OrganizationPrincipalMemberChange[] {\n const previousMembersByKey = new Map(\n input.previous.map((member) => [projectionMemberKey(member), member]),\n );\n const currentMembersByKey = new Map(\n input.current.map((member) => [projectionMemberKey(member), member]),\n );\n const changes: OrganizationPrincipalMemberChange[] = [];\n\n for (const currentMember of input.current) {\n const previousMember = previousMembersByKey.get(\n projectionMemberKey(currentMember),\n );\n if (!previousMember) {\n changes.push({\n changeType: \"added\",\n userId: currentMember.userId,\n nextRole: currentMember.role,\n previousRole: null,\n });\n continue;\n }\n\n if (previousMember.role !== currentMember.role) {\n changes.push({\n changeType: \"role_changed\",\n userId: currentMember.userId,\n nextRole: currentMember.role,\n previousRole: previousMember.role,\n });\n }\n }\n\n for (const previousMember of input.previous) {\n if (currentMembersByKey.has(projectionMemberKey(previousMember))) {\n continue;\n }\n\n changes.push({\n changeType: \"removed\",\n userId: previousMember.userId,\n nextRole: null,\n previousRole: previousMember.role,\n });\n }\n\n return changes.sort(comparePrincipalMemberChanges);\n}\n\nfunction principalPolicyHistoryStates(\n bundle: PrincipalPolicyBundleResponse,\n): PrincipalPolicyHistoryState[] {\n return [\n ...bundle.previousStates.map((entry) => ({\n projection: entry.projection,\n state: entry.state,\n })),\n {\n projection: bundle.currentProjection,\n state: bundle.currentState,\n },\n ].sort((left, right) => left.state.version - right.state.version);\n}\n\nfunction buildPrincipalPolicyHistoryEntries(\n bundle: PrincipalPolicyBundleResponse,\n): OrganizationPrincipalPolicyHistoryEntry[] {\n const states = principalPolicyHistoryStates(bundle);\n const entries = states.map((entry, index) => {\n const previousProjection = states[index - 1]?.projection ?? [];\n\n return {\n changes: diffPrincipalProjectionMembers({\n current: entry.projection,\n previous: previousProjection,\n }),\n createdAt: entry.state.createdAt,\n keyEpoch: entry.state.keyEpoch,\n memberCount: entry.state.memberCount,\n signedAt: entry.state.signedAt,\n signerUserId: entry.state.signerUserId,\n signerUserKeyFingerprint: entry.state.signerUserKeyFingerprint,\n stateHash: entry.state.stateHash,\n version: entry.state.version,\n };\n });\n\n return entries.reverse();\n}\n\n// Entries run newest first; the oldest one held is the cursor for older\n// entries unless it is genesis.\nfunction olderEntriesCursor(\n entries: ReadonlyArray<OrganizationPrincipalPolicyHistoryEntry>,\n): number | null {\n const oldestVersion = entries.at(-1)?.version;\n return oldestVersion !== undefined && oldestVersion > 1\n ? oldestVersion\n : null;\n}\n\nexport function buildOrganizationGroupPolicyHistory(\n bundle: PrincipalPolicyBundleResponse,\n organizationId: string,\n): OrganizationGroupPolicyHistory {\n const entries = buildPrincipalPolicyHistoryEntries(bundle);\n return {\n entries,\n groupId: bundle.currentState.principalId,\n nextBeforeVersion: olderEntriesCursor(entries),\n organizationId,\n principalId: bundle.currentState.principalId,\n principalType: \"group\",\n };\n}\n\nexport function buildOrganizationPolicyHistory(\n bundle: PrincipalPolicyBundleResponse,\n): OrganizationPolicyHistory {\n const entries = buildPrincipalPolicyHistoryEntries(bundle);\n return {\n entries: entries.map((entry) => ({ ...entry, groupChanges: null })),\n nextBeforeVersion: olderEntriesCursor(entries),\n organizationId: bundle.currentState.principalId,\n principalId: bundle.currentState.principalId,\n principalType: \"organization\",\n };\n}\n"]}
@@ -24,14 +24,15 @@ export interface OrganizationPrincipalPolicyHistory {
24
24
  readonly principalType: PrincipalStateResponse["principalType"];
25
25
  }
26
26
  export interface OrganizationGroupPolicyHistory extends OrganizationPrincipalPolicyHistory {
27
- /** Exclusive cursor for older entries; absent on a legacy complete history. */
28
- readonly nextBeforeVersion?: number | null;
27
+ /** Exclusive cursor for older entries; null once the oldest is loaded. */
28
+ readonly nextBeforeVersion: number | null;
29
29
  readonly groupId: string;
30
30
  readonly organizationId: string;
31
31
  readonly principalType: "group";
32
32
  }
33
33
  export interface OrganizationPolicyHistory extends OrganizationPrincipalPolicyHistory {
34
- readonly nextBeforeVersion?: number | null;
34
+ /** Exclusive cursor for older entries; null once the oldest is loaded. */
35
+ readonly nextBeforeVersion: number | null;
35
36
  readonly entries: OrganizationPolicyHistoryEntry[];
36
37
  readonly organizationId: string;
37
38
  readonly principalType: "organization";
@@ -1 +1 @@
1
- {"version":3,"file":"policyHistoryTypes.js","sourceRoot":"","sources":["policyHistoryTypes.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n PrincipalContainerGrantResponse,\n PrincipalProjectionMemberResponse,\n PrincipalStateResponse,\n} from \"@tearleads/validators/response\";\n\nexport type OrganizationPrincipalMemberChangeType =\n | \"added\"\n | \"removed\"\n | \"role_changed\";\nexport interface OrganizationPrincipalMemberChange {\n readonly changeType: OrganizationPrincipalMemberChangeType;\n readonly userId: string;\n readonly nextRole: PrincipalProjectionMemberResponse[\"role\"] | null;\n readonly previousRole: PrincipalProjectionMemberResponse[\"role\"] | null;\n}\nexport interface OrganizationPrincipalPolicyHistoryEntry {\n readonly changes: OrganizationPrincipalMemberChange[];\n readonly createdAt: string;\n readonly keyEpoch: number;\n readonly memberCount: number;\n readonly signedAt: string;\n readonly signerUserId: string;\n readonly signerUserKeyFingerprint: string;\n readonly stateHash: string;\n readonly version: number;\n}\nexport type OrganizationGroupPolicyHistoryEntry =\n OrganizationPrincipalPolicyHistoryEntry;\nexport interface OrganizationPrincipalPolicyHistory {\n readonly entries: OrganizationPrincipalPolicyHistoryEntry[];\n readonly principalId: string;\n readonly principalType: PrincipalStateResponse[\"principalType\"];\n}\nexport interface OrganizationGroupPolicyHistory\n extends OrganizationPrincipalPolicyHistory {\n /** Exclusive cursor for older entries; absent on a legacy complete history. */\n readonly nextBeforeVersion?: number | null;\n readonly groupId: string;\n readonly organizationId: string;\n readonly principalType: \"group\";\n}\nexport interface OrganizationPolicyHistory\n extends OrganizationPrincipalPolicyHistory {\n readonly nextBeforeVersion?: number | null;\n readonly entries: OrganizationPolicyHistoryEntry[];\n readonly organizationId: string;\n readonly principalType: \"organization\";\n}\n\nexport interface OrganizationPolicyHistoryEntry\n extends OrganizationPrincipalPolicyHistoryEntry {\n /** Null when the additional signed evidence has not been loaded. */\n readonly groupChanges: OrganizationPolicyGroupChange[] | null;\n}\n\nexport interface OrganizationPolicyGrantChange {\n readonly containerId: string;\n readonly previousAccess:\n | PrincipalContainerGrantResponse[\"accessLevel\"]\n | null;\n readonly nextAccess: PrincipalContainerGrantResponse[\"accessLevel\"] | null;\n}\n\nexport interface OrganizationPolicyGroupChange {\n readonly groupId: string;\n readonly changeType: \"created\" | \"updated\" | \"deleted\";\n readonly previousVersion: number | null;\n readonly version: number;\n readonly previousKeyEpoch: number | null;\n readonly keyEpoch: number;\n readonly changes: OrganizationPrincipalMemberChange[];\n readonly grantChanges: OrganizationPolicyGrantChange[];\n}\n"]}
1
+ {"version":3,"file":"policyHistoryTypes.js","sourceRoot":"","sources":["policyHistoryTypes.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n PrincipalContainerGrantResponse,\n PrincipalProjectionMemberResponse,\n PrincipalStateResponse,\n} from \"@tearleads/validators/response\";\n\nexport type OrganizationPrincipalMemberChangeType =\n | \"added\"\n | \"removed\"\n | \"role_changed\";\nexport interface OrganizationPrincipalMemberChange {\n readonly changeType: OrganizationPrincipalMemberChangeType;\n readonly userId: string;\n readonly nextRole: PrincipalProjectionMemberResponse[\"role\"] | null;\n readonly previousRole: PrincipalProjectionMemberResponse[\"role\"] | null;\n}\nexport interface OrganizationPrincipalPolicyHistoryEntry {\n readonly changes: OrganizationPrincipalMemberChange[];\n readonly createdAt: string;\n readonly keyEpoch: number;\n readonly memberCount: number;\n readonly signedAt: string;\n readonly signerUserId: string;\n readonly signerUserKeyFingerprint: string;\n readonly stateHash: string;\n readonly version: number;\n}\nexport type OrganizationGroupPolicyHistoryEntry =\n OrganizationPrincipalPolicyHistoryEntry;\nexport interface OrganizationPrincipalPolicyHistory {\n readonly entries: OrganizationPrincipalPolicyHistoryEntry[];\n readonly principalId: string;\n readonly principalType: PrincipalStateResponse[\"principalType\"];\n}\nexport interface OrganizationGroupPolicyHistory\n extends OrganizationPrincipalPolicyHistory {\n /** Exclusive cursor for older entries; null once the oldest is loaded. */\n readonly nextBeforeVersion: number | null;\n readonly groupId: string;\n readonly organizationId: string;\n readonly principalType: \"group\";\n}\nexport interface OrganizationPolicyHistory\n extends OrganizationPrincipalPolicyHistory {\n /** Exclusive cursor for older entries; null once the oldest is loaded. */\n readonly nextBeforeVersion: number | null;\n readonly entries: OrganizationPolicyHistoryEntry[];\n readonly organizationId: string;\n readonly principalType: \"organization\";\n}\n\nexport interface OrganizationPolicyHistoryEntry\n extends OrganizationPrincipalPolicyHistoryEntry {\n /** Null when the additional signed evidence has not been loaded. */\n readonly groupChanges: OrganizationPolicyGroupChange[] | null;\n}\n\nexport interface OrganizationPolicyGrantChange {\n readonly containerId: string;\n readonly previousAccess:\n | PrincipalContainerGrantResponse[\"accessLevel\"]\n | null;\n readonly nextAccess: PrincipalContainerGrantResponse[\"accessLevel\"] | null;\n}\n\nexport interface OrganizationPolicyGroupChange {\n readonly groupId: string;\n readonly changeType: \"created\" | \"updated\" | \"deleted\";\n readonly previousVersion: number | null;\n readonly version: number;\n readonly previousKeyEpoch: number | null;\n readonly keyEpoch: number;\n readonly changes: OrganizationPrincipalMemberChange[];\n readonly grantChanges: OrganizationPolicyGrantChange[];\n}\n"]}