@tearleads/client-sdk 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/client/organizations/organizationGroupPresentation.d.ts +1 -12
  2. package/data/documents/shared/syncResponses.js +5 -7
  3. package/data/documents/shared/syncResponses.js.map +1 -1
  4. package/data/persistence/organizations/organizationReadModelGrantPersistence.js +2 -1
  5. package/data/persistence/organizations/organizationReadModelGrantPersistence.js.map +1 -1
  6. package/data/persistence/organizations/organizationReadModelMemberLoad.d.ts +9 -0
  7. package/data/persistence/organizations/organizationReadModelMemberLoad.js +29 -21
  8. package/data/persistence/organizations/organizationReadModelMemberLoad.js.map +1 -1
  9. package/data/persistence/organizations/organizationReadModelMembershipPersistence.js +91 -46
  10. package/data/persistence/organizations/organizationReadModelMembershipPersistence.js.map +1 -1
  11. package/data/persistence/organizations/organizationReadModelPersistence.js +95 -73
  12. package/data/persistence/organizations/organizationReadModelPersistence.js.map +1 -1
  13. package/data/persistence/organizations/organizationReadModelProjection.js +78 -89
  14. package/data/persistence/organizations/organizationReadModelProjection.js.map +1 -1
  15. package/data/persistence/organizations/organizationReadModelPurge.js +14 -15
  16. package/data/persistence/organizations/organizationReadModelPurge.js.map +1 -1
  17. package/data/sqlite/organizationReadModelSchema.js +23 -28
  18. package/data/sqlite/organizationReadModelSchema.js.map +1 -1
  19. package/data/sqlite/organizationReadModelTableRegistry.d.ts +116 -263
  20. package/data/sqlite/organizationReadModelTableRegistry.js +10 -7
  21. package/data/sqlite/organizationReadModelTableRegistry.js.map +1 -1
  22. package/data/sqlite/schema.d.ts +116 -263
  23. package/data/sqlite/sqliteBindBudget.js +30 -0
  24. package/data/sqlite/sqliteBindBudget.js.map +1 -0
  25. package/internal/validators/documentSyncRefinements.js +5 -0
  26. package/internal/validators/documentSyncRefinements.js.map +1 -1
  27. package/internal/validators/operation/documentSync.d.ts +3 -0
  28. package/internal/validators/operation/organizations.d.ts +0 -16
  29. package/internal/validators/response/documentSyncSchema.js +35 -1
  30. package/internal/validators/response/documentSyncSchema.js.map +1 -1
  31. package/internal/validators/response/index.d.ts +1 -1
  32. package/internal/validators/response/index.js.map +1 -1
  33. package/internal/validators/response/organizationReadModel.d.ts +10 -48
  34. package/internal/validators/response/organizationReadModel.js +9 -1
  35. package/internal/validators/response/organizationReadModel.js.map +1 -1
  36. package/package.json +1 -1
  37. package/workflows/documents/syncContentKeys.js +4 -9
  38. package/workflows/documents/syncContentKeys.js.map +1 -1
  39. package/workflows/organizations/readModel.d.ts +8 -3
  40. package/workflows/organizations/readModel.js.map +1 -1
  41. package/workflows/organizations/readModelProjection.d.ts +2 -12
  42. package/workflows/organizations/readModelProjection.js +1 -1
  43. package/workflows/organizations/readModelProjection.js.map +1 -1
  44. package/workflows/sync/remoteReset.js +1 -3
  45. package/workflows/sync/remoteReset.js.map +1 -1
  46. package/data/persistence/organizations/organizationReadModelPolicyHeadPersistence.js +0 -64
  47. package/data/persistence/organizations/organizationReadModelPolicyHeadPersistence.js.map +0 -1
@@ -6,17 +6,6 @@ export declare function loadOrganizationGroupPresentationDetails(input: {
6
6
  readonly readModelCoordinator: OrganizationReadModelCoordinator;
7
7
  readonly runtime: InternalWorkflowRuntimeInput;
8
8
  }): Promise<{
9
- members: {
10
- groupId: string;
11
- members: {
12
- encapsulationKeyFingerprint: string | null;
13
- encapsulationPublicKey: string | null;
14
- role: "member" | "admin";
15
- signingKeyFingerprint: string | null;
16
- signingPublicKey: string | null;
17
- userId: string;
18
- }[];
19
- organizationId: string;
20
- } | null;
9
+ members: import("./index.js").OrganizationGroupMembers | null;
21
10
  policyHistory: import("./index.js").OrganizationGroupPolicyHistory | null;
22
11
  }>;
@@ -227,14 +227,12 @@ function documentSyncContentKeyBundlesByEpoch(plan, response) {
227
227
  bundle.contentKeyEpoch <= 0) {
228
228
  throw new Error("Document sync response content-key bundle epoch mismatch");
229
229
  }
230
- const existing = bundleByEpoch.get(bundle.contentKeyEpoch);
231
- if (!existing) {
232
- bundleByEpoch.set(bundle.contentKeyEpoch, bundle);
233
- continue;
234
- }
235
- if (contentKeyBundleCanonical(existing) !== contentKeyBundleCanonical(bundle)) {
236
- throw new Error("Document sync response content-key bundle conflict");
230
+ // The current epoch travels only as contentKeyBundle; the array carries
231
+ // each older epoch once.
232
+ if (bundleByEpoch.has(bundle.contentKeyEpoch)) {
233
+ throw new Error("Document sync response repeats a content-key epoch");
237
234
  }
235
+ bundleByEpoch.set(bundle.contentKeyEpoch, bundle);
238
236
  }
239
237
  return bundleByEpoch;
240
238
  }
@@ -1 +1 @@
1
- {"version":3,"file":"syncResponses.js","sourceRoot":"","sources":["syncResponses.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CONTENT_RECORD_ENCRYPTION_SUITE,\n computeContentRecordNonceDomainHash,\n computeDocumentContentRecordCiphertextHash,\n computeDocumentContentRecordMetadataHash,\n verifyWriteHeader,\n type WriteHeader,\n} from \"@tearleads/crypto\";\nimport { isPlainObject as isPlainRecord } from \"@tearleads/validators/isPlainObject\";\nimport {\n DOCUMENT_NOT_FOUND_ERROR_CODE,\n DOCUMENT_SYNC_ERROR_CODES,\n type DocumentSyncResponse,\n} from \"@tearleads/validators/response\";\nimport { parseWalLsn } from \"@tearleads/validators/util\";\nimport { throwKeyingVerificationShapeFailure } from \"../../keyingProjectionVerification/error\";\nimport {\n readRecordPositiveInteger,\n readRecordString,\n readWriteHeader,\n serializeCanonical,\n serializedPersistedDocumentState,\n} from \"./readers\";\n\nexport { submitDocumentSync } from \"./syncPagination\";\n\nimport { documentWriteAuthorizationForHeader } from \"./syncResponseAuthorization\";\nimport type {\n DocumentSyncPlan,\n DocumentSyncSubmitFailure,\n DocumentWriterPublicKeyResolver,\n PersistedDocumentSyncState,\n} from \"./types\";\n\n// Remote-primary databases without a portable WAL position use the zero LSN\n// as an explicit \"checkpoint not tracked\" sentinel. It is accepted only when\n// the response declares that mode, letting a local record carrying a checkpoint\n// from another backend move to that primary without weakening tracked checks.\n// This protects against replica lag, not a server that falsely declares its own\n// consistency capability; the authenticated server remains the source of data.\nconst UNTRACKED_COMMIT_LSN = \"0/0\";\n\nexport class DocumentSyncResponseUpdateContentKeyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DocumentSyncResponseUpdateContentKeyError\";\n }\n}\n\nfunction assertAcceptedOutgoingUpdateIdsMatchPlan(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n const expected = plan.request.outgoingUpdates.map((update) => update.id);\n const accepted = response.acceptedOutgoingUpdateIds;\n const expectedSorted = [...expected].sort();\n const acceptedSorted = [...accepted].sort();\n if (\n expectedSorted.length !== acceptedSorted.length ||\n expectedSorted.some((id, index) => id !== acceptedSorted[index])\n ) {\n throw new Error(\"Document sync response accepted update mismatch\");\n }\n}\n\nasync function assertDocumentSyncResponseUpdateMatchesPlan(input: {\n contentKeyBundlesByEpoch: ReadonlyMap<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >;\n plan: DocumentSyncPlan;\n update: DocumentSyncResponse[\"updates\"][number];\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n}): Promise<void> {\n const { plan, update } = input;\n if (update.documentId !== plan.documentId) {\n throw new Error(\"Document sync response update document mismatch\");\n }\n const header = readWriteHeader(\n update.writeHeader,\n \"Document sync response write header\",\n );\n await assertDocumentSyncResponseUpdateHashes({ update });\n const contentKeyBundle = assertDocumentSyncResponseUpdateContentKeyBundle({\n contentKeyBundlesByEpoch: input.contentKeyBundlesByEpoch,\n header,\n plan,\n });\n assertDocumentSyncResponseWriteHeaderFields({ header, plan, update });\n await assertDocumentSyncResponseNonceDomain({ header, plan });\n await assertDocumentSyncResponseWriteHeaderSignature({\n header,\n contentKeyBundle,\n plan,\n resolveWriterPublicKey: input.resolveWriterPublicKey,\n update,\n });\n}\n\nfunction assertDocumentSyncResponseUpdateContentKeyBundle(input: {\n contentKeyBundlesByEpoch: ReadonlyMap<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >;\n header: WriteHeader;\n plan: DocumentSyncPlan;\n}): DocumentSyncResponse[\"contentKeyBundle\"] {\n const { header, plan } = input;\n if (header.contentKeyEpoch > plan.contentKeyEpoch) {\n throw new DocumentSyncResponseUpdateContentKeyError(\n \"Document sync response includes a future content-key epoch\",\n );\n }\n\n const bundle = input.contentKeyBundlesByEpoch.get(header.contentKeyEpoch);\n if (!bundle) {\n throw new DocumentSyncResponseUpdateContentKeyError(\n \"Document sync response content-key bundle missing\",\n );\n }\n if (\n bundle.documentId !== plan.documentId ||\n bundle.contentKeyEpoch !== header.contentKeyEpoch\n ) {\n throw new Error(\"Document sync response content-key bundle mismatch\");\n }\n return bundle;\n}\n\nasync function assertDocumentSyncResponseUpdateHashes(input: {\n update: DocumentSyncResponse[\"updates\"][number];\n}): Promise<void> {\n const { update } = input;\n const ciphertextHash = await computeDocumentContentRecordCiphertextHash(\n update.encryptedData,\n );\n if (\n ciphertextHash !==\n readRecordString(update.writeHeader, \"ciphertextHash\", \"write header\")\n ) {\n throw new Error(\"Document sync response ciphertext hash mismatch\");\n }\n const metadataHash = await computeDocumentContentRecordMetadataHash({\n ...(update.checkpointKind === undefined\n ? {}\n : { checkpointKind: update.checkpointKind }),\n ...(update.checkpointPayloadKind === undefined\n ? {}\n : { checkpointPayloadKind: update.checkpointPayloadKind }),\n documentId: update.documentId,\n partialEndVersionVector: update.partialEndVersionVector,\n partialStartVersionVector: update.partialStartVersionVector,\n plaintextHash: update.plaintextHash,\n ...(update.sourceVersionVector === undefined\n ? {}\n : { sourceVersionVector: update.sourceVersionVector }),\n updateId: update.id,\n });\n if (\n metadataHash !==\n readRecordString(update.writeHeader, \"metadataHash\", \"write header\")\n ) {\n throw new Error(\"Document sync response metadata hash mismatch\");\n }\n}\n\nfunction assertDocumentSyncResponseWriteHeaderFields(input: {\n header: WriteHeader;\n plan: DocumentSyncPlan;\n update: DocumentSyncResponse[\"updates\"][number];\n}): void {\n const { header, plan, update } = input;\n const mustMatchCurrentBoundary = isAcceptedOutgoingSyncUpdate(plan, update);\n\n if (\n header.version !== 1 ||\n header.objectKind !== \"document\" ||\n header.objectId !== plan.documentId ||\n header.organizationId !== plan.organizationId ||\n header.encryptionSuite !== CONTENT_RECORD_ENCRYPTION_SUITE ||\n header.writerKeyFingerprint !== update.authorFingerprint ||\n (mustMatchCurrentBoundary &&\n (header.contentKeyEpoch !== plan.contentKeyEpoch ||\n header.accessManifestHash !== plan.expectedLinkSetManifestHash ||\n header.targetHash !== plan.expectedTargetHash))\n ) {\n throw new Error(\"Document sync response write header mismatch\");\n }\n}\n\nfunction isAcceptedOutgoingSyncUpdate(\n plan: DocumentSyncPlan,\n update: DocumentSyncResponse[\"updates\"][number],\n): boolean {\n return plan.request.outgoingUpdates.some(\n (outgoingUpdate) => outgoingUpdate.id === update.id,\n );\n}\n\nasync function assertDocumentSyncResponseNonceDomain(input: {\n header: WriteHeader;\n plan: DocumentSyncPlan;\n}): Promise<void> {\n const { header, plan } = input;\n const nonceDomainHash = await computeContentRecordNonceDomainHash({\n version: 1,\n organizationId: plan.organizationId,\n objectKind: \"document\",\n objectId: plan.documentId,\n contentKeyEpoch: header.contentKeyEpoch,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n contentRecordId: header.contentRecordId,\n });\n if (nonceDomainHash !== header.nonceDomainHash) {\n throw new Error(\"Document sync response nonce domain mismatch\");\n }\n}\n\nasync function assertDocumentSyncResponseWriteHeaderSignature(input: {\n contentKeyBundle: DocumentSyncResponse[\"contentKeyBundle\"];\n header: WriteHeader;\n plan: DocumentSyncPlan;\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n update: DocumentSyncResponse[\"updates\"][number];\n}): Promise<void> {\n const { header, plan, update } = input;\n const writerPublicKey = await input.resolveWriterPublicKey({\n writerSigningKeyFingerprint: header.writerKeyFingerprint,\n writerUserId: header.writerUserId,\n });\n if (!writerPublicKey) {\n throw new Error(\"Document sync response writer public key missing\");\n }\n\n const documentAuthorization = await documentWriteAuthorizationForHeader({\n allowMissingAuthorization: isAcceptedOutgoingSyncUpdate(plan, update),\n authorizationTargets: update.authorizationTargets,\n dependencyManifestHashes: header.dependencyManifestHashes,\n contentKeyBundle: input.contentKeyBundle,\n manifestHash: header.accessManifestHash,\n plan,\n targetHash: header.targetHash,\n });\n const verified = await verifyWriteHeader({\n authorizationMembership: \"referenced\",\n expectedAccessManifestHash: documentAuthorization\n ? header.accessManifestHash\n : plan.expectedLinkSetManifestHash,\n expectedObject: {\n objectKind: \"document\",\n objectId: plan.documentId,\n organizationId: plan.organizationId,\n },\n header,\n ...(documentAuthorization ? { documentAuthorization } : {}),\n expectedTargetHash: documentAuthorization\n ? header.targetHash\n : plan.expectedTargetHash,\n writerPublicKey,\n });\n if (!verified.ok) {\n throw verified.error;\n }\n}\n\nfunction assertDocumentSyncCommitCheckpointMatchesPlan(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n if (\n response.commitLsnMode === \"untracked\" &&\n response.commitLsn !== UNTRACKED_COMMIT_LSN\n ) {\n throw new Error(\n \"Document sync response untracked commit LSN must use the 0/0 sentinel\",\n );\n }\n if (\n response.commitLsn === UNTRACKED_COMMIT_LSN &&\n response.commitLsnMode !== \"untracked\"\n ) {\n throw new Error(\n \"Document sync response 0/0 commit LSN must be declared untracked\",\n );\n }\n if (response.commitLsn !== null) {\n parseWalLsn(response.commitLsn, \"Document sync response commit LSN\");\n }\n if (plan.minLsn === undefined) {\n return;\n }\n const minLsn = parseWalLsn(\n plan.minLsn,\n \"Document sync requested minimum LSN\",\n );\n if (response.commitLsn === null) {\n throw new Error(\"Document sync response commit LSN is missing\");\n }\n if (\n !(\n response.commitLsn === UNTRACKED_COMMIT_LSN &&\n response.commitLsnMode === \"untracked\"\n ) &&\n parseWalLsn(response.commitLsn, \"Document sync response commit LSN\") <\n minLsn\n ) {\n throw new Error(\"Document sync response commit LSN is stale\");\n }\n}\n\nfunction documentSyncManifestEpoch(plan: DocumentSyncPlan): number {\n if (!isPlainRecord(plan.documentManifest.state)) {\n throw new Error(\"Document sync manifest state is invalid\");\n }\n\n return readRecordPositiveInteger(\n plan.documentManifest.state,\n \"epoch\",\n \"Document sync manifest state\",\n );\n}\n\n// The one load-bearing epoch check: no returned update may claim an\n// accessEpoch newer than the current manifest epoch.\nfunction assertDocumentSyncAccessEpochsNotInFuture(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n const currentAccessEpoch = documentSyncManifestEpoch(plan);\n for (const update of response.updates) {\n if (update.accessEpoch > currentAccessEpoch) {\n throw new Error(\"Document sync response includes a future access epoch\");\n }\n }\n}\n\nfunction contentKeyBundleCanonical(\n bundle: DocumentSyncResponse[\"contentKeyBundle\"],\n): string {\n return serializeCanonical(bundle, \"content-key bundle\");\n}\n\nfunction documentSyncContentKeyBundlesByEpoch(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): ReadonlyMap<number, DocumentSyncResponse[\"contentKeyBundle\"]> {\n if (\n contentKeyBundleCanonical(response.contentKeyBundle) !==\n contentKeyBundleCanonical(plan.sourceContentKeyBundle)\n ) {\n throw new Error(\"Document sync response content-key bundle mismatch\");\n }\n\n const bundleByEpoch = new Map<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >();\n const bundles = [response.contentKeyBundle, ...response.contentKeyBundles];\n\n for (const bundle of bundles) {\n if (bundle.documentId !== plan.documentId) {\n throw new Error(\n \"Document sync response content-key bundle document mismatch\",\n );\n }\n if (\n !Number.isInteger(bundle.contentKeyEpoch) ||\n bundle.contentKeyEpoch <= 0\n ) {\n throw new Error(\n \"Document sync response content-key bundle epoch mismatch\",\n );\n }\n\n const existing = bundleByEpoch.get(bundle.contentKeyEpoch);\n if (!existing) {\n bundleByEpoch.set(bundle.contentKeyEpoch, bundle);\n continue;\n }\n if (\n contentKeyBundleCanonical(existing) !== contentKeyBundleCanonical(bundle)\n ) {\n throw new Error(\"Document sync response content-key bundle conflict\");\n }\n }\n\n return bundleByEpoch;\n}\n\nasync function persistedDocumentSyncStateFromResponseInternal(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n options: {\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n },\n): Promise<PersistedDocumentSyncState> {\n if (response.documentId !== plan.documentId) {\n throw new Error(\"Document sync response document id mismatch\");\n }\n const contentKeyBundlesByEpoch = documentSyncContentKeyBundlesByEpoch(\n plan,\n response,\n );\n if (\n serializeCanonical(response.documentKekTargets, \"KEK targets\") !==\n serializeCanonical(plan.documentKekTargets, \"KEK targets\")\n ) {\n throw new Error(\"Document sync response KEK target mismatch\");\n }\n assertAcceptedOutgoingUpdateIdsMatchPlan(plan, response);\n assertDocumentSyncCommitCheckpointMatchesPlan(plan, response);\n assertDocumentSyncAccessEpochsNotInFuture(plan, response);\n\n await Promise.all(\n response.updates.map((update) =>\n assertDocumentSyncResponseUpdateMatchesPlan({\n contentKeyBundlesByEpoch,\n plan,\n resolveWriterPublicKey: options.resolveWriterPublicKey,\n update,\n }),\n ),\n );\n\n return serializedPersistedDocumentState({\n contentKeyBundle: response.contentKeyBundle,\n documentId: plan.documentId,\n documentKekTargets: response.documentKekTargets,\n documentManifestBundle: plan.documentManifest,\n });\n}\n\nexport async function persistedDocumentSyncStateFromResponse(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n options: {\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n },\n): Promise<PersistedDocumentSyncState> {\n try {\n return await persistedDocumentSyncStateFromResponseInternal(\n plan,\n response,\n options,\n );\n } catch (error) {\n if (error instanceof DocumentSyncResponseUpdateContentKeyError) throw error;\n throwKeyingVerificationShapeFailure(error);\n }\n}\n\nexport function isRetryableDocumentSyncConflict(\n failure: DocumentSyncSubmitFailure,\n): boolean {\n return (\n failure.status === 409 &&\n failure.code === DOCUMENT_SYNC_ERROR_CODES.stateStale\n );\n}\n\n// The wipe gate is deliberately strict — coded 404s only, which is the right\n// failure mode for a destructive action. A bare 404 proves nothing about the\n// document: proxy/tunnel error pages, deploy-skew route misses, and\n// container-level lookups all produce one.\nexport function isUpstreamDeletedDocumentSyncFailure(\n failure: DocumentSyncSubmitFailure,\n): boolean {\n return (\n failure.status === 404 && failure.code === DOCUMENT_NOT_FOUND_ERROR_CODE\n );\n}\n"]}
1
+ {"version":3,"file":"syncResponses.js","sourceRoot":"","sources":["syncResponses.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CONTENT_RECORD_ENCRYPTION_SUITE,\n computeContentRecordNonceDomainHash,\n computeDocumentContentRecordCiphertextHash,\n computeDocumentContentRecordMetadataHash,\n verifyWriteHeader,\n type WriteHeader,\n} from \"@tearleads/crypto\";\nimport { isPlainObject as isPlainRecord } from \"@tearleads/validators/isPlainObject\";\nimport {\n DOCUMENT_NOT_FOUND_ERROR_CODE,\n DOCUMENT_SYNC_ERROR_CODES,\n type DocumentSyncResponse,\n} from \"@tearleads/validators/response\";\nimport { parseWalLsn } from \"@tearleads/validators/util\";\nimport { throwKeyingVerificationShapeFailure } from \"../../keyingProjectionVerification/error\";\nimport {\n readRecordPositiveInteger,\n readRecordString,\n readWriteHeader,\n serializeCanonical,\n serializedPersistedDocumentState,\n} from \"./readers\";\n\nexport { submitDocumentSync } from \"./syncPagination\";\n\nimport { documentWriteAuthorizationForHeader } from \"./syncResponseAuthorization\";\nimport type {\n DocumentSyncPlan,\n DocumentSyncSubmitFailure,\n DocumentWriterPublicKeyResolver,\n PersistedDocumentSyncState,\n} from \"./types\";\n\n// Remote-primary databases without a portable WAL position use the zero LSN\n// as an explicit \"checkpoint not tracked\" sentinel. It is accepted only when\n// the response declares that mode, letting a local record carrying a checkpoint\n// from another backend move to that primary without weakening tracked checks.\n// This protects against replica lag, not a server that falsely declares its own\n// consistency capability; the authenticated server remains the source of data.\nconst UNTRACKED_COMMIT_LSN = \"0/0\";\n\nexport class DocumentSyncResponseUpdateContentKeyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DocumentSyncResponseUpdateContentKeyError\";\n }\n}\n\nfunction assertAcceptedOutgoingUpdateIdsMatchPlan(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n const expected = plan.request.outgoingUpdates.map((update) => update.id);\n const accepted = response.acceptedOutgoingUpdateIds;\n const expectedSorted = [...expected].sort();\n const acceptedSorted = [...accepted].sort();\n if (\n expectedSorted.length !== acceptedSorted.length ||\n expectedSorted.some((id, index) => id !== acceptedSorted[index])\n ) {\n throw new Error(\"Document sync response accepted update mismatch\");\n }\n}\n\nasync function assertDocumentSyncResponseUpdateMatchesPlan(input: {\n contentKeyBundlesByEpoch: ReadonlyMap<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >;\n plan: DocumentSyncPlan;\n update: DocumentSyncResponse[\"updates\"][number];\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n}): Promise<void> {\n const { plan, update } = input;\n if (update.documentId !== plan.documentId) {\n throw new Error(\"Document sync response update document mismatch\");\n }\n const header = readWriteHeader(\n update.writeHeader,\n \"Document sync response write header\",\n );\n await assertDocumentSyncResponseUpdateHashes({ update });\n const contentKeyBundle = assertDocumentSyncResponseUpdateContentKeyBundle({\n contentKeyBundlesByEpoch: input.contentKeyBundlesByEpoch,\n header,\n plan,\n });\n assertDocumentSyncResponseWriteHeaderFields({ header, plan, update });\n await assertDocumentSyncResponseNonceDomain({ header, plan });\n await assertDocumentSyncResponseWriteHeaderSignature({\n header,\n contentKeyBundle,\n plan,\n resolveWriterPublicKey: input.resolveWriterPublicKey,\n update,\n });\n}\n\nfunction assertDocumentSyncResponseUpdateContentKeyBundle(input: {\n contentKeyBundlesByEpoch: ReadonlyMap<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >;\n header: WriteHeader;\n plan: DocumentSyncPlan;\n}): DocumentSyncResponse[\"contentKeyBundle\"] {\n const { header, plan } = input;\n if (header.contentKeyEpoch > plan.contentKeyEpoch) {\n throw new DocumentSyncResponseUpdateContentKeyError(\n \"Document sync response includes a future content-key epoch\",\n );\n }\n\n const bundle = input.contentKeyBundlesByEpoch.get(header.contentKeyEpoch);\n if (!bundle) {\n throw new DocumentSyncResponseUpdateContentKeyError(\n \"Document sync response content-key bundle missing\",\n );\n }\n if (\n bundle.documentId !== plan.documentId ||\n bundle.contentKeyEpoch !== header.contentKeyEpoch\n ) {\n throw new Error(\"Document sync response content-key bundle mismatch\");\n }\n return bundle;\n}\n\nasync function assertDocumentSyncResponseUpdateHashes(input: {\n update: DocumentSyncResponse[\"updates\"][number];\n}): Promise<void> {\n const { update } = input;\n const ciphertextHash = await computeDocumentContentRecordCiphertextHash(\n update.encryptedData,\n );\n if (\n ciphertextHash !==\n readRecordString(update.writeHeader, \"ciphertextHash\", \"write header\")\n ) {\n throw new Error(\"Document sync response ciphertext hash mismatch\");\n }\n const metadataHash = await computeDocumentContentRecordMetadataHash({\n ...(update.checkpointKind === undefined\n ? {}\n : { checkpointKind: update.checkpointKind }),\n ...(update.checkpointPayloadKind === undefined\n ? {}\n : { checkpointPayloadKind: update.checkpointPayloadKind }),\n documentId: update.documentId,\n partialEndVersionVector: update.partialEndVersionVector,\n partialStartVersionVector: update.partialStartVersionVector,\n plaintextHash: update.plaintextHash,\n ...(update.sourceVersionVector === undefined\n ? {}\n : { sourceVersionVector: update.sourceVersionVector }),\n updateId: update.id,\n });\n if (\n metadataHash !==\n readRecordString(update.writeHeader, \"metadataHash\", \"write header\")\n ) {\n throw new Error(\"Document sync response metadata hash mismatch\");\n }\n}\n\nfunction assertDocumentSyncResponseWriteHeaderFields(input: {\n header: WriteHeader;\n plan: DocumentSyncPlan;\n update: DocumentSyncResponse[\"updates\"][number];\n}): void {\n const { header, plan, update } = input;\n const mustMatchCurrentBoundary = isAcceptedOutgoingSyncUpdate(plan, update);\n\n if (\n header.version !== 1 ||\n header.objectKind !== \"document\" ||\n header.objectId !== plan.documentId ||\n header.organizationId !== plan.organizationId ||\n header.encryptionSuite !== CONTENT_RECORD_ENCRYPTION_SUITE ||\n header.writerKeyFingerprint !== update.authorFingerprint ||\n (mustMatchCurrentBoundary &&\n (header.contentKeyEpoch !== plan.contentKeyEpoch ||\n header.accessManifestHash !== plan.expectedLinkSetManifestHash ||\n header.targetHash !== plan.expectedTargetHash))\n ) {\n throw new Error(\"Document sync response write header mismatch\");\n }\n}\n\nfunction isAcceptedOutgoingSyncUpdate(\n plan: DocumentSyncPlan,\n update: DocumentSyncResponse[\"updates\"][number],\n): boolean {\n return plan.request.outgoingUpdates.some(\n (outgoingUpdate) => outgoingUpdate.id === update.id,\n );\n}\n\nasync function assertDocumentSyncResponseNonceDomain(input: {\n header: WriteHeader;\n plan: DocumentSyncPlan;\n}): Promise<void> {\n const { header, plan } = input;\n const nonceDomainHash = await computeContentRecordNonceDomainHash({\n version: 1,\n organizationId: plan.organizationId,\n objectKind: \"document\",\n objectId: plan.documentId,\n contentKeyEpoch: header.contentKeyEpoch,\n encryptionSuite: CONTENT_RECORD_ENCRYPTION_SUITE,\n contentRecordId: header.contentRecordId,\n });\n if (nonceDomainHash !== header.nonceDomainHash) {\n throw new Error(\"Document sync response nonce domain mismatch\");\n }\n}\n\nasync function assertDocumentSyncResponseWriteHeaderSignature(input: {\n contentKeyBundle: DocumentSyncResponse[\"contentKeyBundle\"];\n header: WriteHeader;\n plan: DocumentSyncPlan;\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n update: DocumentSyncResponse[\"updates\"][number];\n}): Promise<void> {\n const { header, plan, update } = input;\n const writerPublicKey = await input.resolveWriterPublicKey({\n writerSigningKeyFingerprint: header.writerKeyFingerprint,\n writerUserId: header.writerUserId,\n });\n if (!writerPublicKey) {\n throw new Error(\"Document sync response writer public key missing\");\n }\n\n const documentAuthorization = await documentWriteAuthorizationForHeader({\n allowMissingAuthorization: isAcceptedOutgoingSyncUpdate(plan, update),\n authorizationTargets: update.authorizationTargets,\n dependencyManifestHashes: header.dependencyManifestHashes,\n contentKeyBundle: input.contentKeyBundle,\n manifestHash: header.accessManifestHash,\n plan,\n targetHash: header.targetHash,\n });\n const verified = await verifyWriteHeader({\n authorizationMembership: \"referenced\",\n expectedAccessManifestHash: documentAuthorization\n ? header.accessManifestHash\n : plan.expectedLinkSetManifestHash,\n expectedObject: {\n objectKind: \"document\",\n objectId: plan.documentId,\n organizationId: plan.organizationId,\n },\n header,\n ...(documentAuthorization ? { documentAuthorization } : {}),\n expectedTargetHash: documentAuthorization\n ? header.targetHash\n : plan.expectedTargetHash,\n writerPublicKey,\n });\n if (!verified.ok) {\n throw verified.error;\n }\n}\n\nfunction assertDocumentSyncCommitCheckpointMatchesPlan(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n if (\n response.commitLsnMode === \"untracked\" &&\n response.commitLsn !== UNTRACKED_COMMIT_LSN\n ) {\n throw new Error(\n \"Document sync response untracked commit LSN must use the 0/0 sentinel\",\n );\n }\n if (\n response.commitLsn === UNTRACKED_COMMIT_LSN &&\n response.commitLsnMode !== \"untracked\"\n ) {\n throw new Error(\n \"Document sync response 0/0 commit LSN must be declared untracked\",\n );\n }\n if (response.commitLsn !== null) {\n parseWalLsn(response.commitLsn, \"Document sync response commit LSN\");\n }\n if (plan.minLsn === undefined) {\n return;\n }\n const minLsn = parseWalLsn(\n plan.minLsn,\n \"Document sync requested minimum LSN\",\n );\n if (response.commitLsn === null) {\n throw new Error(\"Document sync response commit LSN is missing\");\n }\n if (\n !(\n response.commitLsn === UNTRACKED_COMMIT_LSN &&\n response.commitLsnMode === \"untracked\"\n ) &&\n parseWalLsn(response.commitLsn, \"Document sync response commit LSN\") <\n minLsn\n ) {\n throw new Error(\"Document sync response commit LSN is stale\");\n }\n}\n\nfunction documentSyncManifestEpoch(plan: DocumentSyncPlan): number {\n if (!isPlainRecord(plan.documentManifest.state)) {\n throw new Error(\"Document sync manifest state is invalid\");\n }\n\n return readRecordPositiveInteger(\n plan.documentManifest.state,\n \"epoch\",\n \"Document sync manifest state\",\n );\n}\n\n// The one load-bearing epoch check: no returned update may claim an\n// accessEpoch newer than the current manifest epoch.\nfunction assertDocumentSyncAccessEpochsNotInFuture(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): void {\n const currentAccessEpoch = documentSyncManifestEpoch(plan);\n for (const update of response.updates) {\n if (update.accessEpoch > currentAccessEpoch) {\n throw new Error(\"Document sync response includes a future access epoch\");\n }\n }\n}\n\nfunction contentKeyBundleCanonical(\n bundle: DocumentSyncResponse[\"contentKeyBundle\"],\n): string {\n return serializeCanonical(bundle, \"content-key bundle\");\n}\n\nfunction documentSyncContentKeyBundlesByEpoch(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n): ReadonlyMap<number, DocumentSyncResponse[\"contentKeyBundle\"]> {\n if (\n contentKeyBundleCanonical(response.contentKeyBundle) !==\n contentKeyBundleCanonical(plan.sourceContentKeyBundle)\n ) {\n throw new Error(\"Document sync response content-key bundle mismatch\");\n }\n\n const bundleByEpoch = new Map<\n number,\n DocumentSyncResponse[\"contentKeyBundle\"]\n >();\n const bundles = [response.contentKeyBundle, ...response.contentKeyBundles];\n\n for (const bundle of bundles) {\n if (bundle.documentId !== plan.documentId) {\n throw new Error(\n \"Document sync response content-key bundle document mismatch\",\n );\n }\n if (\n !Number.isInteger(bundle.contentKeyEpoch) ||\n bundle.contentKeyEpoch <= 0\n ) {\n throw new Error(\n \"Document sync response content-key bundle epoch mismatch\",\n );\n }\n\n // The current epoch travels only as contentKeyBundle; the array carries\n // each older epoch once.\n if (bundleByEpoch.has(bundle.contentKeyEpoch)) {\n throw new Error(\"Document sync response repeats a content-key epoch\");\n }\n bundleByEpoch.set(bundle.contentKeyEpoch, bundle);\n }\n\n return bundleByEpoch;\n}\n\nasync function persistedDocumentSyncStateFromResponseInternal(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n options: {\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n },\n): Promise<PersistedDocumentSyncState> {\n if (response.documentId !== plan.documentId) {\n throw new Error(\"Document sync response document id mismatch\");\n }\n const contentKeyBundlesByEpoch = documentSyncContentKeyBundlesByEpoch(\n plan,\n response,\n );\n if (\n serializeCanonical(response.documentKekTargets, \"KEK targets\") !==\n serializeCanonical(plan.documentKekTargets, \"KEK targets\")\n ) {\n throw new Error(\"Document sync response KEK target mismatch\");\n }\n assertAcceptedOutgoingUpdateIdsMatchPlan(plan, response);\n assertDocumentSyncCommitCheckpointMatchesPlan(plan, response);\n assertDocumentSyncAccessEpochsNotInFuture(plan, response);\n\n await Promise.all(\n response.updates.map((update) =>\n assertDocumentSyncResponseUpdateMatchesPlan({\n contentKeyBundlesByEpoch,\n plan,\n resolveWriterPublicKey: options.resolveWriterPublicKey,\n update,\n }),\n ),\n );\n\n return serializedPersistedDocumentState({\n contentKeyBundle: response.contentKeyBundle,\n documentId: plan.documentId,\n documentKekTargets: response.documentKekTargets,\n documentManifestBundle: plan.documentManifest,\n });\n}\n\nexport async function persistedDocumentSyncStateFromResponse(\n plan: DocumentSyncPlan,\n response: DocumentSyncResponse,\n options: {\n resolveWriterPublicKey: DocumentWriterPublicKeyResolver;\n },\n): Promise<PersistedDocumentSyncState> {\n try {\n return await persistedDocumentSyncStateFromResponseInternal(\n plan,\n response,\n options,\n );\n } catch (error) {\n if (error instanceof DocumentSyncResponseUpdateContentKeyError) throw error;\n throwKeyingVerificationShapeFailure(error);\n }\n}\n\nexport function isRetryableDocumentSyncConflict(\n failure: DocumentSyncSubmitFailure,\n): boolean {\n return (\n failure.status === 409 &&\n failure.code === DOCUMENT_SYNC_ERROR_CODES.stateStale\n );\n}\n\n// The wipe gate is deliberately strict — coded 404s only, which is the right\n// failure mode for a destructive action. A bare 404 proves nothing about the\n// document: proxy/tunnel error pages, deploy-skew route misses, and\n// container-level lookups all produce one.\nexport function isUpstreamDeletedDocumentSyncFailure(\n failure: DocumentSyncSubmitFailure,\n): boolean {\n return (\n failure.status === 404 && failure.code === DOCUMENT_NOT_FOUND_ERROR_CODE\n );\n}\n"]}
@@ -1,8 +1,9 @@
1
1
  import { isOrganizationContainerGrantSubjectType, isOrganizationGroupContainerAccessLevel, } from "../../../internal/validators/response/index.js";
2
2
  import { asc, eq } from "drizzle-orm";
3
3
  import { organizationReadModelContainerGrants } from "../../sqlite/organizationReadModelSchema.js";
4
+ import { sqliteInsertBatchSize } from "../../sqlite/sqliteBindBudget.js";
4
5
  import { OrganizationReadModelIntegrityError } from "./organizationReadModelProtocol.js";
5
- const GRANT_INSERT_BATCH_SIZE = 40;
6
+ const GRANT_INSERT_BATCH_SIZE = sqliteInsertBatchSize(organizationReadModelContainerGrants);
6
7
  function grantIdentity(grant) {
7
8
  return `${grant.containerId}\0${grant.subjectType}\0${grant.subjectId}`;
8
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"organizationReadModelGrantPersistence.js","sourceRoot":"","sources":["organizationReadModelGrantPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n OrganizationContainerGrantResponse,\n OrganizationContainerGrantsResponse,\n} from \"@tearleads/validators/response\";\nimport {\n isOrganizationContainerGrantSubjectType,\n isOrganizationGroupContainerAccessLevel,\n} from \"@tearleads/validators/response\";\nimport { asc, eq } from \"drizzle-orm\";\nimport { organizationReadModelContainerGrants } from \"../../sqlite/organizationReadModelSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { OrganizationReadModelIntegrityError } from \"./organizationReadModelProtocol\";\n\nconst GRANT_INSERT_BATCH_SIZE = 40;\n\ntype SelectedGrant = typeof organizationReadModelContainerGrants.$inferSelect;\n\nfunction grantIdentity(\n grant: Pick<\n OrganizationContainerGrantResponse,\n \"containerId\" | \"subjectId\" | \"subjectType\"\n >,\n): string {\n return `${grant.containerId}\\0${grant.subjectType}\\0${grant.subjectId}`;\n}\n\nfunction hasValidGrantSubjectBinding(\n grant: OrganizationContainerGrantResponse,\n): boolean {\n const validUser =\n grant.subjectType === \"user\" &&\n grant.userId === grant.subjectId &&\n grant.groupId === null;\n const validGroup =\n grant.subjectType === \"group\" &&\n grant.userId === null &&\n grant.signingKeyFingerprint === null &&\n grant.groupId === grant.subjectId;\n return validUser || validGroup;\n}\n\nfunction assertGrantsLane(input: {\n readonly lane: OrganizationContainerGrantsResponse;\n readonly organizationId: string;\n}): void {\n if (input.lane.organizationId !== input.organizationId) {\n throw new Error(\"Organization read-model grants scope does not match\");\n }\n const identities = input.lane.grants.map(grantIdentity);\n if (new Set(identities).size !== identities.length) {\n throw new Error(\"Organization read-model grants contain duplicates\");\n }\n for (const grant of input.lane.grants) {\n if (!hasValidGrantSubjectBinding(grant)) {\n throw new Error(\n \"Organization read-model grant subject binding is invalid\",\n );\n }\n }\n}\n\nexport async function applyOrganizationReadModelGrantsLane(input: {\n readonly lane: OrganizationContainerGrantsResponse;\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n assertGrantsLane(input);\n await input.tx\n .delete(organizationReadModelContainerGrants)\n .where(\n eq(\n organizationReadModelContainerGrants.organizationId,\n input.organizationId,\n ),\n )\n .run();\n\n const rows = input.lane.grants.map((grant, sortOrder) => ({\n ...grant,\n organizationId: input.organizationId,\n sortOrder,\n }));\n for (let index = 0; index < rows.length; index += GRANT_INSERT_BATCH_SIZE) {\n await input.tx\n .insert(organizationReadModelContainerGrants)\n .values(rows.slice(index, index + GRANT_INSERT_BATCH_SIZE))\n .run();\n }\n}\n\nfunction toGrant(row: SelectedGrant): OrganizationContainerGrantResponse {\n if (!isOrganizationGroupContainerAccessLevel(row.accessLevel)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant access level is invalid\",\n );\n }\n if (!isOrganizationContainerGrantSubjectType(row.subjectType)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant subject type is invalid\",\n );\n }\n const grant: OrganizationContainerGrantResponse = {\n accessLevel: row.accessLevel,\n containerId: row.containerId,\n createdAt: row.createdAt,\n depth: row.depth,\n isBuiltin: row.isBuiltin,\n metadataAccessEpoch: row.metadataAccessEpoch,\n metadataAccessStateHash: row.metadataAccessStateHash,\n metadataDocumentId: row.metadataDocumentId,\n parentId: row.parentId,\n updatedAt: row.updatedAt,\n subjectType: row.subjectType,\n subjectId: row.subjectId,\n userId: row.userId,\n signingKeyFingerprint: row.signingKeyFingerprint,\n groupId: row.groupId,\n };\n if (!hasValidGrantSubjectBinding(grant)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant subject binding is invalid\",\n );\n }\n return grant;\n}\n\nexport async function loadOrganizationReadModelGrantsInTransaction(input: {\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<OrganizationContainerGrantsResponse> {\n const rows = await input.tx\n .select()\n .from(organizationReadModelContainerGrants)\n .where(\n eq(\n organizationReadModelContainerGrants.organizationId,\n input.organizationId,\n ),\n )\n .orderBy(\n asc(organizationReadModelContainerGrants.sortOrder),\n asc(organizationReadModelContainerGrants.containerId),\n asc(organizationReadModelContainerGrants.subjectType),\n asc(organizationReadModelContainerGrants.subjectId),\n );\n return {\n organizationId: input.organizationId,\n grants: rows.map(toGrant),\n };\n}\n"]}
1
+ {"version":3,"file":"organizationReadModelGrantPersistence.js","sourceRoot":"","sources":["organizationReadModelGrantPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n OrganizationContainerGrantResponse,\n OrganizationContainerGrantsResponse,\n} from \"@tearleads/validators/response\";\nimport {\n isOrganizationContainerGrantSubjectType,\n isOrganizationGroupContainerAccessLevel,\n} from \"@tearleads/validators/response\";\nimport { asc, eq } from \"drizzle-orm\";\nimport { organizationReadModelContainerGrants } from \"../../sqlite/organizationReadModelSchema\";\nimport { sqliteInsertBatchSize } from \"../../sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport { OrganizationReadModelIntegrityError } from \"./organizationReadModelProtocol\";\n\nconst GRANT_INSERT_BATCH_SIZE = sqliteInsertBatchSize(\n organizationReadModelContainerGrants,\n);\n\ntype SelectedGrant = typeof organizationReadModelContainerGrants.$inferSelect;\n\nfunction grantIdentity(\n grant: Pick<\n OrganizationContainerGrantResponse,\n \"containerId\" | \"subjectId\" | \"subjectType\"\n >,\n): string {\n return `${grant.containerId}\\0${grant.subjectType}\\0${grant.subjectId}`;\n}\n\nfunction hasValidGrantSubjectBinding(\n grant: OrganizationContainerGrantResponse,\n): boolean {\n const validUser =\n grant.subjectType === \"user\" &&\n grant.userId === grant.subjectId &&\n grant.groupId === null;\n const validGroup =\n grant.subjectType === \"group\" &&\n grant.userId === null &&\n grant.signingKeyFingerprint === null &&\n grant.groupId === grant.subjectId;\n return validUser || validGroup;\n}\n\nfunction assertGrantsLane(input: {\n readonly lane: OrganizationContainerGrantsResponse;\n readonly organizationId: string;\n}): void {\n if (input.lane.organizationId !== input.organizationId) {\n throw new Error(\"Organization read-model grants scope does not match\");\n }\n const identities = input.lane.grants.map(grantIdentity);\n if (new Set(identities).size !== identities.length) {\n throw new Error(\"Organization read-model grants contain duplicates\");\n }\n for (const grant of input.lane.grants) {\n if (!hasValidGrantSubjectBinding(grant)) {\n throw new Error(\n \"Organization read-model grant subject binding is invalid\",\n );\n }\n }\n}\n\nexport async function applyOrganizationReadModelGrantsLane(input: {\n readonly lane: OrganizationContainerGrantsResponse;\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n assertGrantsLane(input);\n await input.tx\n .delete(organizationReadModelContainerGrants)\n .where(\n eq(\n organizationReadModelContainerGrants.organizationId,\n input.organizationId,\n ),\n )\n .run();\n\n const rows = input.lane.grants.map((grant, sortOrder) => ({\n ...grant,\n organizationId: input.organizationId,\n sortOrder,\n }));\n for (let index = 0; index < rows.length; index += GRANT_INSERT_BATCH_SIZE) {\n await input.tx\n .insert(organizationReadModelContainerGrants)\n .values(rows.slice(index, index + GRANT_INSERT_BATCH_SIZE))\n .run();\n }\n}\n\nfunction toGrant(row: SelectedGrant): OrganizationContainerGrantResponse {\n if (!isOrganizationGroupContainerAccessLevel(row.accessLevel)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant access level is invalid\",\n );\n }\n if (!isOrganizationContainerGrantSubjectType(row.subjectType)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant subject type is invalid\",\n );\n }\n const grant: OrganizationContainerGrantResponse = {\n accessLevel: row.accessLevel,\n containerId: row.containerId,\n createdAt: row.createdAt,\n depth: row.depth,\n isBuiltin: row.isBuiltin,\n metadataAccessEpoch: row.metadataAccessEpoch,\n metadataAccessStateHash: row.metadataAccessStateHash,\n metadataDocumentId: row.metadataDocumentId,\n parentId: row.parentId,\n updatedAt: row.updatedAt,\n subjectType: row.subjectType,\n subjectId: row.subjectId,\n userId: row.userId,\n signingKeyFingerprint: row.signingKeyFingerprint,\n groupId: row.groupId,\n };\n if (!hasValidGrantSubjectBinding(grant)) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization grant subject binding is invalid\",\n );\n }\n return grant;\n}\n\nexport async function loadOrganizationReadModelGrantsInTransaction(input: {\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<OrganizationContainerGrantsResponse> {\n const rows = await input.tx\n .select()\n .from(organizationReadModelContainerGrants)\n .where(\n eq(\n organizationReadModelContainerGrants.organizationId,\n input.organizationId,\n ),\n )\n .orderBy(\n asc(organizationReadModelContainerGrants.sortOrder),\n asc(organizationReadModelContainerGrants.containerId),\n asc(organizationReadModelContainerGrants.subjectType),\n asc(organizationReadModelContainerGrants.subjectId),\n );\n return {\n organizationId: input.organizationId,\n grants: rows.map(toGrant),\n };\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { OrganizationReadModelGroupMemberResponse } from "../../../internal/validators/response/index.js";
2
+ import type { ExecSql } from "../../sqlite/sqlSchema.js";
3
+ /** Locally projected members of one group, by user and role. */
4
+ export interface OrganizationReadModelGroupMembers {
5
+ readonly groupId: string;
6
+ readonly members: OrganizationReadModelGroupMemberResponse[];
7
+ readonly organizationId: string;
8
+ }
9
+ export declare function loadOrganizationReadModelGroupMembers(execSql: ExecSql, organizationId: string, groupId: string, currentUserId: string): Promise<OrganizationReadModelGroupMembers | null>;
@@ -1,5 +1,5 @@
1
1
  import { and, asc, eq } from "drizzle-orm";
2
- import { organizationReadModelGroupMembers, organizationReadModelGroupMemberships, organizationReadModelGroups, organizationReadModelRequesters, organizationReadModelState, } from "../../sqlite/organizationReadModelSchema.js";
2
+ import { organizationReadModelGroupMembers, organizationReadModelGroups, organizationReadModelRequesters, organizationReadModelState, } from "../../sqlite/organizationReadModelSchema.js";
3
3
  import { OrganizationReadModelIntegrityError } from "./organizationReadModelProtocol.js";
4
4
  import { loadWithOrganizationReadModelIntegrityPurge } from "./organizationReadModelPurge.js";
5
5
  function requireOrganizationRole(value) {
@@ -15,47 +15,48 @@ function toGroupMember(row, expectedStateHash) {
15
15
  return {
16
16
  userId: row.userId,
17
17
  role: requireOrganizationRole(row.role),
18
- signingKeyFingerprint: row.signingKeyFingerprint,
19
- signingPublicKey: row.signingPublicKey,
20
- encapsulationPublicKey: row.encapsulationPublicKey,
21
- encapsulationKeyFingerprint: row.encapsulationKeyFingerprint,
22
18
  };
23
19
  }
24
20
  async function loadMembershipRows(input) {
25
21
  return input.tx
26
22
  .select({
27
- encapsulationKeyFingerprint: organizationReadModelGroupMembers.encapsulationKeyFingerprint,
28
- encapsulationPublicKey: organizationReadModelGroupMembers.encapsulationPublicKey,
29
23
  userId: organizationReadModelGroupMembers.userId,
30
24
  role: organizationReadModelGroupMembers.role,
31
- signingKeyFingerprint: organizationReadModelGroupMembers.signingKeyFingerprint,
32
- signingPublicKey: organizationReadModelGroupMembers.signingPublicKey,
33
25
  stateHash: organizationReadModelGroupMembers.stateHash,
34
26
  })
35
27
  .from(organizationReadModelGroupMembers)
36
28
  .where(and(eq(organizationReadModelGroupMembers.organizationId, input.organizationId), eq(organizationReadModelGroupMembers.groupId, input.groupId)))
37
29
  .orderBy(asc(organizationReadModelGroupMembers.sortOrder), asc(organizationReadModelGroupMembers.userId));
38
30
  }
39
- async function loadOrganizationReadModelGroupMembersInTransaction(input) {
40
- const [head] = await input.tx
41
- .select({ stateHash: organizationReadModelGroupMemberships.stateHash })
42
- .from(organizationReadModelGroupMemberships)
43
- .where(and(eq(organizationReadModelGroupMemberships.organizationId, input.organizationId), eq(organizationReadModelGroupMemberships.groupId, input.groupId)))
44
- .limit(1);
45
- if (!head) {
46
- return null;
31
+ /** The reserved Members head lives on the state row; others on catalog rows. */
32
+ async function loadMembershipHead(input) {
33
+ if (input.groupId === input.memberGroup.groupId) {
34
+ return { memberCount: null, stateHash: input.memberGroup.stateHash };
47
35
  }
48
- const rows = await loadMembershipRows(input);
49
36
  const [group] = await input.tx
50
37
  .select({
51
38
  memberCount: organizationReadModelGroups.memberCount,
39
+ membersStateHash: organizationReadModelGroups.membersStateHash,
52
40
  stateHash: organizationReadModelGroups.stateHash,
53
41
  })
54
42
  .from(organizationReadModelGroups)
55
43
  .where(and(eq(organizationReadModelGroups.organizationId, input.organizationId), eq(organizationReadModelGroups.groupId, input.groupId)))
56
44
  .limit(1);
57
- if (group &&
58
- (group.stateHash !== head.stateHash || group.memberCount !== rows.length)) {
45
+ if (!group?.membersStateHash) {
46
+ return null;
47
+ }
48
+ if (group.stateHash !== group.membersStateHash) {
49
+ throw new OrganizationReadModelIntegrityError("Stored organization group membership binding is invalid");
50
+ }
51
+ return { memberCount: group.memberCount, stateHash: group.membersStateHash };
52
+ }
53
+ async function loadOrganizationReadModelGroupMembersInTransaction(input) {
54
+ const head = await loadMembershipHead(input);
55
+ if (!head) {
56
+ return null;
57
+ }
58
+ const rows = await loadMembershipRows(input);
59
+ if (head.memberCount !== null && head.memberCount !== rows.length) {
59
60
  throw new OrganizationReadModelIntegrityError("Stored organization group membership binding is invalid");
60
61
  }
61
62
  return {
@@ -70,7 +71,10 @@ export async function loadOrganizationReadModelGroupMembers(execSql, organizatio
70
71
  organizationId,
71
72
  load: async (tx) => {
72
73
  const [state] = await tx
73
- .select({ organizationId: organizationReadModelState.organizationId })
74
+ .select({
75
+ memberGroupId: organizationReadModelState.memberGroupId,
76
+ memberGroupStateHash: organizationReadModelState.memberGroupStateHash,
77
+ })
74
78
  .from(organizationReadModelState)
75
79
  .where(eq(organizationReadModelState.organizationId, organizationId))
76
80
  .limit(1);
@@ -87,6 +91,10 @@ export async function loadOrganizationReadModelGroupMembers(execSql, organizatio
87
91
  }
88
92
  return loadOrganizationReadModelGroupMembersInTransaction({
89
93
  groupId,
94
+ memberGroup: {
95
+ groupId: state.memberGroupId,
96
+ stateHash: state.memberGroupStateHash,
97
+ },
90
98
  organizationId,
91
99
  tx,
92
100
  });
@@ -1 +1 @@
1
- {"version":3,"file":"organizationReadModelMemberLoad.js","sourceRoot":"","sources":["organizationReadModelMemberLoad.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n OrganizationGroupMemberResponse,\n OrganizationGroupMembersResponse,\n OrganizationRole,\n} from \"@tearleads/validators/response\";\nimport { and, asc, eq } from \"drizzle-orm\";\nimport {\n organizationReadModelGroupMembers,\n organizationReadModelGroupMemberships,\n organizationReadModelGroups,\n organizationReadModelRequesters,\n organizationReadModelState,\n} from \"../../sqlite/organizationReadModelSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport { OrganizationReadModelIntegrityError } from \"./organizationReadModelProtocol\";\nimport { loadWithOrganizationReadModelIntegrityPurge } from \"./organizationReadModelPurge\";\n\ninterface SelectedGroupMember {\n readonly encapsulationKeyFingerprint: string | null;\n readonly encapsulationPublicKey: string | null;\n readonly userId: string;\n readonly role: string;\n readonly signingKeyFingerprint: string | null;\n readonly signingPublicKey: string | null;\n readonly stateHash: string;\n}\n\nfunction requireOrganizationRole(value: string): OrganizationRole {\n if (value !== \"member\" && value !== \"admin\") {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group member role is invalid\",\n );\n }\n return value;\n}\n\nfunction toGroupMember(\n row: SelectedGroupMember,\n expectedStateHash: string,\n): OrganizationGroupMemberResponse {\n if (row.stateHash !== expectedStateHash) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group membership state is inconsistent\",\n );\n }\n return {\n userId: row.userId,\n role: requireOrganizationRole(row.role),\n signingKeyFingerprint: row.signingKeyFingerprint,\n signingPublicKey: row.signingPublicKey,\n encapsulationPublicKey: row.encapsulationPublicKey,\n encapsulationKeyFingerprint: row.encapsulationKeyFingerprint,\n };\n}\n\nasync function loadMembershipRows(input: {\n readonly groupId: string;\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<SelectedGroupMember[]> {\n return input.tx\n .select({\n encapsulationKeyFingerprint:\n organizationReadModelGroupMembers.encapsulationKeyFingerprint,\n encapsulationPublicKey:\n organizationReadModelGroupMembers.encapsulationPublicKey,\n userId: organizationReadModelGroupMembers.userId,\n role: organizationReadModelGroupMembers.role,\n signingKeyFingerprint:\n organizationReadModelGroupMembers.signingKeyFingerprint,\n signingPublicKey: organizationReadModelGroupMembers.signingPublicKey,\n stateHash: organizationReadModelGroupMembers.stateHash,\n })\n .from(organizationReadModelGroupMembers)\n .where(\n and(\n eq(\n organizationReadModelGroupMembers.organizationId,\n input.organizationId,\n ),\n eq(organizationReadModelGroupMembers.groupId, input.groupId),\n ),\n )\n .orderBy(\n asc(organizationReadModelGroupMembers.sortOrder),\n asc(organizationReadModelGroupMembers.userId),\n );\n}\n\nasync function loadOrganizationReadModelGroupMembersInTransaction(input: {\n readonly groupId: string;\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<OrganizationGroupMembersResponse | null> {\n const [head] = await input.tx\n .select({ stateHash: organizationReadModelGroupMemberships.stateHash })\n .from(organizationReadModelGroupMemberships)\n .where(\n and(\n eq(\n organizationReadModelGroupMemberships.organizationId,\n input.organizationId,\n ),\n eq(organizationReadModelGroupMemberships.groupId, input.groupId),\n ),\n )\n .limit(1);\n if (!head) {\n return null;\n }\n\n const rows = await loadMembershipRows(input);\n const [group] = await input.tx\n .select({\n memberCount: organizationReadModelGroups.memberCount,\n stateHash: organizationReadModelGroups.stateHash,\n })\n .from(organizationReadModelGroups)\n .where(\n and(\n eq(organizationReadModelGroups.organizationId, input.organizationId),\n eq(organizationReadModelGroups.groupId, input.groupId),\n ),\n )\n .limit(1);\n if (\n group &&\n (group.stateHash !== head.stateHash || group.memberCount !== rows.length)\n ) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group membership binding is invalid\",\n );\n }\n return {\n organizationId: input.organizationId,\n groupId: input.groupId,\n members: rows.map((row) => toGroupMember(row, head.stateHash)),\n };\n}\n\nexport async function loadOrganizationReadModelGroupMembers(\n execSql: ExecSql,\n organizationId: string,\n groupId: string,\n currentUserId: string,\n): Promise<OrganizationGroupMembersResponse | null> {\n return loadWithOrganizationReadModelIntegrityPurge({\n execSql,\n organizationId,\n load: async (tx) => {\n const [state] = await tx\n .select({ organizationId: organizationReadModelState.organizationId })\n .from(organizationReadModelState)\n .where(eq(organizationReadModelState.organizationId, organizationId))\n .limit(1);\n if (!state) {\n return null;\n }\n const [requester] = await tx\n .select({ userId: organizationReadModelRequesters.userId })\n .from(organizationReadModelRequesters)\n .where(\n and(\n eq(organizationReadModelRequesters.organizationId, organizationId),\n eq(organizationReadModelRequesters.userId, currentUserId),\n ),\n )\n .limit(1);\n if (!requester) {\n return null;\n }\n return loadOrganizationReadModelGroupMembersInTransaction({\n groupId,\n organizationId,\n tx,\n });\n },\n });\n}\n"]}
1
+ {"version":3,"file":"organizationReadModelMemberLoad.js","sourceRoot":"","sources":["organizationReadModelMemberLoad.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n OrganizationReadModelGroupMemberResponse,\n OrganizationRole,\n} from \"@tearleads/validators/response\";\nimport { and, asc, eq } from \"drizzle-orm\";\nimport {\n organizationReadModelGroupMembers,\n organizationReadModelGroups,\n organizationReadModelRequesters,\n organizationReadModelState,\n} from \"../../sqlite/organizationReadModelSchema\";\nimport type { ClientSQLiteTransactionScope } from \"../../sqlite/sqlitePersistenceRuntime\";\nimport type { ExecSql } from \"../../sqlite/sqlSchema\";\nimport { OrganizationReadModelIntegrityError } from \"./organizationReadModelProtocol\";\nimport { loadWithOrganizationReadModelIntegrityPurge } from \"./organizationReadModelPurge\";\n\ninterface SelectedGroupMember {\n readonly userId: string;\n readonly role: string;\n readonly stateHash: string;\n}\n\n/** Locally projected members of one group, by user and role. */\nexport interface OrganizationReadModelGroupMembers {\n readonly groupId: string;\n readonly members: OrganizationReadModelGroupMemberResponse[];\n readonly organizationId: string;\n}\n\nfunction requireOrganizationRole(value: string): OrganizationRole {\n if (value !== \"member\" && value !== \"admin\") {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group member role is invalid\",\n );\n }\n return value;\n}\n\nfunction toGroupMember(\n row: SelectedGroupMember,\n expectedStateHash: string,\n): OrganizationReadModelGroupMemberResponse {\n if (row.stateHash !== expectedStateHash) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group membership state is inconsistent\",\n );\n }\n return {\n userId: row.userId,\n role: requireOrganizationRole(row.role),\n };\n}\n\nasync function loadMembershipRows(input: {\n readonly groupId: string;\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<SelectedGroupMember[]> {\n return input.tx\n .select({\n userId: organizationReadModelGroupMembers.userId,\n role: organizationReadModelGroupMembers.role,\n stateHash: organizationReadModelGroupMembers.stateHash,\n })\n .from(organizationReadModelGroupMembers)\n .where(\n and(\n eq(\n organizationReadModelGroupMembers.organizationId,\n input.organizationId,\n ),\n eq(organizationReadModelGroupMembers.groupId, input.groupId),\n ),\n )\n .orderBy(\n asc(organizationReadModelGroupMembers.sortOrder),\n asc(organizationReadModelGroupMembers.userId),\n );\n}\n\ninterface StoredMembershipHead {\n readonly memberCount: number | null;\n readonly stateHash: string;\n}\n\n/** The reserved Members head lives on the state row; others on catalog rows. */\nasync function loadMembershipHead(input: {\n readonly groupId: string;\n readonly memberGroup: {\n readonly groupId: string;\n readonly stateHash: string;\n };\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<StoredMembershipHead | null> {\n if (input.groupId === input.memberGroup.groupId) {\n return { memberCount: null, stateHash: input.memberGroup.stateHash };\n }\n const [group] = await input.tx\n .select({\n memberCount: organizationReadModelGroups.memberCount,\n membersStateHash: organizationReadModelGroups.membersStateHash,\n stateHash: organizationReadModelGroups.stateHash,\n })\n .from(organizationReadModelGroups)\n .where(\n and(\n eq(organizationReadModelGroups.organizationId, input.organizationId),\n eq(organizationReadModelGroups.groupId, input.groupId),\n ),\n )\n .limit(1);\n if (!group?.membersStateHash) {\n return null;\n }\n if (group.stateHash !== group.membersStateHash) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group membership binding is invalid\",\n );\n }\n return { memberCount: group.memberCount, stateHash: group.membersStateHash };\n}\n\nasync function loadOrganizationReadModelGroupMembersInTransaction(input: {\n readonly groupId: string;\n readonly memberGroup: {\n readonly groupId: string;\n readonly stateHash: string;\n };\n readonly organizationId: string;\n readonly tx: ClientSQLiteTransactionScope;\n}): Promise<OrganizationReadModelGroupMembers | null> {\n const head = await loadMembershipHead(input);\n if (!head) {\n return null;\n }\n const rows = await loadMembershipRows(input);\n if (head.memberCount !== null && head.memberCount !== rows.length) {\n throw new OrganizationReadModelIntegrityError(\n \"Stored organization group membership binding is invalid\",\n );\n }\n return {\n organizationId: input.organizationId,\n groupId: input.groupId,\n members: rows.map((row) => toGroupMember(row, head.stateHash)),\n };\n}\n\nexport async function loadOrganizationReadModelGroupMembers(\n execSql: ExecSql,\n organizationId: string,\n groupId: string,\n currentUserId: string,\n): Promise<OrganizationReadModelGroupMembers | null> {\n return loadWithOrganizationReadModelIntegrityPurge({\n execSql,\n organizationId,\n load: async (tx) => {\n const [state] = await tx\n .select({\n memberGroupId: organizationReadModelState.memberGroupId,\n memberGroupStateHash: organizationReadModelState.memberGroupStateHash,\n })\n .from(organizationReadModelState)\n .where(eq(organizationReadModelState.organizationId, organizationId))\n .limit(1);\n if (!state) {\n return null;\n }\n const [requester] = await tx\n .select({ userId: organizationReadModelRequesters.userId })\n .from(organizationReadModelRequesters)\n .where(\n and(\n eq(organizationReadModelRequesters.organizationId, organizationId),\n eq(organizationReadModelRequesters.userId, currentUserId),\n ),\n )\n .limit(1);\n if (!requester) {\n return null;\n }\n return loadOrganizationReadModelGroupMembersInTransaction({\n groupId,\n memberGroup: {\n groupId: state.memberGroupId,\n stateHash: state.memberGroupStateHash,\n },\n organizationId,\n tx,\n });\n },\n });\n}\n"]}
@@ -1,13 +1,22 @@
1
- import { and, eq, inArray } from "drizzle-orm";
2
- import { organizationReadModelGroupMembers, organizationReadModelGroupMemberships, organizationReadModelGroups, } from "../../sqlite/organizationReadModelSchema.js";
3
- const GROUP_MEMBERSHIP_BATCH_SIZE = 90;
4
- const GROUP_MEMBER_INSERT_BATCH_SIZE = 30;
1
+ import { and, eq, inArray, sql } from "drizzle-orm";
2
+ import { organizationReadModelGroupMembers, organizationReadModelGroups, } from "../../sqlite/organizationReadModelSchema.js";
3
+ import { sqliteInsertBatchSize, sqliteRowsPerStatement, } from "../../sqlite/sqliteBindBudget.js";
4
+ // Group ID lists bind one value per group; the head-clearing update also
5
+ // binds its null value beside the organization ID.
6
+ const GROUP_MEMBERSHIP_BATCH_SIZE = sqliteRowsPerStatement(1, 2);
7
+ // Each head binds two values in its CASE arm and one in the IN list, plus the
8
+ // organization ID.
9
+ const GROUP_HEAD_UPDATE_BATCH_SIZE = sqliteRowsPerStatement(3, 1);
10
+ const GROUP_MEMBER_INSERT_BATCH_SIZE = sqliteInsertBatchSize(organizationReadModelGroupMembers);
5
11
  export class OrganizationReadModelBindingError extends Error {
6
12
  constructor(message) {
7
13
  super(message);
8
14
  this.name = "OrganizationReadModelBindingError";
9
15
  }
10
16
  }
17
+ function membershipCoverageError() {
18
+ return new OrganizationReadModelBindingError("Organization group membership coverage is inconsistent");
19
+ }
11
20
  function membershipMemberKey(member) {
12
21
  return member.userId;
13
22
  }
@@ -36,8 +45,9 @@ async function clearMembershipRows(input) {
36
45
  .where(eq(organizationReadModelGroupMembers.organizationId, input.organizationId))
37
46
  .run();
38
47
  await input.tx
39
- .delete(organizationReadModelGroupMemberships)
40
- .where(eq(organizationReadModelGroupMemberships.organizationId, input.organizationId))
48
+ .update(organizationReadModelGroups)
49
+ .set({ membersStateHash: null })
50
+ .where(eq(organizationReadModelGroups.organizationId, input.organizationId))
41
51
  .run();
42
52
  }
43
53
  async function deleteMembershipRows(input, groupIds) {
@@ -48,8 +58,9 @@ async function deleteMembershipRows(input, groupIds) {
48
58
  .where(and(eq(organizationReadModelGroupMembers.organizationId, input.organizationId), inArray(organizationReadModelGroupMembers.groupId, groupIdBatch)))
49
59
  .run();
50
60
  await input.tx
51
- .delete(organizationReadModelGroupMemberships)
52
- .where(and(eq(organizationReadModelGroupMemberships.organizationId, input.organizationId), inArray(organizationReadModelGroupMemberships.groupId, groupIdBatch)))
61
+ .update(organizationReadModelGroups)
62
+ .set({ membersStateHash: null })
63
+ .where(and(eq(organizationReadModelGroups.organizationId, input.organizationId), inArray(organizationReadModelGroups.groupId, groupIdBatch)))
53
64
  .run();
54
65
  }
55
66
  }
@@ -66,21 +77,38 @@ async function removeExistingMembershipRows(input) {
66
77
  await deleteMembershipRows(input, removedGroupIds);
67
78
  }
68
79
  }
69
- async function insertMembershipRows(input) {
70
- if (input.lane.groups.length === 0) {
71
- return;
72
- }
73
- const membershipRows = input.lane.groups.map((group) => ({
74
- organizationId: input.organizationId,
75
- groupId: group.groupId,
76
- stateHash: group.stateHash,
77
- }));
78
- for (let index = 0; index < membershipRows.length; index += GROUP_MEMBERSHIP_BATCH_SIZE) {
80
+ /**
81
+ * Record each visible group's membership head on its catalog row. A head for
82
+ * a group the catalog does not list cannot be bound and fails the page. The
83
+ * reserved Members head binds to the state row instead; the catalog normally
84
+ * omits Members, so its row is updated only when one exists.
85
+ */
86
+ async function writeVisibleGroupHeads(input) {
87
+ const memberGroupHead = input.lane.groups.find((group) => group.groupId === input.memberGroupId);
88
+ if (memberGroupHead) {
79
89
  await input.tx
80
- .insert(organizationReadModelGroupMemberships)
81
- .values(membershipRows.slice(index, index + GROUP_MEMBERSHIP_BATCH_SIZE))
90
+ .update(organizationReadModelGroups)
91
+ .set({ membersStateHash: memberGroupHead.stateHash })
92
+ .where(and(eq(organizationReadModelGroups.organizationId, input.organizationId), eq(organizationReadModelGroups.groupId, input.memberGroupId)))
82
93
  .run();
83
94
  }
95
+ const heads = input.lane.groups.filter((group) => group.groupId !== input.memberGroupId);
96
+ for (let index = 0; index < heads.length; index += GROUP_HEAD_UPDATE_BATCH_SIZE) {
97
+ const batch = heads.slice(index, index + GROUP_HEAD_UPDATE_BATCH_SIZE);
98
+ const arms = sql.join(batch.map((head) => sql `WHEN ${head.groupId} THEN ${head.stateHash}`), sql ` `);
99
+ const updated = await input.tx
100
+ .update(organizationReadModelGroups)
101
+ .set({
102
+ membersStateHash: sql `CASE ${organizationReadModelGroups.groupId} ${arms} END`,
103
+ })
104
+ .where(and(eq(organizationReadModelGroups.organizationId, input.organizationId), inArray(organizationReadModelGroups.groupId, batch.map((head) => head.groupId))))
105
+ .returning({ groupId: organizationReadModelGroups.groupId });
106
+ if (updated.length !== batch.length) {
107
+ throw membershipCoverageError();
108
+ }
109
+ }
110
+ }
111
+ async function insertMemberRows(input) {
84
112
  const memberRows = input.lane.groups.flatMap((group) => group.members.map((member, sortOrder) => ({
85
113
  organizationId: input.organizationId,
86
114
  groupId: group.groupId,
@@ -88,10 +116,6 @@ async function insertMembershipRows(input) {
88
116
  sortOrder,
89
117
  stateHash: group.stateHash,
90
118
  role: member.role,
91
- signingKeyFingerprint: member.signingKeyFingerprint,
92
- signingPublicKey: member.signingPublicKey,
93
- encapsulationPublicKey: member.encapsulationPublicKey,
94
- encapsulationKeyFingerprint: member.encapsulationKeyFingerprint,
95
119
  })));
96
120
  for (let index = 0; index < memberRows.length; index += GROUP_MEMBER_INSERT_BATCH_SIZE) {
97
121
  await input.tx
@@ -100,27 +124,59 @@ async function insertMembershipRows(input) {
100
124
  .run();
101
125
  }
102
126
  }
127
+ function nextMemberGroupStateHash(input) {
128
+ const head = input.lane.groups.find((group) => group.groupId === input.memberGroupId);
129
+ if (head) {
130
+ return head.stateHash;
131
+ }
132
+ return input.replaceAll ||
133
+ input.lane.deletedGroupIds.includes(input.memberGroupId)
134
+ ? null
135
+ : input.currentMemberGroupStateHash;
136
+ }
137
+ /**
138
+ * Apply one groupMemberships lane and return the reserved Members head, which
139
+ * the caller persists on the organization state row.
140
+ */
103
141
  export async function applyGroupMembershipsLane(input) {
104
142
  assertUniqueMembershipRows(input.lane);
105
143
  await removeExistingMembershipRows(input);
106
- await insertMembershipRows(input);
144
+ await writeVisibleGroupHeads(input);
145
+ await insertMemberRows(input);
146
+ return nextMemberGroupStateHash(input);
147
+ }
148
+ function membershipHeadsByGroupId(input) {
149
+ if (input.memberGroupStateHash === null) {
150
+ throw membershipCoverageError();
151
+ }
152
+ const heads = new Map([[input.memberGroupId, input.memberGroupStateHash]]);
153
+ for (const group of input.groups) {
154
+ if (group.membersStateHash === null) {
155
+ continue;
156
+ }
157
+ // A head is expected exactly for stateful visible groups; a listed
158
+ // Members row must carry the same head as the state row.
159
+ const isMemberGroup = group.groupId === input.memberGroupId;
160
+ if (group.stateHash === null ||
161
+ (isMemberGroup
162
+ ? group.membersStateHash !== input.memberGroupStateHash
163
+ : heads.has(group.groupId))) {
164
+ throw membershipCoverageError();
165
+ }
166
+ heads.set(group.groupId, group.membersStateHash);
167
+ }
168
+ return heads;
107
169
  }
108
170
  export async function assertStoredGroupMembershipBindings(input) {
109
171
  const groups = await input.tx
110
172
  .select({
111
173
  groupId: organizationReadModelGroups.groupId,
112
174
  memberCount: organizationReadModelGroups.memberCount,
175
+ membersStateHash: organizationReadModelGroups.membersStateHash,
113
176
  stateHash: organizationReadModelGroups.stateHash,
114
177
  })
115
178
  .from(organizationReadModelGroups)
116
179
  .where(eq(organizationReadModelGroups.organizationId, input.organizationId));
117
- const heads = await input.tx
118
- .select({
119
- groupId: organizationReadModelGroupMemberships.groupId,
120
- stateHash: organizationReadModelGroupMemberships.stateHash,
121
- })
122
- .from(organizationReadModelGroupMemberships)
123
- .where(eq(organizationReadModelGroupMemberships.organizationId, input.organizationId));
124
180
  const members = await input.tx
125
181
  .select({
126
182
  groupId: organizationReadModelGroupMembers.groupId,
@@ -128,30 +184,19 @@ export async function assertStoredGroupMembershipBindings(input) {
128
184
  })
129
185
  .from(organizationReadModelGroupMembers)
130
186
  .where(eq(organizationReadModelGroupMembers.organizationId, input.organizationId));
131
- const headsByGroupId = new Map(heads.map((head) => [head.groupId, head]));
132
- const expectedHeadIds = new Set([
133
- input.memberGroupId,
134
- ...groups.flatMap((group) => (group.stateHash ? [group.groupId] : [])),
135
- ]);
136
- if (heads.length !== expectedHeadIds.size ||
137
- heads.some((head) => !expectedHeadIds.has(head.groupId))) {
138
- throw new OrganizationReadModelBindingError("Organization group membership coverage is inconsistent");
139
- }
187
+ const headsByGroupId = membershipHeadsByGroupId({ ...input, groups });
140
188
  const memberCountsByGroupId = new Map();
141
189
  for (const member of members) {
142
- const head = headsByGroupId.get(member.groupId);
143
- if (!head || head.stateHash !== member.stateHash) {
190
+ if (headsByGroupId.get(member.groupId) !== member.stateHash) {
144
191
  throw new OrganizationReadModelBindingError("Organization group membership member state is inconsistent");
145
192
  }
146
193
  memberCountsByGroupId.set(member.groupId, (memberCountsByGroupId.get(member.groupId) ?? 0) + 1);
147
194
  }
148
195
  for (const group of groups) {
149
- const head = headsByGroupId.get(group.groupId);
150
196
  if (group.stateHash === null) {
151
197
  continue;
152
198
  }
153
- if (!head ||
154
- head.stateHash !== group.stateHash ||
199
+ if (group.membersStateHash !== group.stateHash ||
155
200
  group.memberCount === null ||
156
201
  group.memberCount !== (memberCountsByGroupId.get(group.groupId) ?? 0)) {
157
202
  throw new OrganizationReadModelBindingError("Organization group membership state hash is inconsistent");