@tearleads/client-sdk 0.1.114 → 0.1.116

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 (80) hide show
  1. package/client/containerContents/documentDiscovery.js +5 -3
  2. package/client/containerContents/documentDiscovery.js.map +1 -1
  3. package/data/persistence/container-contents/containerRecoveryPersistence.js +152 -38
  4. package/data/persistence/container-contents/containerRecoveryPersistence.js.map +1 -1
  5. package/data/persistence/container-contents/documentContainerProjectionPersistence.js +47 -18
  6. package/data/persistence/container-contents/documentContainerProjectionPersistence.js.map +1 -1
  7. package/data/persistence/container-contents/documentPlacement.js +24 -5
  8. package/data/persistence/container-contents/documentPlacement.js.map +1 -1
  9. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js +1 -1
  10. package/data/persistence/documents/containerDocumentTombstoneHoldsPersistence.js.map +1 -1
  11. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js +43 -35
  12. package/data/persistence/documents/documentDiscoveryEvidencePersistence.js.map +1 -1
  13. package/data/persistence/documents/documentsPersistence.js +4 -58
  14. package/data/persistence/documents/documentsPersistence.js.map +1 -1
  15. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js +15 -11
  16. package/data/persistence/documents/internal/containerDocumentTombstoneHolds.js.map +1 -1
  17. package/data/persistence/documents/internal/containerDocumentTombstones.js +87 -35
  18. package/data/persistence/documents/internal/containerDocumentTombstones.js.map +1 -1
  19. package/data/persistence/documents/internal/discoveredDocumentPlacement.js +5 -2
  20. package/data/persistence/documents/internal/discoveredDocumentPlacement.js.map +1 -1
  21. package/data/persistence/documents/internal/discoveredDocumentUpsert.js +174 -0
  22. package/data/persistence/documents/internal/discoveredDocumentUpsert.js.map +1 -0
  23. package/data/persistence/documents/internal/documentRows.js +96 -1
  24. package/data/persistence/documents/internal/documentRows.js.map +1 -1
  25. package/data/persistence/documents/internal/rowQueryPersistence.js +38 -20
  26. package/data/persistence/documents/internal/rowQueryPersistence.js.map +1 -1
  27. package/data/persistence/documents/records/documentRecordPersistence.d.ts +5 -0
  28. package/data/persistence/documents/records/documentRecordPersistence.js +41 -6
  29. package/data/persistence/documents/records/documentRecordPersistence.js.map +1 -1
  30. package/data/persistence/documents/records/index.d.ts +1 -1
  31. package/data/persistence/documents/records/index.js +1 -1
  32. package/data/persistence/documents/records/index.js.map +1 -1
  33. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.d.ts +2 -0
  34. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js +17 -1
  35. package/data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence.js.map +1 -1
  36. package/internal/api-client/ApiClient.d.ts +118 -0
  37. package/internal/api-client/ApiClient.js +4 -0
  38. package/internal/api-client/ApiClient.js.map +1 -1
  39. package/internal/api-client/index.d.ts +1 -1
  40. package/internal/api-client/index.js.map +1 -1
  41. package/internal/api-client/principalPolicyPages.d.ts +15 -1
  42. package/internal/api-client/principalPolicyPages.js +48 -2
  43. package/internal/api-client/principalPolicyPages.js.map +1 -1
  44. package/internal/api-client/principalPolicyRequests.d.ts +118 -1
  45. package/internal/api-client/principalPolicyRequests.js +4 -1
  46. package/internal/api-client/principalPolicyRequests.js.map +1 -1
  47. package/internal/validators/operation/principalPolicyPageQuery.d.ts +9 -0
  48. package/internal/validators/operation/principalPolicyPageQuery.js +10 -0
  49. package/internal/validators/operation/principalPolicyPageQuery.js.map +1 -1
  50. package/internal/validators/operation/principals.d.ts +37 -4
  51. package/internal/validators/operation/principals.js +5 -3
  52. package/internal/validators/operation/principals.js.map +1 -1
  53. package/internal/validators/response/index.d.ts +1 -1
  54. package/internal/validators/response/index.js +1 -1
  55. package/internal/validators/response/index.js.map +1 -1
  56. package/internal/validators/response/principalPolicyPage.d.ts +497 -1
  57. package/internal/validators/response/principalPolicyPage.js +18 -0
  58. package/internal/validators/response/principalPolicyPage.js.map +1 -1
  59. package/package.json +1 -1
  60. package/workflows/container-contents/documentLinks/index.d.ts +4 -0
  61. package/workflows/container-contents/documentLinks/index.js +10 -3
  62. package/workflows/container-contents/documentLinks/index.js.map +1 -1
  63. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js +7 -2
  64. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js.map +1 -1
  65. package/workflows/container-contents/remoteReset/containerRemoteReset.js +12 -7
  66. package/workflows/container-contents/remoteReset/containerRemoteReset.js.map +1 -1
  67. package/workflows/container-contents/tombstones/documentTombstoneEvidence.js +6 -5
  68. package/workflows/container-contents/tombstones/documentTombstoneEvidence.js.map +1 -1
  69. package/workflows/documents/remoteReset/documentRemoteReset.js +31 -18
  70. package/workflows/documents/remoteReset/documentRemoteReset.js.map +1 -1
  71. package/workflows/principals/principalPolicyHistoryReader.d.ts +6 -1
  72. package/workflows/principals/principalPolicyHistoryReader.js +4 -1
  73. package/workflows/principals/principalPolicyHistoryReader.js.map +1 -1
  74. package/workflows/principals/recoveryOperation/principalRecoveryDirectory.js +108 -59
  75. package/workflows/principals/recoveryOperation/principalRecoveryDirectory.js.map +1 -1
  76. package/workflows/principals/runtimePolicyRecovery.js +26 -14
  77. package/workflows/principals/runtimePolicyRecovery.js.map +1 -1
  78. package/workflows/principals/runtimePolicyWarmer.d.ts +26 -0
  79. package/workflows/principals/runtimePolicyWarmer.js +8 -5
  80. package/workflows/principals/runtimePolicyWarmer.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"documentTombstoneEvidence.js","sourceRoot":"","sources":["documentTombstoneEvidence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DOCUMENT_NOT_FOUND_ERROR_CODE,\n type DocumentWriterProjectionResponse,\n} from \"@tearleads/validators/response\";\nimport { assertDocumentWriterProjectionConsistent } from \"../../../data/documents/shared/projection\";\nimport { uniqueSortedStrings } from \"../../../data/documents/shared/readers\";\nimport { ProjectionDependencyUnavailableError } from \"../../../data/keyingProjectionVerification/dependencyUnavailable\";\nimport { reportKeyingVerificationErrorInCauseChain } from \"../../../data/keyingProjectionVerification/error\";\nimport { createProjectionUserKeyResolver } from \"../../../data/keyingProjectionVerification/userKeyResolver\";\nimport { loadDocumentPurgeCheckpoint } from \"../../../data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence\";\nimport { createRuntimePrincipalPolicyWarmer } from \"../../principals/runtimePolicyWarmer\";\nimport type {\n ContainerDocumentTombstone,\n ContainerDocumentTombstoneVerdict,\n ContainerDocumentTombstoneVerifier,\n} from \"../documentDiscovery/documentDiscoveryTypes\";\nimport { documentHeadMatchesOrganization } from \"../documentDiscovery/documentHeadScope\";\nimport type { ContainerContentsWorkflowRuntime } from \"../runtime\";\n\nexport interface VerifiedDocumentHeadLinkSet {\n readonly organizationId: string | null;\n readonly accessEpoch: number;\n /** Present for a signed head; a terminal purge has no live access state. */\n readonly accessStateHash?: string;\n readonly linkedContainerIds: ReadonlyArray<string>;\n}\n\n/**\n * The document's verified head link set, or `null` when no verified head is\n * available: the projection could not be fetched (the requester may no\n * longer read the document, or it was purged) or failed verification.\n */\nexport type DocumentHeadLinkSetLoader = (\n documentId: string,\n expectedManifestHash?: string,\n) => Promise<VerifiedDocumentHeadLinkSet | \"not-found\" | null>;\n\n/** The highest access epoch local state records for the document. */\nexport type LocalDocumentAccessEpochLoader = (\n documentId: string,\n) => Promise<number>;\n\nexport interface DocumentHeadLinkSetLoaderDeps {\n readonly assertDocumentWriterProjectionConsistent: typeof assertDocumentWriterProjectionConsistent;\n readonly loadDocumentPurgeCheckpoint: typeof loadDocumentPurgeCheckpoint;\n}\n\nexport const HEAD_LINK_SET_LOAD_CONCURRENCY = 4;\n/**\n * Head loads per settle. Tombstones beyond it stay unverified and are held\n * for a later, backed-off retry, so a bulk move out of a folder does not cost\n * every device an unbounded burst of fetches before the watermark advances.\n */\nexport const HEAD_LINK_SET_LOADS_PER_RUN = 32;\n\nasync function verifiedHeadLinkSet(\n runtime: ContainerContentsWorkflowRuntime,\n deps: DocumentHeadLinkSetLoaderDeps,\n documentId: string,\n projection: DocumentWriterProjectionResponse,\n): Promise<VerifiedDocumentHeadLinkSet> {\n const verified: { value?: VerifiedDocumentHeadLinkSet } = {};\n await deps.assertDocumentWriterProjectionConsistent(projection, {\n // Only the link set is read; a bundle wrapped to superseded KEK targets\n // still carries the signed head.\n allowStaleContentKeyBundle: true,\n execSql: runtime.infra.execSql,\n onVerifiedAuthorization: (authorization) => {\n const head = authorization.documentManifestByHash.get(\n projection.documentManifest.manifestHash,\n );\n if (head && head.state.documentId === documentId) {\n verified.value = {\n organizationId: head.state.organizationId,\n accessEpoch: head.state.epoch,\n accessStateHash: head.manifestHash,\n linkedContainerIds: uniqueSortedStrings(\n head.state.linkedContainerIds,\n ),\n };\n }\n },\n resolveProjectionUserKey: createProjectionUserKeyResolver(runtime),\n warmReferencedPrincipalPolicies:\n createRuntimePrincipalPolicyWarmer(runtime),\n });\n if (!verified.value) {\n throw new Error(\"Tombstone evidence lacks a verified document head\");\n }\n return verified.value;\n}\n\nasync function reportUnavailableHead(\n runtime: ContainerContentsWorkflowRuntime,\n documentId: string,\n error: unknown,\n): Promise<void> {\n const integrityFailure = await reportKeyingVerificationErrorInCauseChain(\n error,\n runtime.util.reportSecurityIncident,\n {\n objectId: documentId,\n objectKind: \"document\",\n operation: \"document.tombstone-evidence\",\n },\n );\n try {\n if (\n !integrityFailure &&\n error instanceof ProjectionDependencyUnavailableError\n ) {\n // A policy can advance without changing this document's hash. Drop its\n // cached evidence so the held head can recover on the next run.\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n runtime.util.log(\n `Container contents: tombstone evidence for document ${documentId} is awaiting current policy history`,\n );\n return;\n }\n runtime.util.logError?.(\n \"Container contents: tombstone evidence is unavailable\",\n error,\n );\n } catch {\n // A host logger that throws must not fail the discovery pass.\n }\n}\n\nexport function createDocumentHeadLinkSetLoader(\n runtime: ContainerContentsWorkflowRuntime,\n deps: DocumentHeadLinkSetLoaderDeps = {\n assertDocumentWriterProjectionConsistent,\n loadDocumentPurgeCheckpoint,\n },\n): DocumentHeadLinkSetLoader {\n return async (documentId, expectedManifestHash) => {\n try {\n // A verified purge proof is terminal signed evidence that the document\n // links nothing any more.\n if (\n await deps.loadDocumentPurgeCheckpoint(\n runtime.infra.execSql,\n documentId,\n )\n ) {\n return {\n organizationId: null,\n accessEpoch: Number.MAX_SAFE_INTEGER,\n linkedContainerIds: [],\n };\n }\n // Tombstones require a fresh head. Listings can reuse a matching cached\n // projection, whose signature and local checkpoint are still verified.\n if (!expectedManifestHash)\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n let result = await runtime.apiClient.getDocumentWriterProjectionResult(\n documentId,\n { reportErrors: false },\n );\n if (\n expectedManifestHash &&\n result.ok &&\n result.data.documentManifest.manifestHash !== expectedManifestHash\n ) {\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n result = await runtime.apiClient.getDocumentWriterProjectionResult(\n documentId,\n { reportErrors: false },\n );\n }\n if (!result.ok) {\n if (\n result.status === 404 &&\n result.code === DOCUMENT_NOT_FOUND_ERROR_CODE\n )\n return \"not-found\";\n runtime.util.log(\n `Container contents: tombstone evidence for document ${documentId} is unavailable (${result.status ?? \"offline\"})`,\n );\n return null;\n }\n return await verifiedHeadLinkSet(runtime, deps, documentId, result.data);\n } catch (error) {\n // Any failure leaves the tombstone unverified (held and retried); a\n // thrown error here must not fail the whole discovery pass.\n await reportUnavailableHead(runtime, documentId, error);\n return null;\n }\n };\n}\n\nfunction judgeTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstone>,\n head: VerifiedDocumentHeadLinkSet | null,\n): ContainerDocumentTombstoneVerdict[] {\n return tombstones.map((tombstone): ContainerDocumentTombstoneVerdict => {\n if (head === null) {\n return { kind: \"unverified\", tombstone };\n }\n if (head.linkedContainerIds.includes(tombstone.containerId)) {\n return {\n kind: \"refuted\",\n linkedContainerIds: head.linkedContainerIds,\n tombstone,\n };\n }\n return {\n kind: \"verified\",\n tombstone: {\n ...tombstone,\n accessEpoch: head.accessEpoch,\n linkedContainerIds: head.linkedContainerIds,\n },\n };\n });\n}\n\n/**\n * Judge listing tombstones against each document's verified head link set,\n * fetched once per document with bounded concurrency and a per-run cap. A\n * head whose epoch is below local document state is no evidence: it may be a\n * lagging replica or a replay, and applying it could delete rows a newer\n * listing or settled move wrote.\n */\nexport function createContainerDocumentTombstoneVerifier(\n organizationId: string | undefined,\n loadDocumentHeadLinkSet: DocumentHeadLinkSetLoader,\n loadLocalDocumentAccessEpoch: LocalDocumentAccessEpochLoader,\n): ContainerDocumentTombstoneVerifier {\n return async (tombstones) => {\n const byDocumentId = new Map<string, ContainerDocumentTombstone[]>();\n for (const tombstone of tombstones) {\n const group = byDocumentId.get(tombstone.documentId) ?? [];\n group.push(tombstone);\n byDocumentId.set(tombstone.documentId, group);\n }\n const groups = [...byDocumentId.entries()];\n const verdicts: ContainerDocumentTombstoneVerdict[][] = groups.map(\n ([, group], index): ContainerDocumentTombstoneVerdict[] =>\n index < HEAD_LINK_SET_LOADS_PER_RUN\n ? judgeTombstones(group, null)\n : group.map((tombstone) => ({ kind: \"deferred\", tombstone })),\n );\n let next = 0;\n const worker = async () => {\n while (next < Math.min(groups.length, HEAD_LINK_SET_LOADS_PER_RUN)) {\n const index = next++;\n const entry = groups[index];\n if (!entry) break;\n const [documentId, group] = entry;\n const head = await loadDocumentHeadLinkSet(documentId);\n if (head === null || head === \"not-found\") continue;\n if (!documentHeadMatchesOrganization(head, organizationId)) continue;\n // A local-state read that fails is treated like an unavailable head:\n // the tombstones stay held rather than failing the discovery pass.\n const localEpoch = await loadLocalDocumentAccessEpoch(documentId).catch(\n () => Number.MAX_SAFE_INTEGER,\n );\n if (head.accessEpoch < localEpoch) continue;\n verdicts[index] = judgeTombstones(group, head);\n }\n };\n await Promise.all(\n Array.from(\n { length: Math.min(HEAD_LINK_SET_LOAD_CONCURRENCY, groups.length) },\n worker,\n ),\n );\n return verdicts.flat();\n };\n}\n"]}
1
+ {"version":3,"file":"documentTombstoneEvidence.js","sourceRoot":"","sources":["documentTombstoneEvidence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DOCUMENT_NOT_FOUND_ERROR_CODE,\n type DocumentWriterProjectionResponse,\n} from \"@tearleads/validators/response\";\nimport { assertDocumentWriterProjectionConsistent } from \"../../../data/documents/shared/projection\";\nimport { uniqueSortedStrings } from \"../../../data/documents/shared/readers\";\nimport { ProjectionDependencyUnavailableError } from \"../../../data/keyingProjectionVerification/dependencyUnavailable\";\nimport { reportKeyingVerificationErrorInCauseChain } from \"../../../data/keyingProjectionVerification/error\";\nimport { createProjectionUserKeyResolver } from \"../../../data/keyingProjectionVerification/userKeyResolver\";\nimport { loadDocumentPurgeCheckpoint } from \"../../../data/persistence/keyingCheckpoints/documentPurgeCheckpointPersistence\";\nimport {\n createRuntimePrincipalPolicyWarmer,\n type PrincipalPolicyOperation,\n} from \"../../principals/runtimePolicyWarmer\";\nimport type {\n ContainerDocumentTombstone,\n ContainerDocumentTombstoneVerdict,\n ContainerDocumentTombstoneVerifier,\n} from \"../documentDiscovery/documentDiscoveryTypes\";\nimport { documentHeadMatchesOrganization } from \"../documentDiscovery/documentHeadScope\";\nimport type { ContainerContentsWorkflowRuntime } from \"../runtime\";\n\nexport interface VerifiedDocumentHeadLinkSet {\n readonly organizationId: string | null;\n readonly accessEpoch: number;\n /** Present for a signed head; a terminal purge has no live access state. */\n readonly accessStateHash?: string;\n readonly linkedContainerIds: ReadonlyArray<string>;\n}\n\n/**\n * The document's verified head link set, or `null` when no verified head is\n * available: the projection could not be fetched (the requester may no\n * longer read the document, or it was purged) or failed verification.\n */\nexport type DocumentHeadLinkSetLoader = (\n documentId: string,\n expectedManifestHash?: string,\n) => Promise<VerifiedDocumentHeadLinkSet | \"not-found\" | null>;\n\n/** The highest access epoch local state records for the document. */\nexport type LocalDocumentAccessEpochLoader = (\n documentId: string,\n) => Promise<number>;\n\nexport interface DocumentHeadLinkSetLoaderDeps {\n readonly assertDocumentWriterProjectionConsistent: typeof assertDocumentWriterProjectionConsistent;\n readonly loadDocumentPurgeCheckpoint: typeof loadDocumentPurgeCheckpoint;\n}\n\nexport const HEAD_LINK_SET_LOAD_CONCURRENCY = 4;\n/**\n * Head loads per settle. Tombstones beyond it stay unverified and are held\n * for a later, backed-off retry, so a bulk move out of a folder does not cost\n * every device an unbounded burst of fetches before the watermark advances.\n */\nexport const HEAD_LINK_SET_LOADS_PER_RUN = 32;\n\nasync function verifiedHeadLinkSet(\n runtime: ContainerContentsWorkflowRuntime,\n deps: DocumentHeadLinkSetLoaderDeps,\n documentId: string,\n projection: DocumentWriterProjectionResponse,\n policyOperation: PrincipalPolicyOperation | undefined,\n): Promise<VerifiedDocumentHeadLinkSet> {\n const verified: { value?: VerifiedDocumentHeadLinkSet } = {};\n await deps.assertDocumentWriterProjectionConsistent(projection, {\n // Only the link set is read; a bundle wrapped to superseded KEK targets\n // still carries the signed head.\n allowStaleContentKeyBundle: true,\n execSql: runtime.infra.execSql,\n onVerifiedAuthorization: (authorization) => {\n const head = authorization.documentManifestByHash.get(\n projection.documentManifest.manifestHash,\n );\n if (head && head.state.documentId === documentId) {\n verified.value = {\n organizationId: head.state.organizationId,\n accessEpoch: head.state.epoch,\n accessStateHash: head.manifestHash,\n linkedContainerIds: uniqueSortedStrings(\n head.state.linkedContainerIds,\n ),\n };\n }\n },\n resolveProjectionUserKey: createProjectionUserKeyResolver(runtime),\n warmReferencedPrincipalPolicies: createRuntimePrincipalPolicyWarmer(\n runtime,\n { operation: policyOperation },\n ),\n });\n if (!verified.value) {\n throw new Error(\"Tombstone evidence lacks a verified document head\");\n }\n return verified.value;\n}\n\nasync function reportUnavailableHead(\n runtime: ContainerContentsWorkflowRuntime,\n documentId: string,\n error: unknown,\n): Promise<void> {\n const integrityFailure = await reportKeyingVerificationErrorInCauseChain(\n error,\n runtime.util.reportSecurityIncident,\n {\n objectId: documentId,\n objectKind: \"document\",\n operation: \"document.tombstone-evidence\",\n },\n );\n try {\n if (\n !integrityFailure &&\n error instanceof ProjectionDependencyUnavailableError\n ) {\n // A policy can advance without changing this document's hash. Drop its\n // cached evidence so the held head can recover on the next run.\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n runtime.util.log(\n `Container contents: tombstone evidence for document ${documentId} is awaiting current policy history`,\n );\n return;\n }\n runtime.util.logError?.(\n \"Container contents: tombstone evidence is unavailable\",\n error,\n );\n } catch {\n // A host logger that throws must not fail the discovery pass.\n }\n}\n\n/** `policyOperation` lets one discovery pass share a directory discovery. */\nexport function createDocumentHeadLinkSetLoader(\n runtime: ContainerContentsWorkflowRuntime,\n deps: DocumentHeadLinkSetLoaderDeps = {\n assertDocumentWriterProjectionConsistent,\n loadDocumentPurgeCheckpoint,\n },\n policyOperation?: PrincipalPolicyOperation,\n): DocumentHeadLinkSetLoader {\n return async (documentId, expectedManifestHash) => {\n try {\n // A verified purge proof is terminal signed evidence that the document\n // links nothing any more.\n if (\n await deps.loadDocumentPurgeCheckpoint(\n runtime.infra.execSql,\n documentId,\n )\n ) {\n return {\n organizationId: null,\n accessEpoch: Number.MAX_SAFE_INTEGER,\n linkedContainerIds: [],\n };\n }\n // Tombstones require a fresh head. Listings can reuse a matching cached\n // projection, whose signature and local checkpoint are still verified.\n if (!expectedManifestHash)\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n let result = await runtime.apiClient.getDocumentWriterProjectionResult(\n documentId,\n { reportErrors: false },\n );\n if (\n expectedManifestHash &&\n result.ok &&\n result.data.documentManifest.manifestHash !== expectedManifestHash\n ) {\n runtime.apiClient.evictDocumentWriterProjection(documentId);\n result = await runtime.apiClient.getDocumentWriterProjectionResult(\n documentId,\n { reportErrors: false },\n );\n }\n if (!result.ok) {\n if (\n result.status === 404 &&\n result.code === DOCUMENT_NOT_FOUND_ERROR_CODE\n )\n return \"not-found\";\n runtime.util.log(\n `Container contents: tombstone evidence for document ${documentId} is unavailable (${result.status ?? \"offline\"})`,\n );\n return null;\n }\n return await verifiedHeadLinkSet(\n runtime,\n deps,\n documentId,\n result.data,\n policyOperation,\n );\n } catch (error) {\n // Any failure leaves the tombstone unverified (held and retried); a\n // thrown error here must not fail the whole discovery pass.\n await reportUnavailableHead(runtime, documentId, error);\n return null;\n }\n };\n}\n\nfunction judgeTombstones(\n tombstones: ReadonlyArray<ContainerDocumentTombstone>,\n head: VerifiedDocumentHeadLinkSet | null,\n): ContainerDocumentTombstoneVerdict[] {\n return tombstones.map((tombstone): ContainerDocumentTombstoneVerdict => {\n if (head === null) {\n return { kind: \"unverified\", tombstone };\n }\n if (head.linkedContainerIds.includes(tombstone.containerId)) {\n return {\n kind: \"refuted\",\n linkedContainerIds: head.linkedContainerIds,\n tombstone,\n };\n }\n return {\n kind: \"verified\",\n tombstone: {\n ...tombstone,\n accessEpoch: head.accessEpoch,\n linkedContainerIds: head.linkedContainerIds,\n },\n };\n });\n}\n\n/**\n * Judge listing tombstones against each document's verified head link set,\n * fetched once per document with bounded concurrency and a per-run cap. A\n * head whose epoch is below local document state is no evidence: it may be a\n * lagging replica or a replay, and applying it could delete rows a newer\n * listing or settled move wrote.\n */\nexport function createContainerDocumentTombstoneVerifier(\n organizationId: string | undefined,\n loadDocumentHeadLinkSet: DocumentHeadLinkSetLoader,\n loadLocalDocumentAccessEpoch: LocalDocumentAccessEpochLoader,\n): ContainerDocumentTombstoneVerifier {\n return async (tombstones) => {\n const byDocumentId = new Map<string, ContainerDocumentTombstone[]>();\n for (const tombstone of tombstones) {\n const group = byDocumentId.get(tombstone.documentId) ?? [];\n group.push(tombstone);\n byDocumentId.set(tombstone.documentId, group);\n }\n const groups = [...byDocumentId.entries()];\n const verdicts: ContainerDocumentTombstoneVerdict[][] = groups.map(\n ([, group], index): ContainerDocumentTombstoneVerdict[] =>\n index < HEAD_LINK_SET_LOADS_PER_RUN\n ? judgeTombstones(group, null)\n : group.map((tombstone) => ({ kind: \"deferred\", tombstone })),\n );\n let next = 0;\n const worker = async () => {\n while (next < Math.min(groups.length, HEAD_LINK_SET_LOADS_PER_RUN)) {\n const index = next++;\n const entry = groups[index];\n if (!entry) break;\n const [documentId, group] = entry;\n const head = await loadDocumentHeadLinkSet(documentId);\n if (head === null || head === \"not-found\") continue;\n if (!documentHeadMatchesOrganization(head, organizationId)) continue;\n // A local-state read that fails is treated like an unavailable head:\n // the tombstones stay held rather than failing the discovery pass.\n const localEpoch = await loadLocalDocumentAccessEpoch(documentId).catch(\n () => Number.MAX_SAFE_INTEGER,\n );\n if (head.accessEpoch < localEpoch) continue;\n verdicts[index] = judgeTombstones(group, head);\n }\n };\n await Promise.all(\n Array.from(\n { length: Math.min(HEAD_LINK_SET_LOAD_CONCURRENCY, groups.length) },\n worker,\n ),\n );\n return verdicts.flat();\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { and, eq, inArray, or } from "drizzle-orm";
1
+ import { and, eq, inArray, or, sql } from "drizzle-orm";
2
2
  import { queuedUpdateRow, unqueueDocumentScopesInTransaction, } from "../../../data/persistence/documents/records/documentPendingUpdatePersistence.js";
3
3
  import { CLEARED_DOCUMENT_SYNC_FAILURE } from "../../../data/persistence/documents/records/documentSyncFailurePersistence.js";
4
4
  import { documentAttachmentBlobProjection, documentHistoryUpdates, documentPendingAttachments, documentProjection, documents, } from "../../../data/sqlite/schema/index.js";
@@ -71,24 +71,37 @@ export async function clearDocumentRowsForRemoteReset(input) {
71
71
  */
72
72
  export async function resetDocumentServerIdentity(input) {
73
73
  const { now, tx } = input;
74
+ const localIdsByAppKind = new Map();
74
75
  for (const row of input.documentRows) {
75
- await tx
76
- .update(documents)
77
- .set({
78
- accessEpoch: 1,
79
- accessStateHash: null,
80
- contentKeyBundle: null,
81
- documentId: null,
82
- documentKekTargets: null,
83
- documentManifestBundle: null,
84
- lastCommitLsn: null,
85
- pullContinuation: null,
86
- recoveryDocumentId: row.documentId ?? row.recoveryDocumentId,
87
- ...CLEARED_DOCUMENT_SYNC_FAILURE,
88
- updatedAt: now,
89
- })
90
- .where(and(eq(documents.appKind, row.appKind), eq(documents.localId, row.localId)))
91
- .run();
76
+ const localIds = localIdsByAppKind.get(row.appKind);
77
+ if (localIds)
78
+ localIds.push(row.localId);
79
+ else
80
+ localIdsByAppKind.set(row.appKind, [row.localId]);
81
+ }
82
+ // One update per batch: SQLite evaluates every SET value against the old
83
+ // row, so COALESCE keeps the id being cleared (or the earlier recovery id)
84
+ // per row; no earlier reset step writes either column.
85
+ for (const [appKind, localIds] of localIdsByAppKind) {
86
+ for (const localIdBatch of remoteResetBatches(localIds)) {
87
+ await tx
88
+ .update(documents)
89
+ .set({
90
+ accessEpoch: 1,
91
+ accessStateHash: null,
92
+ contentKeyBundle: null,
93
+ documentId: null,
94
+ documentKekTargets: null,
95
+ documentManifestBundle: null,
96
+ lastCommitLsn: null,
97
+ pullContinuation: null,
98
+ recoveryDocumentId: sql `coalesce(${documents.documentId}, ${documents.recoveryDocumentId})`,
99
+ ...CLEARED_DOCUMENT_SYNC_FAILURE,
100
+ updatedAt: now,
101
+ })
102
+ .where(and(eq(documents.appKind, appKind), inArray(documents.localId, localIdBatch)))
103
+ .run();
104
+ }
92
105
  }
93
106
  for (const localIdBatch of remoteResetBatches(input.documentLocalIds)) {
94
107
  await tx
@@ -1 +1 @@
1
- {"version":3,"file":"documentRemoteReset.js","sourceRoot":"","sources":["documentRemoteReset.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray, or } from \"drizzle-orm\";\nimport {\n queuedUpdateRow,\n unqueueDocumentScopesInTransaction,\n} from \"../../../data/persistence/documents/records/documentPendingUpdatePersistence\";\nimport { CLEARED_DOCUMENT_SYNC_FAILURE } from \"../../../data/persistence/documents/records/documentSyncFailurePersistence\";\nimport {\n documentAttachmentBlobProjection,\n documentHistoryUpdates,\n documentPendingAttachments,\n documentProjection,\n documents,\n} from \"../../../data/sqlite/schema\";\nimport { remoteResetBatches } from \"../../../data/sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../../data/sqlite/sqlitePersistenceRuntime\";\nimport { clearResetPendingAttachments } from \"./remoteResetAttachments\";\nimport type {\n ResetAttachmentUpload,\n ResetDocumentScope,\n ResetDocumentUpdate,\n} from \"./remoteResetPlans\";\n\n// Document Store's part of a remote reset. The sync remote reset calls these in\n// one transaction: it forgets what the server held for the organization's\n// documents, strips their server identity, and queues their local history and\n// attachments to be published again.\nexport { clearDocumentDiscoveryEvidenceOnRemoteReset } from \"../../../data/persistence/documents/documentDiscoveryReset\";\nexport {\n buildResetPlans,\n type ResetAttachmentUpload,\n type ResetDocumentUpdate,\n} from \"./remoteResetPlans\";\n\nexport interface ResetDocumentRow extends ResetDocumentScope {\n readonly documentId: string | null;\n readonly recoveryDocumentId: string | null;\n}\n\ninterface DocumentPlacementRow {\n readonly containerId: string;\n readonly documentId: string;\n}\n\n/**\n * The documents a reset covers: metadata documents of its folders, documents\n * projected into the organization or its folders, and documents placed in them.\n */\nexport async function loadDocumentResetScope(input: {\n containerIds: readonly string[];\n organizationId: string;\n placementRows: readonly DocumentPlacementRow[];\n tx: ClientSQLiteTransactionScope;\n}): Promise<{\n documentLocalIds: string[];\n documentRows: ResetDocumentRow[];\n oldDocumentIds: string[];\n}> {\n const { organizationId, tx } = input;\n const containerIdSet = new Set(input.containerIds);\n const [allDocuments, projectionRows] = await Promise.all([\n tx\n .select({\n appKind: documents.appKind,\n documentId: documents.documentId,\n localId: documents.localId,\n recoveryDocumentId: documents.recoveryDocumentId,\n })\n .from(documents),\n tx.select().from(documentProjection),\n ]);\n const linkedDocumentIds = new Set(\n input.placementRows\n .filter((row) => containerIdSet.has(row.containerId))\n .map((row) => row.documentId),\n );\n const projectedLocalIds = new Set(\n projectionRows\n .filter(\n (row) =>\n (row.containerId !== null && containerIdSet.has(row.containerId)) ||\n row.organizationId === organizationId ||\n (row.documentId !== null && linkedDocumentIds.has(row.documentId)),\n )\n .flatMap((row) => (row.localId ? [row.localId] : [])),\n );\n const documentRows = allDocuments.filter(\n (row) =>\n (row.appKind === \"container-metadata\" &&\n containerIdSet.has(row.localId)) ||\n (row.appKind === \"documents\" && projectedLocalIds.has(row.localId)) ||\n (row.documentId !== null && linkedDocumentIds.has(row.documentId)),\n );\n return {\n documentLocalIds: [\n ...new Set(\n documentRows\n .filter((row) => row.appKind === \"documents\")\n .map((row) => row.localId),\n ),\n ],\n documentRows,\n oldDocumentIds: [\n ...new Set(\n documentRows.flatMap((row) => (row.documentId ? [row.documentId] : [])),\n ),\n ],\n };\n}\n\n/** Clears pending attachments (keeping re-queued bytes) and queued updates. */\nexport async function clearDocumentRowsForRemoteReset(input: {\n attachmentUploads: readonly ResetAttachmentUpload[];\n documentLocalIds: readonly string[];\n documentRows: readonly ResetDocumentRow[];\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { tx } = input;\n await clearResetPendingAttachments({\n attachmentUploads: input.attachmentUploads,\n documentLocalIds: input.documentLocalIds,\n tx,\n });\n // Sync failures clear with the document columns in resetDocumentServerIdentity.\n for (const documentBatch of remoteResetBatches(input.documentRows)) {\n await unqueueDocumentScopesInTransaction(tx, documentBatch);\n }\n}\n\n/**\n * Drops each document's server id, keys, manifest, and sync position, keeping\n * the old id for recovery, and moves its projection to `organizationId`.\n */\nexport async function resetDocumentServerIdentity(input: {\n documentLocalIds: readonly string[];\n documentRows: readonly ResetDocumentRow[];\n now: string;\n organizationId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { now, tx } = input;\n for (const row of input.documentRows) {\n await tx\n .update(documents)\n .set({\n accessEpoch: 1,\n accessStateHash: null,\n contentKeyBundle: null,\n documentId: null,\n documentKekTargets: null,\n documentManifestBundle: null,\n lastCommitLsn: null,\n pullContinuation: null,\n recoveryDocumentId: row.documentId ?? row.recoveryDocumentId,\n ...CLEARED_DOCUMENT_SYNC_FAILURE,\n updatedAt: now,\n })\n .where(\n and(\n eq(documents.appKind, row.appKind),\n eq(documents.localId, row.localId),\n ),\n )\n .run();\n }\n for (const localIdBatch of remoteResetBatches(input.documentLocalIds)) {\n await tx\n .update(documentProjection)\n .set({\n documentId: null,\n organizationId: input.organizationId,\n })\n .where(inArray(documentProjection.localId, localIdBatch))\n .run();\n }\n}\n\n/** Queues each document's rebuilt local history as one outbox update. */\nexport async function queueDocumentRepublishForRemoteReset(input: {\n documentUpdates: readonly ResetDocumentUpdate[];\n now: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n for (const updateBatch of remoteResetBatches(input.documentUpdates)) {\n await input.tx\n .insert(documentHistoryUpdates)\n .values(\n updateBatch.map((update) =>\n queuedUpdateRow({\n createdAt: input.now,\n origin: \"outbox\",\n pendingUpdate: update,\n scope: update,\n updateId: crypto.randomUUID(),\n }),\n ),\n )\n .run();\n }\n}\n\n/** Queues the attachments the documents still advertise for upload. */\nexport async function queueAttachmentReuploadsForRemoteReset(input: {\n attachmentUploads: readonly ResetAttachmentUpload[];\n now: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n for (const attachmentBatch of remoteResetBatches(input.attachmentUploads)) {\n await input.tx\n .insert(documentPendingAttachments)\n .values(\n attachmentBatch.map((attachment) => ({\n ...attachment,\n createdAt: input.now,\n })),\n )\n .run();\n await input.tx\n .delete(documentAttachmentBlobProjection)\n .where(\n or(\n ...attachmentBatch.map((attachment) =>\n and(\n eq(documentAttachmentBlobProjection.localId, attachment.localId),\n eq(documentAttachmentBlobProjection.slotId, attachment.slotId),\n ),\n ),\n ),\n )\n .run();\n }\n}\n"]}
1
+ {"version":3,"file":"documentRemoteReset.js","sourceRoot":"","sources":["documentRemoteReset.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, inArray, or, sql } from \"drizzle-orm\";\nimport {\n queuedUpdateRow,\n unqueueDocumentScopesInTransaction,\n} from \"../../../data/persistence/documents/records/documentPendingUpdatePersistence\";\nimport { CLEARED_DOCUMENT_SYNC_FAILURE } from \"../../../data/persistence/documents/records/documentSyncFailurePersistence\";\nimport {\n documentAttachmentBlobProjection,\n documentHistoryUpdates,\n documentPendingAttachments,\n documentProjection,\n documents,\n} from \"../../../data/sqlite/schema\";\nimport { remoteResetBatches } from \"../../../data/sqlite/sqliteBindBudget\";\nimport type { ClientSQLiteTransactionScope } from \"../../../data/sqlite/sqlitePersistenceRuntime\";\nimport { clearResetPendingAttachments } from \"./remoteResetAttachments\";\nimport type {\n ResetAttachmentUpload,\n ResetDocumentScope,\n ResetDocumentUpdate,\n} from \"./remoteResetPlans\";\n\n// Document Store's part of a remote reset. The sync remote reset calls these in\n// one transaction: it forgets what the server held for the organization's\n// documents, strips their server identity, and queues their local history and\n// attachments to be published again.\nexport { clearDocumentDiscoveryEvidenceOnRemoteReset } from \"../../../data/persistence/documents/documentDiscoveryReset\";\nexport {\n buildResetPlans,\n type ResetAttachmentUpload,\n type ResetDocumentUpdate,\n} from \"./remoteResetPlans\";\n\nexport interface ResetDocumentRow extends ResetDocumentScope {\n readonly documentId: string | null;\n readonly recoveryDocumentId: string | null;\n}\n\ninterface DocumentPlacementRow {\n readonly containerId: string;\n readonly documentId: string;\n}\n\n/**\n * The documents a reset covers: metadata documents of its folders, documents\n * projected into the organization or its folders, and documents placed in them.\n */\nexport async function loadDocumentResetScope(input: {\n containerIds: readonly string[];\n organizationId: string;\n placementRows: readonly DocumentPlacementRow[];\n tx: ClientSQLiteTransactionScope;\n}): Promise<{\n documentLocalIds: string[];\n documentRows: ResetDocumentRow[];\n oldDocumentIds: string[];\n}> {\n const { organizationId, tx } = input;\n const containerIdSet = new Set(input.containerIds);\n const [allDocuments, projectionRows] = await Promise.all([\n tx\n .select({\n appKind: documents.appKind,\n documentId: documents.documentId,\n localId: documents.localId,\n recoveryDocumentId: documents.recoveryDocumentId,\n })\n .from(documents),\n tx.select().from(documentProjection),\n ]);\n const linkedDocumentIds = new Set(\n input.placementRows\n .filter((row) => containerIdSet.has(row.containerId))\n .map((row) => row.documentId),\n );\n const projectedLocalIds = new Set(\n projectionRows\n .filter(\n (row) =>\n (row.containerId !== null && containerIdSet.has(row.containerId)) ||\n row.organizationId === organizationId ||\n (row.documentId !== null && linkedDocumentIds.has(row.documentId)),\n )\n .flatMap((row) => (row.localId ? [row.localId] : [])),\n );\n const documentRows = allDocuments.filter(\n (row) =>\n (row.appKind === \"container-metadata\" &&\n containerIdSet.has(row.localId)) ||\n (row.appKind === \"documents\" && projectedLocalIds.has(row.localId)) ||\n (row.documentId !== null && linkedDocumentIds.has(row.documentId)),\n );\n return {\n documentLocalIds: [\n ...new Set(\n documentRows\n .filter((row) => row.appKind === \"documents\")\n .map((row) => row.localId),\n ),\n ],\n documentRows,\n oldDocumentIds: [\n ...new Set(\n documentRows.flatMap((row) => (row.documentId ? [row.documentId] : [])),\n ),\n ],\n };\n}\n\n/** Clears pending attachments (keeping re-queued bytes) and queued updates. */\nexport async function clearDocumentRowsForRemoteReset(input: {\n attachmentUploads: readonly ResetAttachmentUpload[];\n documentLocalIds: readonly string[];\n documentRows: readonly ResetDocumentRow[];\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { tx } = input;\n await clearResetPendingAttachments({\n attachmentUploads: input.attachmentUploads,\n documentLocalIds: input.documentLocalIds,\n tx,\n });\n // Sync failures clear with the document columns in resetDocumentServerIdentity.\n for (const documentBatch of remoteResetBatches(input.documentRows)) {\n await unqueueDocumentScopesInTransaction(tx, documentBatch);\n }\n}\n\n/**\n * Drops each document's server id, keys, manifest, and sync position, keeping\n * the old id for recovery, and moves its projection to `organizationId`.\n */\nexport async function resetDocumentServerIdentity(input: {\n documentLocalIds: readonly string[];\n documentRows: readonly ResetDocumentRow[];\n now: string;\n organizationId: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n const { now, tx } = input;\n const localIdsByAppKind = new Map<string, string[]>();\n for (const row of input.documentRows) {\n const localIds = localIdsByAppKind.get(row.appKind);\n if (localIds) localIds.push(row.localId);\n else localIdsByAppKind.set(row.appKind, [row.localId]);\n }\n // One update per batch: SQLite evaluates every SET value against the old\n // row, so COALESCE keeps the id being cleared (or the earlier recovery id)\n // per row; no earlier reset step writes either column.\n for (const [appKind, localIds] of localIdsByAppKind) {\n for (const localIdBatch of remoteResetBatches(localIds)) {\n await tx\n .update(documents)\n .set({\n accessEpoch: 1,\n accessStateHash: null,\n contentKeyBundle: null,\n documentId: null,\n documentKekTargets: null,\n documentManifestBundle: null,\n lastCommitLsn: null,\n pullContinuation: null,\n recoveryDocumentId: sql`coalesce(${documents.documentId}, ${documents.recoveryDocumentId})`,\n ...CLEARED_DOCUMENT_SYNC_FAILURE,\n updatedAt: now,\n })\n .where(\n and(\n eq(documents.appKind, appKind),\n inArray(documents.localId, localIdBatch),\n ),\n )\n .run();\n }\n }\n for (const localIdBatch of remoteResetBatches(input.documentLocalIds)) {\n await tx\n .update(documentProjection)\n .set({\n documentId: null,\n organizationId: input.organizationId,\n })\n .where(inArray(documentProjection.localId, localIdBatch))\n .run();\n }\n}\n\n/** Queues each document's rebuilt local history as one outbox update. */\nexport async function queueDocumentRepublishForRemoteReset(input: {\n documentUpdates: readonly ResetDocumentUpdate[];\n now: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n for (const updateBatch of remoteResetBatches(input.documentUpdates)) {\n await input.tx\n .insert(documentHistoryUpdates)\n .values(\n updateBatch.map((update) =>\n queuedUpdateRow({\n createdAt: input.now,\n origin: \"outbox\",\n pendingUpdate: update,\n scope: update,\n updateId: crypto.randomUUID(),\n }),\n ),\n )\n .run();\n }\n}\n\n/** Queues the attachments the documents still advertise for upload. */\nexport async function queueAttachmentReuploadsForRemoteReset(input: {\n attachmentUploads: readonly ResetAttachmentUpload[];\n now: string;\n tx: ClientSQLiteTransactionScope;\n}): Promise<void> {\n for (const attachmentBatch of remoteResetBatches(input.attachmentUploads)) {\n await input.tx\n .insert(documentPendingAttachments)\n .values(\n attachmentBatch.map((attachment) => ({\n ...attachment,\n createdAt: input.now,\n })),\n )\n .run();\n await input.tx\n .delete(documentAttachmentBlobProjection)\n .where(\n or(\n ...attachmentBatch.map((attachment) =>\n and(\n eq(documentAttachmentBlobProjection.localId, attachment.localId),\n eq(documentAttachmentBlobProjection.slotId, attachment.slotId),\n ),\n ),\n ),\n )\n .run();\n }\n}\n"]}
@@ -9,9 +9,14 @@ import type { ApiClient } from "../../internal/api-client/index.js";
9
9
  export interface PrincipalPolicyHistoryReader {
10
10
  readonly getPrincipalPolicyPages: ApiClient["getPrincipalPolicyPages"];
11
11
  readonly getProjectionPolicyHistoryPages: ApiClient["getProjectionPolicyHistoryPages"];
12
+ /**
13
+ * Conditional unpinned discovery. Readers that replay local or already
14
+ * fetched pages omit it, so discovery reads their first page instead.
15
+ */
16
+ readonly getPrincipalPolicyDiscovery?: ApiClient["getPrincipalPolicyDiscovery"] | undefined;
12
17
  }
13
18
  /** Reads one principal's own policy history. */
14
- export type PrincipalPolicyPageReader = Pick<PrincipalPolicyHistoryReader, "getPrincipalPolicyPages">;
19
+ export type PrincipalPolicyPageReader = Pick<PrincipalPolicyHistoryReader, "getPrincipalPolicyPages" | "getPrincipalPolicyDiscovery">;
15
20
  /** Reads an organization's public projection policy history. */
16
21
  export type ProjectionPolicyHistoryReader = Pick<PrincipalPolicyHistoryReader, "getProjectionPolicyHistoryPages">;
17
22
  /** The API client as a page reader, or undefined when this client has none. */
@@ -1,7 +1,10 @@
1
1
  /** The API client as a page reader, or undefined when this client has none. */
2
2
  export function apiPrincipalPolicyPageReader(apiClient) {
3
3
  const getPrincipalPolicyPages = apiClient.getPrincipalPolicyPages?.bind(apiClient);
4
- return getPrincipalPolicyPages ? { getPrincipalPolicyPages } : undefined;
4
+ const getPrincipalPolicyDiscovery = apiClient.getPrincipalPolicyDiscovery?.bind(apiClient);
5
+ return getPrincipalPolicyPages
6
+ ? { getPrincipalPolicyPages, getPrincipalPolicyDiscovery }
7
+ : undefined;
5
8
  }
6
9
  /** The API client as a projection reader, or undefined when it has none. */
7
10
  export function apiProjectionPolicyHistoryReader(apiClient) {
@@ -1 +1 @@
1
- {"version":3,"file":"principalPolicyHistoryReader.js","sourceRoot":"","sources":["principalPolicyHistoryReader.ts"],"names":[],"mappings":"AA0BA,+EAA+E;AAC/E,MAAM,UAAU,4BAA4B,CAC1C,SAA6C;IAE7C,MAAM,uBAAuB,GAC3B,SAAS,CAAC,uBAAuB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,OAAO,uBAAuB,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,gCAAgC,CAC9C,SAAiD;IAEjD,MAAM,+BAA+B,GACnC,SAAS,CAAC,+BAA+B,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7D,OAAO,+BAA+B;QACpC,CAAC,CAAC,EAAE,+BAA+B,EAAE;QACrC,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC","sourcesContent":["import type { ApiClient } from \"@tearleads/api-client\";\n\n/**\n * Where recovery reads signed principal policy history. The API client is the\n * production reader; registration reads a just-provisioned principal from its\n * local bundles, and directory discovery replays a page it already fetched.\n * Every page is untrusted input to the same verifier, whichever reader\n * supplied it.\n */\nexport interface PrincipalPolicyHistoryReader {\n readonly getPrincipalPolicyPages: ApiClient[\"getPrincipalPolicyPages\"];\n readonly getProjectionPolicyHistoryPages: ApiClient[\"getProjectionPolicyHistoryPages\"];\n}\n\n/** Reads one principal's own policy history. */\nexport type PrincipalPolicyPageReader = Pick<\n PrincipalPolicyHistoryReader,\n \"getPrincipalPolicyPages\"\n>;\n\n/** Reads an organization's public projection policy history. */\nexport type ProjectionPolicyHistoryReader = Pick<\n PrincipalPolicyHistoryReader,\n \"getProjectionPolicyHistoryPages\"\n>;\n\n/** The API client as a page reader, or undefined when this client has none. */\nexport function apiPrincipalPolicyPageReader(\n apiClient: Partial<PrincipalPolicyPageReader>,\n): PrincipalPolicyPageReader | undefined {\n const getPrincipalPolicyPages =\n apiClient.getPrincipalPolicyPages?.bind(apiClient);\n return getPrincipalPolicyPages ? { getPrincipalPolicyPages } : undefined;\n}\n\n/** The API client as a projection reader, or undefined when it has none. */\nexport function apiProjectionPolicyHistoryReader(\n apiClient: Partial<ProjectionPolicyHistoryReader>,\n): ProjectionPolicyHistoryReader | undefined {\n const getProjectionPolicyHistoryPages =\n apiClient.getProjectionPolicyHistoryPages?.bind(apiClient);\n return getProjectionPolicyHistoryPages\n ? { getProjectionPolicyHistoryPages }\n : undefined;\n}\n"]}
1
+ {"version":3,"file":"principalPolicyHistoryReader.js","sourceRoot":"","sources":["principalPolicyHistoryReader.ts"],"names":[],"mappings":"AAiCA,+EAA+E;AAC/E,MAAM,UAAU,4BAA4B,CAC1C,SAA6C;IAE7C,MAAM,uBAAuB,GAC3B,SAAS,CAAC,uBAAuB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,MAAM,2BAA2B,GAC/B,SAAS,CAAC,2BAA2B,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACzD,OAAO,uBAAuB;QAC5B,CAAC,CAAC,EAAE,uBAAuB,EAAE,2BAA2B,EAAE;QAC1D,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,gCAAgC,CAC9C,SAAiD;IAEjD,MAAM,+BAA+B,GACnC,SAAS,CAAC,+BAA+B,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7D,OAAO,+BAA+B;QACpC,CAAC,CAAC,EAAE,+BAA+B,EAAE;QACrC,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC","sourcesContent":["import type { ApiClient } from \"@tearleads/api-client\";\n\n/**\n * Where recovery reads signed principal policy history. The API client is the\n * production reader; registration reads a just-provisioned principal from its\n * local bundles, and directory discovery replays a page it already fetched.\n * Every page is untrusted input to the same verifier, whichever reader\n * supplied it.\n */\nexport interface PrincipalPolicyHistoryReader {\n readonly getPrincipalPolicyPages: ApiClient[\"getPrincipalPolicyPages\"];\n readonly getProjectionPolicyHistoryPages: ApiClient[\"getProjectionPolicyHistoryPages\"];\n /**\n * Conditional unpinned discovery. Readers that replay local or already\n * fetched pages omit it, so discovery reads their first page instead.\n */\n readonly getPrincipalPolicyDiscovery?:\n | ApiClient[\"getPrincipalPolicyDiscovery\"]\n | undefined;\n}\n\n/** Reads one principal's own policy history. */\nexport type PrincipalPolicyPageReader = Pick<\n PrincipalPolicyHistoryReader,\n \"getPrincipalPolicyPages\" | \"getPrincipalPolicyDiscovery\"\n>;\n\n/** Reads an organization's public projection policy history. */\nexport type ProjectionPolicyHistoryReader = Pick<\n PrincipalPolicyHistoryReader,\n \"getProjectionPolicyHistoryPages\"\n>;\n\n/** The API client as a page reader, or undefined when this client has none. */\nexport function apiPrincipalPolicyPageReader(\n apiClient: Partial<PrincipalPolicyPageReader>,\n): PrincipalPolicyPageReader | undefined {\n const getPrincipalPolicyPages =\n apiClient.getPrincipalPolicyPages?.bind(apiClient);\n const getPrincipalPolicyDiscovery =\n apiClient.getPrincipalPolicyDiscovery?.bind(apiClient);\n return getPrincipalPolicyPages\n ? { getPrincipalPolicyPages, getPrincipalPolicyDiscovery }\n : undefined;\n}\n\n/** The API client as a projection reader, or undefined when it has none. */\nexport function apiProjectionPolicyHistoryReader(\n apiClient: Partial<ProjectionPolicyHistoryReader>,\n): ProjectionPolicyHistoryReader | undefined {\n const getProjectionPolicyHistoryPages =\n apiClient.getProjectionPolicyHistoryPages?.bind(apiClient);\n return getProjectionPolicyHistoryPages\n ? { getProjectionPolicyHistoryPages }\n : undefined;\n}\n"]}
@@ -1,7 +1,8 @@
1
1
  import { KeyingVerificationError, verifyPrincipalPolicyHistoryReferences, } from "../../../internal/crypto/index.js";
2
+ import { isPrincipalPolicyPageResponse, isPrincipalPolicyUnchangedResponse, } from "../../../internal/validators/response/index.js";
2
3
  import { assertProjectionVerificationCurrent } from "../../../data/keyingProjectionVerification/types.js";
3
4
  import { loadPrincipalHistoryReference } from "../../../data/persistence/principalHistory/principalHistoryEvidencePersistence.js";
4
- import { parseOrganizationAuthorityDescriptor, } from "../../../data/principals/organizationAuthorityDescriptor.js";
5
+ import { parseOrganizationAuthorityDescriptor, principalHeadMatchesReference, } from "../../../data/principals/organizationAuthorityDescriptor.js";
5
6
  import { principalHistoryEvidenceScopeId } from "../../../data/principals/principalHistoryPrefixProtection.js";
6
7
  import { directoryHistoryProtection } from "../../../data/principals/principalHistoryScopeProtection.js";
7
8
  import { loadVerifiedPrincipalHistoryPrefix } from "../historyRecovery/principalHistoryRecoveryPrefix.js";
@@ -20,72 +21,120 @@ function exactHead(state) {
20
21
  keyFingerprint: state.keyFingerprint,
21
22
  };
22
23
  }
23
- /** An unverified discovery read chooses a pin; recovery must authenticate it. */
24
- async function discoverDirectoryHead(input) {
24
+ function assertDiscoveryCurrent(input) {
25
25
  assertProjectionVerificationCurrent(() => !input.signal?.aborted && input.stillCurrent());
26
- if (input.offline) {
27
- const principal = {
26
+ }
27
+ /**
28
+ * The directory head and genesis this device has completely authenticated,
29
+ * from its protected verified prefix, or null when it holds none.
30
+ */
31
+ async function loadHeldDirectoryHead(input) {
32
+ const principal = {
33
+ principalType: "organization",
34
+ principalId: input.organizationId,
35
+ };
36
+ const scopeId = await principalHistoryEvidenceScopeId({
37
+ organizationId: input.organizationId,
38
+ head: principal,
39
+ protection: input.protection,
40
+ });
41
+ const prefix = await loadVerifiedPrincipalHistoryPrefix(input, principal, scopeId);
42
+ if (!prefix)
43
+ return null;
44
+ const history = prefix.verifier.finish(prefix.head);
45
+ if (!history.ok)
46
+ throw history.error;
47
+ const genesis = await loadPrincipalHistoryReference({
48
+ execSql: input.execSql,
49
+ scopeId,
50
+ history: history.value,
51
+ version: 1,
52
+ });
53
+ const selected = await verifyPrincipalPolicyHistoryReferences({
54
+ history: history.value,
55
+ references: [genesis],
56
+ });
57
+ if (!selected.ok)
58
+ throw selected.error;
59
+ return { head: exactHead(prefix.head), genesis: genesis.reference };
60
+ }
61
+ /** Disposable local evidence never refuses an online discovery; it reads fully. */
62
+ async function loadHeldDirectoryHeadOnline(input) {
63
+ try {
64
+ return await loadHeldDirectoryHead(input);
65
+ }
66
+ catch {
67
+ assertDiscoveryCurrent(input);
68
+ return null;
69
+ }
70
+ }
71
+ function discoveredPage(input, result) {
72
+ const state = result.data.currentState;
73
+ const first = result.data.previousStates[0]?.state ?? state;
74
+ if (first.version !== 1 ||
75
+ first.principalType !== "organization" ||
76
+ first.principalId !== input.organizationId)
77
+ throw new KeyingVerificationError("missing_dependency", "Organization directory discovery requires genesis");
78
+ return {
79
+ historyReader: reusePrincipalDiscoveryPage(input.historyReader, result),
80
+ genesis: exactHead(first),
81
+ // The requested identity, never the page's: discovery picks only a pin.
82
+ head: {
28
83
  principalType: "organization",
29
84
  principalId: input.organizationId,
30
- };
31
- const scopeId = await principalHistoryEvidenceScopeId({
32
- organizationId: input.organizationId,
33
- head: principal,
34
- protection: input.protection,
35
- });
36
- const prefix = await loadVerifiedPrincipalHistoryPrefix(input, principal, scopeId);
37
- if (!prefix)
85
+ version: state.version,
86
+ stateHash: state.stateHash,
87
+ keyEpoch: state.keyEpoch,
88
+ keyFingerprint: state.keyFingerprint,
89
+ },
90
+ };
91
+ }
92
+ /**
93
+ * Ask whether the held head is still live. An `unchanged` reply naming exactly
94
+ * that head reuses it; recovery then authenticates the held artifacts again
95
+ * and reloads the checkpoint. Any other reply that is not a page leaves the
96
+ * caller to read the first page unconditionally.
97
+ */
98
+ async function discoverConditionally(input, discover) {
99
+ const held = await loadHeldDirectoryHeadOnline(input);
100
+ assertDiscoveryCurrent(input);
101
+ const result = await discover("organization", input.organizationId, {
102
+ signal: input.signal,
103
+ ifHead: held?.head,
104
+ });
105
+ if (!result.ok)
106
+ throw new PrincipalPolicyHistoryReadError(result);
107
+ assertDiscoveryCurrent(input);
108
+ const reply = result.data;
109
+ if (isPrincipalPolicyUnchangedResponse(reply))
110
+ return held && principalHeadMatchesReference(reply.head, held.head)
111
+ ? { ...held, historyReader: input.historyReader }
112
+ : null;
113
+ // Anything but the strict bare head must be a page; otherwise read again.
114
+ return isPrincipalPolicyPageResponse(reply)
115
+ ? discoveredPage(input, { ok: true, data: reply })
116
+ : null;
117
+ }
118
+ /** An unverified discovery read chooses a pin; recovery must authenticate it. */
119
+ async function discoverDirectoryHead(input) {
120
+ assertDiscoveryCurrent(input);
121
+ if (input.offline) {
122
+ const held = await loadHeldDirectoryHead(input);
123
+ if (!held)
38
124
  throw new KeyingVerificationError("missing_dependency", "Organization directory is unavailable offline");
39
- const history = prefix.verifier.finish(prefix.head);
40
- if (!history.ok)
41
- throw history.error;
42
- const genesis = await loadPrincipalHistoryReference({
43
- execSql: input.execSql,
44
- scopeId,
45
- history: history.value,
46
- version: 1,
47
- });
48
- const selected = await verifyPrincipalPolicyHistoryReferences({
49
- history: history.value,
50
- references: [genesis],
51
- });
52
- if (!selected.ok)
53
- throw selected.error;
54
- return {
55
- head: prefix.head,
56
- genesis: genesis.reference,
57
- historyReader: input.historyReader,
58
- };
125
+ return { ...held, historyReader: input.historyReader };
59
126
  }
127
+ const discover = input.historyReader.getPrincipalPolicyDiscovery;
128
+ const conditional = discover
129
+ ? await discoverConditionally(input, discover)
130
+ : null;
131
+ if (conditional)
132
+ return conditional;
60
133
  for await (const result of input.historyReader.getPrincipalPolicyPages("organization", input.organizationId, { signal: input.signal })) {
61
134
  if (!result.ok)
62
135
  throw new PrincipalPolicyHistoryReadError(result);
63
- assertProjectionVerificationCurrent(() => !input.signal?.aborted && input.stillCurrent());
64
- const state = result.data.currentState;
65
- const first = result.data.previousStates[0]?.state ?? state;
66
- if (first.version !== 1 ||
67
- first.principalType !== "organization" ||
68
- first.principalId !== input.organizationId)
69
- throw new KeyingVerificationError("missing_dependency", "Organization directory discovery requires genesis");
70
- return {
71
- historyReader: reusePrincipalDiscoveryPage(input.historyReader, result),
72
- genesis: {
73
- principalType: first.principalType,
74
- principalId: first.principalId,
75
- version: first.version,
76
- stateHash: first.stateHash,
77
- keyEpoch: first.keyEpoch,
78
- keyFingerprint: first.keyFingerprint,
79
- },
80
- head: {
81
- principalType: "organization",
82
- principalId: input.organizationId,
83
- version: state.version,
84
- stateHash: state.stateHash,
85
- keyEpoch: state.keyEpoch,
86
- keyFingerprint: state.keyFingerprint,
87
- },
88
- };
136
+ assertDiscoveryCurrent(input);
137
+ return discoveredPage(input, result);
89
138
  }
90
139
  throw new KeyingVerificationError("missing_dependency", "Organization directory head is unavailable");
91
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"principalRecoveryDirectory.js","sourceRoot":"","sources":["principalRecoveryDirectory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n KeyingVerificationError,\n type ReferencedPrincipalHead,\n verifyPrincipalPolicyHistoryReferences,\n} from \"@tearleads/crypto\";\nimport { assertProjectionVerificationCurrent } from \"../../../data/keyingProjectionVerification/types\";\nimport { loadPrincipalHistoryReference } from \"../../../data/persistence/principalHistory/principalHistoryEvidencePersistence\";\nimport {\n type OrganizationAuthorityDescriptor,\n parseOrganizationAuthorityDescriptor,\n} from \"../../../data/principals/organizationAuthorityDescriptor\";\nimport { principalHistoryEvidenceScopeId } from \"../../../data/principals/principalHistoryPrefixProtection\";\nimport { directoryHistoryProtection } from \"../../../data/principals/principalHistoryScopeProtection\";\nimport { loadVerifiedPrincipalHistoryPrefix } from \"../historyRecovery/principalHistoryRecoveryPrefix\";\nimport {\n PrincipalPolicyHistoryReadError,\n type RecoveredPrincipalPolicyHistory,\n type RecoverPrincipalPolicyHistoryOptions,\n} from \"../historyRecovery/principalHistoryRecoveryTypes\";\nimport { recoverPrincipalPolicyHistory } from \"../historyRecovery/recoverPrincipalPolicyHistory\";\nimport { reusePrincipalDiscoveryPage } from \"./principalRecoveryDiscoveryPage\";\n\nexport type PrincipalRecoveryContext = Omit<\n RecoverPrincipalPolicyHistoryOptions,\n | \"expectedHead\"\n | \"retainedReferences\"\n | \"historyVerification\"\n | \"loadExternalAuthority\"\n>;\n\nexport interface RecoveredPolicyDirectory\n extends RecoveredPrincipalPolicyHistory {\n readonly descriptor: OrganizationAuthorityDescriptor;\n}\n\nexport class PrincipalRecoveryDirectoryAdvanced extends Error {}\n\nfunction exactHead(state: ReferencedPrincipalHead): ReferencedPrincipalHead {\n return {\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 };\n}\n\n/** An unverified discovery read chooses a pin; recovery must authenticate it. */\nasync function discoverDirectoryHead(input: PrincipalRecoveryContext): Promise<{\n head: ReferencedPrincipalHead;\n genesis: ReferencedPrincipalHead;\n historyReader: PrincipalRecoveryContext[\"historyReader\"];\n}> {\n assertProjectionVerificationCurrent(\n () => !input.signal?.aborted && input.stillCurrent(),\n );\n if (input.offline) {\n const principal = {\n principalType: \"organization\" as const,\n principalId: input.organizationId,\n };\n const scopeId = await principalHistoryEvidenceScopeId({\n organizationId: input.organizationId,\n head: principal,\n protection: input.protection,\n });\n const prefix = await loadVerifiedPrincipalHistoryPrefix(\n input,\n principal,\n scopeId,\n );\n if (!prefix)\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory is unavailable offline\",\n );\n const history = prefix.verifier.finish(prefix.head);\n if (!history.ok) throw history.error;\n const genesis = await loadPrincipalHistoryReference({\n execSql: input.execSql,\n scopeId,\n history: history.value,\n version: 1,\n });\n const selected = await verifyPrincipalPolicyHistoryReferences({\n history: history.value,\n references: [genesis],\n });\n if (!selected.ok) throw selected.error;\n return {\n head: prefix.head,\n genesis: genesis.reference,\n historyReader: input.historyReader,\n };\n }\n for await (const result of input.historyReader.getPrincipalPolicyPages(\n \"organization\",\n input.organizationId,\n { signal: input.signal },\n )) {\n if (!result.ok) throw new PrincipalPolicyHistoryReadError(result);\n assertProjectionVerificationCurrent(\n () => !input.signal?.aborted && input.stillCurrent(),\n );\n const state = result.data.currentState;\n const first = result.data.previousStates[0]?.state ?? state;\n if (\n first.version !== 1 ||\n first.principalType !== \"organization\" ||\n first.principalId !== input.organizationId\n )\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory discovery requires genesis\",\n );\n return {\n historyReader: reusePrincipalDiscoveryPage(input.historyReader, result),\n genesis: {\n principalType: first.principalType,\n principalId: first.principalId,\n version: first.version,\n stateHash: first.stateHash,\n keyEpoch: first.keyEpoch,\n keyFingerprint: first.keyFingerprint,\n },\n head: {\n principalType: \"organization\",\n principalId: input.organizationId,\n version: state.version,\n stateHash: state.stateHash,\n keyEpoch: state.keyEpoch,\n keyFingerprint: state.keyFingerprint,\n },\n };\n }\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory head is unavailable\",\n );\n}\n\nexport async function recoverPolicyDirectory(\n input: PrincipalRecoveryContext,\n references: readonly ReferencedPrincipalHead[],\n selected?: RecoveredPolicyDirectory,\n): Promise<RecoveredPolicyDirectory> {\n const scoped = {\n ...input,\n protection: directoryHistoryProtection(input.protection),\n };\n const {\n head: expectedHead,\n genesis,\n historyReader,\n } = selected\n ? {\n head: exactHead(selected.policy.state),\n genesis: selected.policy.retainedHistory[0]?.state,\n historyReader: input.historyReader,\n }\n : await discoverDirectoryHead(scoped);\n if (genesis?.version !== 1)\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory recovery requires verified genesis\",\n );\n if (references.some((reference) => reference.version > expectedHead.version))\n throw new PrincipalRecoveryDirectoryAdvanced();\n const recovered = await recoverPrincipalPolicyHistory({\n ...scoped,\n historyReader,\n expectedHead,\n retainedReferences: references.some((reference) => reference.version === 1)\n ? references\n : [exactHead(genesis), ...references],\n });\n let descriptor: OrganizationAuthorityDescriptor;\n try {\n descriptor = parseOrganizationAuthorityDescriptor(\n recovered.current.currentPayload.ciphertext,\n );\n } catch {\n throw new KeyingVerificationError(\n \"invalid_shape\",\n \"Organization directory payload is invalid\",\n );\n }\n if (descriptor.organizationId !== input.organizationId)\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Organization directory scope does not match\",\n );\n return { ...recovered, descriptor };\n}\n"]}
1
+ {"version":3,"file":"principalRecoveryDirectory.js","sourceRoot":"","sources":["principalRecoveryDirectory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RequestSuccess } from \"@tearleads/api-client\";\nimport {\n KeyingVerificationError,\n type ReferencedPrincipalHead,\n verifyPrincipalPolicyHistoryReferences,\n} from \"@tearleads/crypto\";\nimport {\n isPrincipalPolicyPageResponse,\n isPrincipalPolicyUnchangedResponse,\n type PrincipalPolicyPageResponse,\n} from \"@tearleads/validators/response\";\nimport { assertProjectionVerificationCurrent } from \"../../../data/keyingProjectionVerification/types\";\nimport { loadPrincipalHistoryReference } from \"../../../data/persistence/principalHistory/principalHistoryEvidencePersistence\";\nimport {\n type OrganizationAuthorityDescriptor,\n parseOrganizationAuthorityDescriptor,\n principalHeadMatchesReference,\n} from \"../../../data/principals/organizationAuthorityDescriptor\";\nimport { principalHistoryEvidenceScopeId } from \"../../../data/principals/principalHistoryPrefixProtection\";\nimport { directoryHistoryProtection } from \"../../../data/principals/principalHistoryScopeProtection\";\nimport { loadVerifiedPrincipalHistoryPrefix } from \"../historyRecovery/principalHistoryRecoveryPrefix\";\nimport {\n PrincipalPolicyHistoryReadError,\n type RecoveredPrincipalPolicyHistory,\n type RecoverPrincipalPolicyHistoryOptions,\n} from \"../historyRecovery/principalHistoryRecoveryTypes\";\nimport { recoverPrincipalPolicyHistory } from \"../historyRecovery/recoverPrincipalPolicyHistory\";\nimport { reusePrincipalDiscoveryPage } from \"./principalRecoveryDiscoveryPage\";\n\nexport type PrincipalRecoveryContext = Omit<\n RecoverPrincipalPolicyHistoryOptions,\n | \"expectedHead\"\n | \"retainedReferences\"\n | \"historyVerification\"\n | \"loadExternalAuthority\"\n>;\n\nexport interface RecoveredPolicyDirectory\n extends RecoveredPrincipalPolicyHistory {\n readonly descriptor: OrganizationAuthorityDescriptor;\n}\n\nexport class PrincipalRecoveryDirectoryAdvanced extends Error {}\n\nfunction exactHead(state: ReferencedPrincipalHead): ReferencedPrincipalHead {\n return {\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 };\n}\n\ninterface DiscoveredDirectoryHead {\n readonly head: ReferencedPrincipalHead;\n readonly genesis: ReferencedPrincipalHead;\n readonly historyReader: PrincipalRecoveryContext[\"historyReader\"];\n}\n\nfunction assertDiscoveryCurrent(input: PrincipalRecoveryContext): void {\n assertProjectionVerificationCurrent(\n () => !input.signal?.aborted && input.stillCurrent(),\n );\n}\n\n/**\n * The directory head and genesis this device has completely authenticated,\n * from its protected verified prefix, or null when it holds none.\n */\nasync function loadHeldDirectoryHead(\n input: PrincipalRecoveryContext,\n): Promise<Omit<DiscoveredDirectoryHead, \"historyReader\"> | null> {\n const principal = {\n principalType: \"organization\" as const,\n principalId: input.organizationId,\n };\n const scopeId = await principalHistoryEvidenceScopeId({\n organizationId: input.organizationId,\n head: principal,\n protection: input.protection,\n });\n const prefix = await loadVerifiedPrincipalHistoryPrefix(\n input,\n principal,\n scopeId,\n );\n if (!prefix) return null;\n const history = prefix.verifier.finish(prefix.head);\n if (!history.ok) throw history.error;\n const genesis = await loadPrincipalHistoryReference({\n execSql: input.execSql,\n scopeId,\n history: history.value,\n version: 1,\n });\n const selected = await verifyPrincipalPolicyHistoryReferences({\n history: history.value,\n references: [genesis],\n });\n if (!selected.ok) throw selected.error;\n return { head: exactHead(prefix.head), genesis: genesis.reference };\n}\n\n/** Disposable local evidence never refuses an online discovery; it reads fully. */\nasync function loadHeldDirectoryHeadOnline(input: PrincipalRecoveryContext) {\n try {\n return await loadHeldDirectoryHead(input);\n } catch {\n assertDiscoveryCurrent(input);\n return null;\n }\n}\n\nfunction discoveredPage(\n input: PrincipalRecoveryContext,\n result: RequestSuccess<PrincipalPolicyPageResponse>,\n): DiscoveredDirectoryHead {\n const state = result.data.currentState;\n const first = result.data.previousStates[0]?.state ?? state;\n if (\n first.version !== 1 ||\n first.principalType !== \"organization\" ||\n first.principalId !== input.organizationId\n )\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory discovery requires genesis\",\n );\n return {\n historyReader: reusePrincipalDiscoveryPage(input.historyReader, result),\n genesis: exactHead(first),\n // The requested identity, never the page's: discovery picks only a pin.\n head: {\n principalType: \"organization\",\n principalId: input.organizationId,\n version: state.version,\n stateHash: state.stateHash,\n keyEpoch: state.keyEpoch,\n keyFingerprint: state.keyFingerprint,\n },\n };\n}\n\n/**\n * Ask whether the held head is still live. An `unchanged` reply naming exactly\n * that head reuses it; recovery then authenticates the held artifacts again\n * and reloads the checkpoint. Any other reply that is not a page leaves the\n * caller to read the first page unconditionally.\n */\nasync function discoverConditionally(\n input: PrincipalRecoveryContext,\n discover: NonNullable<\n PrincipalRecoveryContext[\"historyReader\"][\"getPrincipalPolicyDiscovery\"]\n >,\n): Promise<DiscoveredDirectoryHead | null> {\n const held = await loadHeldDirectoryHeadOnline(input);\n assertDiscoveryCurrent(input);\n const result = await discover(\"organization\", input.organizationId, {\n signal: input.signal,\n ifHead: held?.head,\n });\n if (!result.ok) throw new PrincipalPolicyHistoryReadError(result);\n assertDiscoveryCurrent(input);\n const reply = result.data;\n if (isPrincipalPolicyUnchangedResponse(reply))\n return held && principalHeadMatchesReference(reply.head, held.head)\n ? { ...held, historyReader: input.historyReader }\n : null;\n // Anything but the strict bare head must be a page; otherwise read again.\n return isPrincipalPolicyPageResponse(reply)\n ? discoveredPage(input, { ok: true, data: reply })\n : null;\n}\n\n/** An unverified discovery read chooses a pin; recovery must authenticate it. */\nasync function discoverDirectoryHead(\n input: PrincipalRecoveryContext,\n): Promise<DiscoveredDirectoryHead> {\n assertDiscoveryCurrent(input);\n if (input.offline) {\n const held = await loadHeldDirectoryHead(input);\n if (!held)\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory is unavailable offline\",\n );\n return { ...held, historyReader: input.historyReader };\n }\n const discover = input.historyReader.getPrincipalPolicyDiscovery;\n const conditional = discover\n ? await discoverConditionally(input, discover)\n : null;\n if (conditional) return conditional;\n for await (const result of input.historyReader.getPrincipalPolicyPages(\n \"organization\",\n input.organizationId,\n { signal: input.signal },\n )) {\n if (!result.ok) throw new PrincipalPolicyHistoryReadError(result);\n assertDiscoveryCurrent(input);\n return discoveredPage(input, result);\n }\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory head is unavailable\",\n );\n}\n\nexport async function recoverPolicyDirectory(\n input: PrincipalRecoveryContext,\n references: readonly ReferencedPrincipalHead[],\n selected?: RecoveredPolicyDirectory,\n): Promise<RecoveredPolicyDirectory> {\n const scoped = {\n ...input,\n protection: directoryHistoryProtection(input.protection),\n };\n const {\n head: expectedHead,\n genesis,\n historyReader,\n } = selected\n ? {\n head: exactHead(selected.policy.state),\n genesis: selected.policy.retainedHistory[0]?.state,\n historyReader: input.historyReader,\n }\n : await discoverDirectoryHead(scoped);\n if (genesis?.version !== 1)\n throw new KeyingVerificationError(\n \"missing_dependency\",\n \"Organization directory recovery requires verified genesis\",\n );\n if (references.some((reference) => reference.version > expectedHead.version))\n throw new PrincipalRecoveryDirectoryAdvanced();\n const recovered = await recoverPrincipalPolicyHistory({\n ...scoped,\n historyReader,\n expectedHead,\n retainedReferences: references.some((reference) => reference.version === 1)\n ? references\n : [exactHead(genesis), ...references],\n });\n let descriptor: OrganizationAuthorityDescriptor;\n try {\n descriptor = parseOrganizationAuthorityDescriptor(\n recovered.current.currentPayload.ciphertext,\n );\n } catch {\n throw new KeyingVerificationError(\n \"invalid_shape\",\n \"Organization directory payload is invalid\",\n );\n }\n if (descriptor.organizationId !== input.organizationId)\n throw new KeyingVerificationError(\n \"object_mismatch\",\n \"Organization directory scope does not match\",\n );\n return { ...recovered, descriptor };\n}\n"]}
@@ -3,7 +3,7 @@ import { ProjectionDependencyUnavailableError } from "../../data/keyingProjectio
3
3
  import { runWithSecurityIncidentReporting } from "../../data/keyingProjectionVerification/error.js";
4
4
  import { assertProjectionVerificationCurrent, } from "../../data/keyingProjectionVerification/types.js";
5
5
  import { readPrincipalHistoryProtection } from "../../data/principals/principalHistoryRuntime.js";
6
- import { getOrCreatePrincipalPolicyCoordinator } from "../../data/principals/principalPolicyCoordinator.js";
6
+ import { getOrCreatePrincipalPolicyCoordinator, } from "../../data/principals/principalPolicyCoordinator.js";
7
7
  import { PrincipalHistoryRecoveryRaceError, PrincipalPolicyHistoryReadError, } from "./historyRecovery/principalHistoryRecoveryTypes.js";
8
8
  import { apiPrincipalPolicyPageReader, } from "./principalPolicyHistoryReader.js";
9
9
  import { createPrincipalCurrentRecoveryBatch } from "./recoveryOperation/principalCurrentRecoveryBatch.js";
@@ -16,6 +16,28 @@ export function createRuntimePrincipalPolicyResolver(runtime, options = {}) {
16
16
  ? (input) => resolve({ ...input, preferLocalCurrent: options.preferLocalCurrent })
17
17
  : undefined;
18
18
  }
19
+ /**
20
+ * Operation batches belong to the database's coordinator, keyed by the
21
+ * operation token, so every resolver and warmer one operation creates for that
22
+ * database shares one authenticated view. A batch dies with its token (the end
23
+ * of the pass or workflow) and its entries with any organization hint.
24
+ */
25
+ const operationBatches = new WeakMap();
26
+ function operationRecoveryBatch(coordinator, operation) {
27
+ if (!operation)
28
+ return createPrincipalCurrentRecoveryBatch();
29
+ let batches = operationBatches.get(coordinator);
30
+ if (!batches) {
31
+ batches = new WeakMap();
32
+ operationBatches.set(coordinator, batches);
33
+ }
34
+ let batch = batches.get(operation);
35
+ if (!batch) {
36
+ batch = createPrincipalCurrentRecoveryBatch();
37
+ batches.set(operation, batch);
38
+ }
39
+ return batch;
40
+ }
19
41
  /** One recovery attempt for a resolver request through the operation's batch. */
20
42
  function recoverPolicyCurrent(batch, options, input) {
21
43
  return input.reference
@@ -32,18 +54,8 @@ export function createRuntimePrincipalPolicyCurrentResolver(runtime) {
32
54
  const historyReader = apiPrincipalPolicyPageReader(runtime.apiClient);
33
55
  if (!lease || !historyReader)
34
56
  return undefined;
35
- const batches = new WeakMap();
36
- const recoverFor = (batch) => {
37
- if (!batch)
38
- return createPrincipalCurrentRecoveryBatch();
39
- let recover = batches.get(batch);
40
- if (!recover) {
41
- recover = createPrincipalCurrentRecoveryBatch();
42
- batches.set(batch, recover);
43
- }
44
- return recover;
45
- };
46
- return (input) => getOrCreatePrincipalPolicyCoordinator(runtime.infra.execSql).queueRecovery(input.organizationId, () => runWithSecurityIncidentReporting(runtime.util.reportSecurityIncident, {
57
+ const coordinator = getOrCreatePrincipalPolicyCoordinator(runtime.infra.execSql);
58
+ return (input) => coordinator.queueRecovery(input.organizationId, () => runWithSecurityIncidentReporting(runtime.util.reportSecurityIncident, {
47
59
  objectId: input.reference?.principalId ?? input.organizationId,
48
60
  objectKind: "principal",
49
61
  operation: "principal.policy.recover",
@@ -63,7 +75,7 @@ export function createRuntimePrincipalPolicyCurrentResolver(runtime) {
63
75
  resolveTrustedUserIdentity: runtime.resolveTrustedUserIdentity,
64
76
  stillCurrent,
65
77
  };
66
- const result = await resumeAfterSupersededStage(() => recoverPolicyCurrent(recoverFor(input.recoveryBatch), options, input), stillCurrent);
78
+ const result = await resumeAfterSupersededStage(() => recoverPolicyCurrent(operationRecoveryBatch(coordinator, input.recoveryBatch), options, input), stillCurrent);
67
79
  // Reused evidence stays bound to every producer lifetime.
68
80
  const resultCurrent = () => stillCurrent() && result.stillCurrent();
69
81
  assertProjectionVerificationCurrent(resultCurrent);