@tearleads/client-sdk 0.1.116 → 0.1.118

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 (87) hide show
  1. package/data/keyingProjectionVerification/checkpoints/checkpointContext.d.ts +8 -1
  2. package/data/keyingProjectionVerification/checkpoints/checkpointContext.js +15 -0
  3. package/data/keyingProjectionVerification/checkpoints/checkpointContext.js.map +1 -1
  4. package/data/keyingProjectionVerification/container/containerProjectionVerification.js +3 -1
  5. package/data/keyingProjectionVerification/container/containerProjectionVerification.js.map +1 -1
  6. package/data/keyingProjectionVerification/document/documentProjectionVerification.js +2 -1
  7. package/data/keyingProjectionVerification/document/documentProjectionVerification.js.map +1 -1
  8. package/data/persistence/container-contents/containerContentsPersistence.js +14 -13
  9. package/data/persistence/container-contents/containerContentsPersistence.js.map +1 -1
  10. package/data/persistence/documents/internal/ensureDocumentsSchema.js +10 -12
  11. package/data/persistence/documents/internal/ensureDocumentsSchema.js.map +1 -1
  12. package/data/persistence/verifiedCitations/citationScope.js +121 -0
  13. package/data/persistence/verifiedCitations/citationScope.js.map +1 -0
  14. package/data/persistence/verifiedCitations/kekLevelCitations.d.ts +33 -0
  15. package/data/persistence/verifiedCitations/kekLevelCitations.js +44 -0
  16. package/data/persistence/verifiedCitations/kekLevelCitations.js.map +1 -0
  17. package/data/persistence/verifiedManifestBundlePersistence.d.ts +17 -12
  18. package/data/persistence/verifiedManifestBundlePersistence.js +75 -104
  19. package/data/persistence/verifiedManifestBundlePersistence.js.map +1 -1
  20. package/data/sqlite/schema/containerSchema.d.ts +1 -0
  21. package/data/sqlite/schema/containerSchema.js +8 -1
  22. package/data/sqlite/schema/containerSchema.js.map +1 -1
  23. package/data/sqlite/schema/documentAttachmentSchema.d.ts +0 -2
  24. package/data/sqlite/schema/documentAttachmentSchema.js +0 -4
  25. package/data/sqlite/schema/documentAttachmentSchema.js.map +1 -1
  26. package/data/sqlite/schema/index.d.ts +2 -0
  27. package/data/sqlite/schema/index.js +10 -1
  28. package/data/sqlite/schema/index.js.map +1 -1
  29. package/data/sqlite/schema/principalPolicySchema.js +0 -1
  30. package/data/sqlite/schema/principalPolicySchema.js.map +1 -1
  31. package/data/sqlite/schema/verifiedManifestBundleSchema.d.ts +7 -2
  32. package/data/sqlite/schema/verifiedManifestBundleSchema.js +11 -4
  33. package/data/sqlite/schema/verifiedManifestBundleSchema.js.map +1 -1
  34. package/data/sqlite/sqlExec.d.ts +12 -0
  35. package/data/sqlite/sqlExec.js +47 -3
  36. package/data/sqlite/sqlExec.js.map +1 -1
  37. package/data/sqlite/sqlTableSchema.js +40 -15
  38. package/data/sqlite/sqlTableSchema.js.map +1 -1
  39. package/data/sqlite/sqlitePersistenceRuntime.js +18 -10
  40. package/data/sqlite/sqlitePersistenceRuntime.js.map +1 -1
  41. package/internal/api-client/ApiClient.d.ts +3 -2
  42. package/internal/api-client/ApiClient.js +3 -2
  43. package/internal/api-client/ApiClient.js.map +1 -1
  44. package/internal/api-client/index.d.ts +1 -1
  45. package/internal/api-client/index.js.map +1 -1
  46. package/internal/api-client/types.d.ts +7 -0
  47. package/internal/api-client/types.js.map +1 -1
  48. package/internal/api-client/verifiedManifestCitations.d.ts +26 -9
  49. package/internal/api-client/verifiedManifestCitations.js +44 -25
  50. package/internal/api-client/verifiedManifestCitations.js.map +1 -1
  51. package/internal/api-client/writerProjectionRequests.js +21 -10
  52. package/internal/api-client/writerProjectionRequests.js.map +1 -1
  53. package/internal/crypto/index.d.ts +1 -1
  54. package/internal/crypto/index.js +1 -1
  55. package/internal/crypto/index.js.map +1 -1
  56. package/internal/crypto/keying/canonicalJsonGuard.d.ts +7 -0
  57. package/internal/crypto/keying/canonicalJsonGuard.js +20 -0
  58. package/internal/crypto/keying/canonicalJsonGuard.js.map +1 -1
  59. package/internal/crypto/keying/index.d.ts +1 -1
  60. package/internal/crypto/keying/index.js +1 -1
  61. package/internal/crypto/keying/index.js.map +1 -1
  62. package/internal/validators/knownManifestsRefinements.js +1 -1
  63. package/internal/validators/knownManifestsRefinements.js.map +1 -1
  64. package/internal/validators/operation/writerProjections.d.ts +42 -18
  65. package/internal/validators/response/container.d.ts +137 -16
  66. package/internal/validators/response/container.js +23 -2
  67. package/internal/validators/response/container.js.map +1 -1
  68. package/internal/validators/response/documentMutation.d.ts +20 -8
  69. package/internal/validators/response/index.d.ts +2 -2
  70. package/internal/validators/response/index.js +2 -2
  71. package/internal/validators/response/index.js.map +1 -1
  72. package/internal/validators/response/writerProjectionWire.d.ts +19 -8
  73. package/internal/validators/response/writerProjectionWire.js +78 -20
  74. package/internal/validators/response/writerProjectionWire.js.map +1 -1
  75. package/internal/validators/util/index.d.ts +1 -1
  76. package/internal/validators/util/index.js +1 -1
  77. package/internal/validators/util/index.js.map +1 -1
  78. package/internal/validators/util/knownManifestWire.d.ts +13 -3
  79. package/internal/validators/util/knownManifestWire.js +37 -10
  80. package/internal/validators/util/knownManifestWire.js.map +1 -1
  81. package/package.json +1 -1
  82. package/workflows/container-contents/remoteHydration/destination/destinationPrefetch.js +10 -5
  83. package/workflows/container-contents/remoteHydration/destination/destinationPrefetch.js.map +1 -1
  84. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js +4 -6
  85. package/workflows/container-contents/remoteHydration/remoteContainers/remoteContainerPageHydration.js.map +1 -1
  86. package/workflows/container-contents/remoteHydration/types.d.ts +2 -2
  87. package/workflows/container-contents/remoteHydration/types.js.map +1 -1
@@ -0,0 +1,121 @@
1
+ import { MAX_CONTAINER_PATH_LENGTH } from "../../../internal/validators/util/index.js";
2
+ import { and, eq, inArray } from "drizzle-orm";
3
+ import { documentContainerProjection } from "../../sqlite/schema/index.js";
4
+ import { containers } from "../../sqlite/schema/containerSchema.js";
5
+ import { documentDiscoveryHeads, pendingDocumentDiscoveries, } from "../../sqlite/schema/documentDiscoveryEvidenceSchema.js";
6
+ import { verifiedAccessManifestBundles as bundles } from "../../sqlite/schema/verifiedManifestBundleSchema.js";
7
+ /** The requested object first, then each container whose lineage it cites. */
8
+ async function containerLineage(db, containerId, prefix = []) {
9
+ const lineage = [...prefix, containerId];
10
+ while (lineage.length < MAX_CONTAINER_PATH_LENGTH) {
11
+ const [row] = await db
12
+ .select({ parentId: containers.parentId })
13
+ .from(containers)
14
+ .where(eq(containers.id, lineage.at(-1) ?? ""))
15
+ .limit(1);
16
+ const parentId = row?.parentId;
17
+ if (!parentId || lineage.includes(parentId))
18
+ break;
19
+ lineage.push(parentId);
20
+ }
21
+ return lineage;
22
+ }
23
+ /**
24
+ * A container the device has not stored yet has no local parent, so its
25
+ * listing names one: the lineage continues through the listed parent's
26
+ * stored lineage. A wrong parent only changes which held hashes are cited.
27
+ */
28
+ async function containerTargetLineage(db, target) {
29
+ const parentId = target.listing?.parentId;
30
+ if (!parentId || parentId === target.id)
31
+ return containerLineage(db, target.id);
32
+ const [stored] = await db
33
+ .select({ id: containers.id })
34
+ .from(containers)
35
+ .where(eq(containers.id, target.id))
36
+ .limit(1);
37
+ return stored
38
+ ? containerLineage(db, target.id)
39
+ : containerLineage(db, parentId, [target.id]);
40
+ }
41
+ export async function citedObjects(db, target) {
42
+ if (target.kind === "container")
43
+ return (await containerTargetLineage(db, target)).map((id) => ({
44
+ kind: "container",
45
+ id,
46
+ }));
47
+ // Linked containers, containers listing the document before its signed
48
+ // placement is checked, and a container whose metadata document it is.
49
+ const placements = [
50
+ ...(await db
51
+ .select({ containerId: documentContainerProjection.containerId })
52
+ .from(documentContainerProjection)
53
+ .where(eq(documentContainerProjection.documentId, target.id))),
54
+ ...(await db
55
+ .select({ containerId: pendingDocumentDiscoveries.containerId })
56
+ .from(pendingDocumentDiscoveries)
57
+ .where(eq(pendingDocumentDiscoveries.documentId, target.id))),
58
+ ...(await db
59
+ .select({ containerId: containers.id })
60
+ .from(containers)
61
+ .where(eq(containers.metadataDocumentId, target.id))),
62
+ ];
63
+ const containerIds = new Set();
64
+ for (const { containerId } of placements)
65
+ if (containerId)
66
+ for (const id of await containerLineage(db, containerId))
67
+ containerIds.add(id);
68
+ return [
69
+ { kind: "document", id: target.id },
70
+ ...[...containerIds].map((id) => ({ kind: "container", id })),
71
+ ];
72
+ }
73
+ /**
74
+ * The one organization every local source places the read in. Verified
75
+ * ownership (stored verified bundles of the target and of the containers on
76
+ * its walk, and a document's verified discovery head) and local placement
77
+ * (listed container rows reached through links, pending listings, metadata,
78
+ * or parents) must all agree: any disagreement, or no source at all, yields
79
+ * null, so the read is sent with no hints rather than another
80
+ * organization's.
81
+ */
82
+ export async function targetOrganization(db, target, objects) {
83
+ const containerIds = objects
84
+ .filter((object) => object.kind === "container")
85
+ .map((object) => object.id);
86
+ const listed = containerIds.length === 0
87
+ ? []
88
+ : await db
89
+ .select({ organizationId: containers.organizationId })
90
+ .from(containers)
91
+ .where(inArray(containers.id, containerIds));
92
+ const verifiedContainers = containerIds.length === 0
93
+ ? []
94
+ : await db
95
+ .select({ organizationId: bundles.organizationId })
96
+ .from(bundles)
97
+ .where(and(eq(bundles.objectKind, "container"), inArray(bundles.objectId, containerIds)));
98
+ const verifiedTarget = await db
99
+ .select({ organizationId: bundles.organizationId })
100
+ .from(bundles)
101
+ .where(and(eq(bundles.objectKind, target.kind), eq(bundles.objectId, target.id)));
102
+ const discovered = target.kind === "document"
103
+ ? await db
104
+ .select({ organizationId: documentDiscoveryHeads.organizationId })
105
+ .from(documentDiscoveryHeads)
106
+ .where(eq(documentDiscoveryHeads.documentId, target.id))
107
+ : [];
108
+ // The listing that placed an unstored container must agree as well.
109
+ const listing = target.listing ? [target.listing] : [];
110
+ const sources = [
111
+ listed,
112
+ verifiedContainers,
113
+ verifiedTarget,
114
+ discovered,
115
+ listing,
116
+ ];
117
+ const organizations = new Set(sources.flat().map((row) => row.organizationId));
118
+ const [organizationId] = organizations;
119
+ return organizations.size === 1 && organizationId ? organizationId : null;
120
+ }
121
+ //# sourceMappingURL=citationScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citationScope.js","sourceRoot":"","sources":["citationScope.ts"],"names":[],"mappings":"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","sourcesContent":["import type { WriterProjectionTarget } from \"@tearleads/api-client\";\nimport { MAX_CONTAINER_PATH_LENGTH } from \"@tearleads/validators/util\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { documentContainerProjection } from \"../../sqlite/schema\";\nimport { containers } from \"../../sqlite/schema/containerSchema\";\nimport {\n documentDiscoveryHeads,\n pendingDocumentDiscoveries,\n} from \"../../sqlite/schema/documentDiscoveryEvidenceSchema\";\nimport { verifiedAccessManifestBundles as bundles } from \"../../sqlite/schema/verifiedManifestBundleSchema\";\nimport type { ClientSQLiteDatabase } from \"../../sqlite/sqlitePersistenceRuntime\";\n\n/** An object whose verified bytes a projection read may cite. */\nexport interface CitedObject {\n readonly kind: string;\n readonly id: string;\n}\n\n/** The requested object first, then each container whose lineage it cites. */\nasync function containerLineage(\n db: ClientSQLiteDatabase,\n containerId: string,\n prefix: readonly string[] = [],\n): Promise<string[]> {\n const lineage = [...prefix, containerId];\n while (lineage.length < MAX_CONTAINER_PATH_LENGTH) {\n const [row] = await db\n .select({ parentId: containers.parentId })\n .from(containers)\n .where(eq(containers.id, lineage.at(-1) ?? \"\"))\n .limit(1);\n const parentId = row?.parentId;\n if (!parentId || lineage.includes(parentId)) break;\n lineage.push(parentId);\n }\n return lineage;\n}\n\n/**\n * A container the device has not stored yet has no local parent, so its\n * listing names one: the lineage continues through the listed parent's\n * stored lineage. A wrong parent only changes which held hashes are cited.\n */\nasync function containerTargetLineage(\n db: ClientSQLiteDatabase,\n target: WriterProjectionTarget,\n): Promise<string[]> {\n const parentId = target.listing?.parentId;\n if (!parentId || parentId === target.id)\n return containerLineage(db, target.id);\n const [stored] = await db\n .select({ id: containers.id })\n .from(containers)\n .where(eq(containers.id, target.id))\n .limit(1);\n return stored\n ? containerLineage(db, target.id)\n : containerLineage(db, parentId, [target.id]);\n}\n\nexport async function citedObjects(\n db: ClientSQLiteDatabase,\n target: WriterProjectionTarget,\n): Promise<{ readonly kind: string; readonly id: string }[]> {\n if (target.kind === \"container\")\n return (await containerTargetLineage(db, target)).map((id) => ({\n kind: \"container\",\n id,\n }));\n // Linked containers, containers listing the document before its signed\n // placement is checked, and a container whose metadata document it is.\n const placements = [\n ...(await db\n .select({ containerId: documentContainerProjection.containerId })\n .from(documentContainerProjection)\n .where(eq(documentContainerProjection.documentId, target.id))),\n ...(await db\n .select({ containerId: pendingDocumentDiscoveries.containerId })\n .from(pendingDocumentDiscoveries)\n .where(eq(pendingDocumentDiscoveries.documentId, target.id))),\n ...(await db\n .select({ containerId: containers.id })\n .from(containers)\n .where(eq(containers.metadataDocumentId, target.id))),\n ];\n const containerIds = new Set<string>();\n for (const { containerId } of placements)\n if (containerId)\n for (const id of await containerLineage(db, containerId))\n containerIds.add(id);\n return [\n { kind: \"document\", id: target.id },\n ...[...containerIds].map((id) => ({ kind: \"container\", id })),\n ];\n}\n\n/**\n * The one organization every local source places the read in. Verified\n * ownership (stored verified bundles of the target and of the containers on\n * its walk, and a document's verified discovery head) and local placement\n * (listed container rows reached through links, pending listings, metadata,\n * or parents) must all agree: any disagreement, or no source at all, yields\n * null, so the read is sent with no hints rather than another\n * organization's.\n */\nexport async function targetOrganization(\n db: ClientSQLiteDatabase,\n target: WriterProjectionTarget,\n objects: readonly { readonly kind: string; readonly id: string }[],\n): Promise<string | null> {\n const containerIds = objects\n .filter((object) => object.kind === \"container\")\n .map((object) => object.id);\n const listed =\n containerIds.length === 0\n ? []\n : await db\n .select({ organizationId: containers.organizationId })\n .from(containers)\n .where(inArray(containers.id, containerIds));\n const verifiedContainers =\n containerIds.length === 0\n ? []\n : await db\n .select({ organizationId: bundles.organizationId })\n .from(bundles)\n .where(\n and(\n eq(bundles.objectKind, \"container\"),\n inArray(bundles.objectId, containerIds),\n ),\n );\n const verifiedTarget = await db\n .select({ organizationId: bundles.organizationId })\n .from(bundles)\n .where(\n and(eq(bundles.objectKind, target.kind), eq(bundles.objectId, target.id)),\n );\n const discovered =\n target.kind === \"document\"\n ? await db\n .select({ organizationId: documentDiscoveryHeads.organizationId })\n .from(documentDiscoveryHeads)\n .where(eq(documentDiscoveryHeads.documentId, target.id))\n : [];\n // The listing that placed an unstored container must agree as well.\n const listing = target.listing ? [target.listing] : [];\n const sources = [\n listed,\n verifiedContainers,\n verifiedTarget,\n discovered,\n listing,\n ];\n const organizations = new Set(\n sources.flat().map((row) => row.organizationId),\n );\n const [organizationId] = organizations;\n return organizations.size === 1 && organizationId ? organizationId : null;\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { type ContainerKekWireLevel } from "../../../internal/validators/response/index.js";
2
+ /**
3
+ * Typed rows in the verified-bundle cache that hold one container KEK level
4
+ * each. `manifestHash` (the cache key) and `bundleDigest` are the level's
5
+ * citation digest, the SHA-256 of `bundleJson`, its canonical wire bytes;
6
+ * `objectId` and `epoch` are the level's container and key epoch.
7
+ */
8
+ export declare const KEK_LEVEL_OBJECT_KIND = "container-kek-level";
9
+ /** A KEK level a projection verification accepted, with its organization. */
10
+ export interface VerifiedKekLevel {
11
+ readonly kekLevel: ContainerKekWireLevel;
12
+ readonly organizationId: string;
13
+ }
14
+ export interface KekLevelRow {
15
+ readonly manifestHash: string;
16
+ readonly organizationId: string;
17
+ readonly objectKind: string;
18
+ readonly objectId: string;
19
+ readonly epoch: number;
20
+ readonly bundleJson: string;
21
+ readonly bundleDigest: string;
22
+ }
23
+ /** Null when the level has no canonical form; such a level is not cited. */
24
+ export declare function kekLevelRow({ kekLevel, organizationId, }: VerifiedKekLevel): Promise<KekLevelRow | null>;
25
+ /**
26
+ * The stored level, or null for a damaged row: its bytes must parse as a
27
+ * full KEK level whose recomputed digest is the row's key.
28
+ */
29
+ export declare function readStoredKekLevel(row: {
30
+ readonly bundleDigest: string;
31
+ readonly bundleJson: string;
32
+ readonly manifestHash: string;
33
+ }): Promise<ContainerKekWireLevel | null>;
@@ -0,0 +1,44 @@
1
+ import { computeContainerKekLevelDigest, readKeyingCanonicalJson, serializeKeyingCanonicalJson, } from "../../../internal/crypto/index.js";
2
+ import { isContainerKekWireLevel, } from "../../../internal/validators/response/index.js";
3
+ /**
4
+ * Typed rows in the verified-bundle cache that hold one container KEK level
5
+ * each. `manifestHash` (the cache key) and `bundleDigest` are the level's
6
+ * citation digest, the SHA-256 of `bundleJson`, its canonical wire bytes;
7
+ * `objectId` and `epoch` are the level's container and key epoch.
8
+ */
9
+ export const KEK_LEVEL_OBJECT_KIND = "container-kek-level";
10
+ /** Null when the level has no canonical form; such a level is not cited. */
11
+ export async function kekLevelRow({ kekLevel, organizationId, }) {
12
+ const digest = await computeContainerKekLevelDigest(kekLevel);
13
+ if (digest === null)
14
+ return null;
15
+ return {
16
+ manifestHash: digest,
17
+ organizationId,
18
+ objectKind: KEK_LEVEL_OBJECT_KIND,
19
+ objectId: kekLevel.containerId,
20
+ epoch: kekLevel.containerKeyEpoch,
21
+ // Canonical: the digest above is the SHA-256 of exactly these bytes.
22
+ bundleJson: serializeKeyingCanonicalJson(readKeyingCanonicalJson(kekLevel, "Verified KEK level")),
23
+ bundleDigest: digest,
24
+ };
25
+ }
26
+ /**
27
+ * The stored level, or null for a damaged row: its bytes must parse as a
28
+ * full KEK level whose recomputed digest is the row's key.
29
+ */
30
+ export async function readStoredKekLevel(row) {
31
+ if (row.bundleDigest !== row.manifestHash)
32
+ return null;
33
+ try {
34
+ const level = JSON.parse(row.bundleJson);
35
+ return isContainerKekWireLevel(level) &&
36
+ (await computeContainerKekLevelDigest(level)) === row.manifestHash
37
+ ? level
38
+ : null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ //# sourceMappingURL=kekLevelCitations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kekLevelCitations.js","sourceRoot":"","sources":["kekLevelCitations.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,uBAAuB,GACxB,MAAM,gCAAgC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AAkB3D,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAChC,QAAQ,EACR,cAAc,GACG;IACjB,MAAM,MAAM,GAAG,MAAM,8BAA8B,CAAC,QAAQ,CAAC,CAAC;IAC9D,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO;QACL,YAAY,EAAE,MAAM;QACpB,cAAc;QACd,UAAU,EAAE,qBAAqB;QACjC,QAAQ,EAAE,QAAQ,CAAC,WAAW;QAC9B,KAAK,EAAE,QAAQ,CAAC,iBAAiB;QACjC,qEAAqE;QACrE,UAAU,EAAE,4BAA4B,CACtC,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CACxD;QACD,YAAY,EAAE,MAAM;KACrB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAIxC;IACC,IAAI,GAAG,CAAC,YAAY,KAAK,GAAG,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClD,OAAO,uBAAuB,CAAC,KAAK,CAAC;YACnC,CAAC,MAAM,8BAA8B,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,YAAY;YAClE,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import {\n computeContainerKekLevelDigest,\n readKeyingCanonicalJson,\n serializeKeyingCanonicalJson,\n} from \"@tearleads/crypto\";\nimport {\n type ContainerKekWireLevel,\n isContainerKekWireLevel,\n} from \"@tearleads/validators/response\";\n\n/**\n * Typed rows in the verified-bundle cache that hold one container KEK level\n * each. `manifestHash` (the cache key) and `bundleDigest` are the level's\n * citation digest, the SHA-256 of `bundleJson`, its canonical wire bytes;\n * `objectId` and `epoch` are the level's container and key epoch.\n */\nexport const KEK_LEVEL_OBJECT_KIND = \"container-kek-level\";\n\n/** A KEK level a projection verification accepted, with its organization. */\nexport interface VerifiedKekLevel {\n readonly kekLevel: ContainerKekWireLevel;\n readonly organizationId: string;\n}\n\nexport interface KekLevelRow {\n readonly manifestHash: string;\n readonly organizationId: string;\n readonly objectKind: string;\n readonly objectId: string;\n readonly epoch: number;\n readonly bundleJson: string;\n readonly bundleDigest: string;\n}\n\n/** Null when the level has no canonical form; such a level is not cited. */\nexport async function kekLevelRow({\n kekLevel,\n organizationId,\n}: VerifiedKekLevel): Promise<KekLevelRow | null> {\n const digest = await computeContainerKekLevelDigest(kekLevel);\n if (digest === null) return null;\n return {\n manifestHash: digest,\n organizationId,\n objectKind: KEK_LEVEL_OBJECT_KIND,\n objectId: kekLevel.containerId,\n epoch: kekLevel.containerKeyEpoch,\n // Canonical: the digest above is the SHA-256 of exactly these bytes.\n bundleJson: serializeKeyingCanonicalJson(\n readKeyingCanonicalJson(kekLevel, \"Verified KEK level\"),\n ),\n bundleDigest: digest,\n };\n}\n\n/**\n * The stored level, or null for a damaged row: its bytes must parse as a\n * full KEK level whose recomputed digest is the row's key.\n */\nexport async function readStoredKekLevel(row: {\n readonly bundleDigest: string;\n readonly bundleJson: string;\n readonly manifestHash: string;\n}): Promise<ContainerKekWireLevel | null> {\n if (row.bundleDigest !== row.manifestHash) return null;\n try {\n const level: unknown = JSON.parse(row.bundleJson);\n return isContainerKekWireLevel(level) &&\n (await computeContainerKekLevelDigest(level)) === row.manifestHash\n ? level\n : null;\n } catch {\n return null;\n }\n}\n"]}
@@ -1,14 +1,15 @@
1
- import type { VerifiedManifestCitations, WriterProjectionTarget } from "../../internal/api-client/index.js";
1
+ import type { HeldProjectionCitation, VerifiedManifestCitations, WriterProjectionTarget } from "../../internal/api-client/index.js";
2
2
  import { type AccessManifestBundleWireResponse } from "../../internal/validators/response/index.js";
3
3
  import { verifiedAccessManifestBundles as bundles } from "../sqlite/schema/verifiedManifestBundleSchema.js";
4
4
  import { type ClientSQLiteTransactionScope } from "../sqlite/sqlitePersistenceRuntime.js";
5
5
  import { type ExecSql } from "../sqlite/sqlSchema.js";
6
+ import { type VerifiedKekLevel } from "./verifiedCitations/kekLevelCitations.js";
6
7
  /**
7
8
  * Bounds local bytes (a bundle is ~10–20 KB). Evicting the least recently
8
9
  * verified rows only makes a later read carry those bundles again.
9
10
  */
10
11
  export declare const MAX_VERIFIED_MANIFEST_BUNDLES = 1024;
11
- export interface VerifiedManifestBundle {
12
+ interface VerifiedBundle {
12
13
  readonly bundle: AccessManifestBundleWireResponse;
13
14
  readonly checkpoint: {
14
15
  readonly objectKind: string;
@@ -17,12 +18,14 @@ export interface VerifiedManifestBundle {
17
18
  readonly epoch: number;
18
19
  };
19
20
  }
21
+ /** Bytes a projection verification accepted: a bundle or a KEK level. */
22
+ export type VerifiedManifestBundle = VerifiedBundle | VerifiedKekLevel;
20
23
  /** A verified bundle's exact JSON and that JSON's digest. */
21
24
  export type VerifiedManifestBundleRow = Omit<typeof bundles.$inferInsert, "verifiedAt">;
22
25
  /**
23
- * Prepares rows for bundles that just passed projection verification. The
24
- * digests are computed here, before the checkpoint transaction that writes
25
- * them begins.
26
+ * Prepares rows for bundles and KEK levels that just passed projection
27
+ * verification. The digests are computed here, before the checkpoint
28
+ * transaction that writes them begins.
26
29
  */
27
30
  export declare function verifiedManifestBundleRows(entries: readonly VerifiedManifestBundle[]): Promise<VerifiedManifestBundleRow[]>;
28
31
  /**
@@ -33,15 +36,17 @@ export declare function verifiedManifestBundleRows(entries: readonly VerifiedMan
33
36
  */
34
37
  export declare function writeVerifiedManifestBundlesInTransaction(tx: ClientSQLiteTransactionScope, rows: readonly VerifiedManifestBundleRow[], verifiedAt: string): Promise<void>;
35
38
  /**
36
- * Verified bundles of the target's object and of the containers its
37
- * projection walks, newest epochs first; then the most recently verified rows
38
- * of its organization, since a just-verified parent is the likeliest part of
39
- * a new child's projection. Citations never leave the target's organization:
40
- * an object whose organization the device cannot place cites nothing.
41
- * Damaged rows are dropped.
39
+ * The latest verified KEK level of each container the target's projection
40
+ * walks; then verified bundles of those objects, newest epochs first; then
41
+ * the most recently verified bundles of its organization, since a
42
+ * just-verified parent is the likeliest part of a new child's projection.
43
+ * Citations never leave the target's organization: an object whose
44
+ * organization the device cannot place cites nothing. Damaged rows are
45
+ * dropped.
42
46
  */
43
- export declare function loadVerifiedManifestCitations(execSql: ExecSql, target: WriterProjectionTarget, limit?: number): Promise<AccessManifestBundleWireResponse[]>;
47
+ export declare function loadVerifiedManifestCitations(execSql: ExecSql, target: WriterProjectionTarget, limit?: number): Promise<HeldProjectionCitation[]>;
44
48
  /** Cites from whichever database the signed-in identity currently uses. */
45
49
  export declare function createVerifiedManifestCitations(resolveExecSql: () => ExecSql | null): VerifiedManifestCitations;
46
50
  /** Drops an organization's verified bundles when a remote reset forgets it. */
47
51
  export declare function deleteVerifiedManifestBundlesForOrganization(tx: ClientSQLiteTransactionScope, organizationId: string): Promise<void>;
52
+ export {};
@@ -1,15 +1,15 @@
1
1
  import { toFingerprint } from "../../internal/crypto/index.js";
2
2
  import { isAccessManifestBundleWireResponse, } from "../../internal/validators/response/index.js";
3
- import { KNOWN_MANIFEST_HINT_LIMIT, MAX_CONTAINER_PATH_LENGTH, } from "../../internal/validators/util/index.js";
4
- import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
5
- import { containerTables, documentContainerProjection, documentContainerProjectionTables, } from "../sqlite/schema/index.js";
6
- import { containers } from "../sqlite/schema/containerSchema.js";
7
- import { documentDiscoveryHeads, pendingDocumentDiscoveries, } from "../sqlite/schema/documentDiscoveryEvidenceSchema.js";
3
+ import { KNOWN_MANIFEST_HINT_LIMIT } from "../../internal/validators/util/index.js";
4
+ import { and, asc, desc, eq, inArray, ne, notInArray, sql } from "drizzle-orm";
5
+ import { containerTables, documentContainerProjectionTables, } from "../sqlite/schema/index.js";
8
6
  import { verifiedAccessManifestBundles as bundles, verifiedManifestBundleTables, } from "../sqlite/schema/verifiedManifestBundleSchema.js";
9
7
  import { sqliteInsertBatchSize } from "../sqlite/sqliteBindBudget.js";
10
8
  import { getClientSQLitePersistenceRuntime, } from "../sqlite/sqlitePersistenceRuntime.js";
11
9
  import { excludedValue } from "../sqlite/sqliteUpsert.js";
12
10
  import { ensureSqlTables } from "../sqlite/sqlSchema.js";
11
+ import { citedObjects, targetOrganization, } from "./verifiedCitations/citationScope.js";
12
+ import { KEK_LEVEL_OBJECT_KIND, kekLevelRow, readStoredKekLevel, } from "./verifiedCitations/kekLevelCitations.js";
13
13
  /**
14
14
  * Bounds local bytes (a bundle is ~10–20 KB). Evicting the least recently
15
15
  * verified rows only makes a later read carry those bundles again.
@@ -43,11 +43,29 @@ async function pruneInTransaction(tx) {
43
43
  await tx.delete(bundles).where(inArray(bundles.manifestHash, batch)).run();
44
44
  }
45
45
  /**
46
- * Prepares rows for bundles that just passed projection verification. The
47
- * digests are computed here, before the checkpoint transaction that writes
48
- * them begins.
46
+ * Prepares rows for bundles and KEK levels that just passed projection
47
+ * verification. The digests are computed here, before the checkpoint
48
+ * transaction that writes them begins.
49
49
  */
50
- export function verifiedManifestBundleRows(entries) {
50
+ export async function verifiedManifestBundleRows(entries) {
51
+ const bundleEntries = [];
52
+ const levelEntries = [];
53
+ for (const entry of entries)
54
+ if ("kekLevel" in entry)
55
+ levelEntries.push(entry);
56
+ else
57
+ bundleEntries.push(entry);
58
+ // One level per container: its newest epoch, the last one seen on a tie.
59
+ const levelRows = new Map();
60
+ for (const level of levelEntries) {
61
+ const row = await kekLevelRow(level);
62
+ const held = row && levelRows.get(row.objectId);
63
+ if (row && (!held || held.epoch <= row.epoch))
64
+ levelRows.set(row.objectId, row);
65
+ }
66
+ return [...(await bundleRows(bundleEntries)), ...levelRows.values()];
67
+ }
68
+ function bundleRows(entries) {
51
69
  const unique = new Map(entries.map((entry) => [entry.bundle.manifestHash, entry]));
52
70
  return Promise.all([...unique.values()].map(async ({ bundle, checkpoint }) => {
53
71
  const bundleJson = JSON.stringify(bundle);
@@ -101,8 +119,21 @@ export async function writeVerifiedManifestBundlesInTransaction(tx, rows, verifi
101
119
  },
102
120
  })
103
121
  .run();
122
+ await dropSupersededKekLevels(tx, rows);
104
123
  await pruneInTransaction(tx);
105
124
  }
125
+ /**
126
+ * A container keeps only its latest verified level: the API serves its
127
+ * current level, so an older one can never be cited usefully again.
128
+ */
129
+ async function dropSupersededKekLevels(tx, rows) {
130
+ const levels = rows.filter((row) => row.objectKind === KEK_LEVEL_OBJECT_KIND);
131
+ for (const batch of batches(levels))
132
+ await tx
133
+ .delete(bundles)
134
+ .where(and(eq(bundles.objectKind, KEK_LEVEL_OBJECT_KIND), inArray(bundles.objectId, batch.map((row) => row.objectId)), notInArray(bundles.manifestHash, batch.map((row) => row.manifestHash))))
135
+ .run();
136
+ }
106
137
  async function storedDigests(tx, manifestHashes) {
107
138
  const digests = new Map();
108
139
  for (const batch of batches(manifestHashes)) {
@@ -118,94 +149,6 @@ async function storedDigests(tx, manifestHashes) {
118
149
  }
119
150
  return digests;
120
151
  }
121
- /** The requested object first, then each container whose lineage it cites. */
122
- async function containerLineage(db, containerId) {
123
- const lineage = [containerId];
124
- while (lineage.length < MAX_CONTAINER_PATH_LENGTH) {
125
- const [row] = await db
126
- .select({ parentId: containers.parentId })
127
- .from(containers)
128
- .where(eq(containers.id, lineage.at(-1) ?? ""))
129
- .limit(1);
130
- const parentId = row?.parentId;
131
- if (!parentId || lineage.includes(parentId))
132
- break;
133
- lineage.push(parentId);
134
- }
135
- return lineage;
136
- }
137
- async function citedObjects(db, target) {
138
- if (target.kind === "container")
139
- return (await containerLineage(db, target.id)).map((id) => ({
140
- kind: "container",
141
- id,
142
- }));
143
- // Linked containers, containers listing the document before its signed
144
- // placement is checked, and a container whose metadata document it is.
145
- const placements = [
146
- ...(await db
147
- .select({ containerId: documentContainerProjection.containerId })
148
- .from(documentContainerProjection)
149
- .where(eq(documentContainerProjection.documentId, target.id))),
150
- ...(await db
151
- .select({ containerId: pendingDocumentDiscoveries.containerId })
152
- .from(pendingDocumentDiscoveries)
153
- .where(eq(pendingDocumentDiscoveries.documentId, target.id))),
154
- ...(await db
155
- .select({ containerId: containers.id })
156
- .from(containers)
157
- .where(eq(containers.metadataDocumentId, target.id))),
158
- ];
159
- const containerIds = new Set();
160
- for (const { containerId } of placements)
161
- if (containerId)
162
- for (const id of await containerLineage(db, containerId))
163
- containerIds.add(id);
164
- return [
165
- { kind: "document", id: target.id },
166
- ...[...containerIds].map((id) => ({ kind: "container", id })),
167
- ];
168
- }
169
- /**
170
- * The one organization every local source places the read in. Verified
171
- * ownership (stored verified bundles of the target and of the containers on
172
- * its walk, and a document's verified discovery head) and local placement
173
- * (listed container rows reached through links, pending listings, metadata,
174
- * or parents) must all agree: any disagreement, or no source at all, yields
175
- * null, so the read is sent with no hints rather than another
176
- * organization's.
177
- */
178
- async function targetOrganization(db, target, objects) {
179
- const containerIds = objects
180
- .filter((object) => object.kind === "container")
181
- .map((object) => object.id);
182
- const listed = containerIds.length === 0
183
- ? []
184
- : await db
185
- .select({ organizationId: containers.organizationId })
186
- .from(containers)
187
- .where(inArray(containers.id, containerIds));
188
- const verifiedContainers = containerIds.length === 0
189
- ? []
190
- : await db
191
- .select({ organizationId: bundles.organizationId })
192
- .from(bundles)
193
- .where(and(eq(bundles.objectKind, "container"), inArray(bundles.objectId, containerIds)));
194
- const verifiedTarget = await db
195
- .select({ organizationId: bundles.organizationId })
196
- .from(bundles)
197
- .where(and(eq(bundles.objectKind, target.kind), eq(bundles.objectId, target.id)));
198
- const discovered = target.kind === "document"
199
- ? await db
200
- .select({ organizationId: documentDiscoveryHeads.organizationId })
201
- .from(documentDiscoveryHeads)
202
- .where(eq(documentDiscoveryHeads.documentId, target.id))
203
- : [];
204
- const sources = [listed, verifiedContainers, verifiedTarget, discovered];
205
- const organizations = new Set(sources.flat().map((row) => row.organizationId));
206
- const [organizationId] = organizations;
207
- return organizations.size === 1 && organizationId ? organizationId : null;
208
- }
209
152
  const storedColumns = {
210
153
  bundleDigest: bundles.bundleDigest,
211
154
  bundleJson: bundles.bundleJson,
@@ -237,17 +180,36 @@ function recentRows(db, organizationId, limit) {
237
180
  return db
238
181
  .select(storedColumns)
239
182
  .from(bundles)
240
- .where(eq(bundles.organizationId, organizationId))
183
+ .where(and(eq(bundles.organizationId, organizationId), ne(bundles.objectKind, KEK_LEVEL_OBJECT_KIND)))
241
184
  .orderBy(desc(bundles.verifiedAt), asc(bundles.manifestHash))
242
185
  .limit(limit);
243
186
  }
187
+ /** The latest verified KEK level of each container, in lineage order. */
188
+ async function lineageKekLevels(db, organizationId, objects) {
189
+ const containerIds = objects
190
+ .filter((object) => object.kind === "container")
191
+ .map((object) => object.id);
192
+ if (containerIds.length === 0)
193
+ return [];
194
+ const rows = await db
195
+ .select({ ...storedColumns, objectId: bundles.objectId })
196
+ .from(bundles)
197
+ .where(and(eq(bundles.organizationId, organizationId), eq(bundles.objectKind, KEK_LEVEL_OBJECT_KIND), inArray(bundles.objectId, containerIds)))
198
+ .orderBy(desc(bundles.epoch), desc(bundles.verifiedAt));
199
+ const latest = new Map();
200
+ for (const row of rows)
201
+ if (!latest.has(row.objectId))
202
+ latest.set(row.objectId, row);
203
+ return containerIds.flatMap((id) => latest.get(id) ?? []);
204
+ }
244
205
  /**
245
- * Verified bundles of the target's object and of the containers its
246
- * projection walks, newest epochs first; then the most recently verified rows
247
- * of its organization, since a just-verified parent is the likeliest part of
248
- * a new child's projection. Citations never leave the target's organization:
249
- * an object whose organization the device cannot place cites nothing.
250
- * Damaged rows are dropped.
206
+ * The latest verified KEK level of each container the target's projection
207
+ * walks; then verified bundles of those objects, newest epochs first; then
208
+ * the most recently verified bundles of its organization, since a
209
+ * just-verified parent is the likeliest part of a new child's projection.
210
+ * Citations never leave the target's organization: an object whose
211
+ * organization the device cannot place cites nothing. Damaged rows are
212
+ * dropped.
251
213
  */
252
214
  export async function loadVerifiedManifestCitations(execSql, target, limit = KNOWN_MANIFEST_HINT_LIMIT) {
253
215
  await ensureSqlTables(execSql, [
@@ -273,6 +235,15 @@ export async function loadVerifiedManifestCitations(execSql, target, limit = KNO
273
235
  const organizationId = await targetOrganization(db, target, objects);
274
236
  if (organizationId === null)
275
237
  return [];
238
+ for (const row of await lineageKekLevels(db, organizationId, objects)) {
239
+ if (cited.size >= limit)
240
+ break;
241
+ const level = await readStoredKekLevel(row);
242
+ if (level)
243
+ cited.set(row.manifestHash, level);
244
+ else
245
+ damaged.push(row.manifestHash);
246
+ }
276
247
  for (const object of objects)
277
248
  if (cited.size < limit)
278
249
  await collect(await objectRows(db, organizationId, object, limit - cited.size));