@tearleads/client-sdk 0.1.97 → 0.1.99
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.
package/README.md
CHANGED
|
@@ -24,13 +24,20 @@ const tearleads = new Tearleads();
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
The minimal instance uses same-origin API routes, memory blob storage, default
|
|
27
|
-
logging, and an idle database.
|
|
28
|
-
or identity generation
|
|
27
|
+
logging, and an idle database. Initialize SQLite, then configure the SDK before
|
|
28
|
+
using persistence, sync, or identity generation. Here `localKeys` comes from the
|
|
29
|
+
host's [local keyring](https://github.com/a2f0/tearleads/blob/main/docs/developer/client-sdk.md#local-keyring):
|
|
29
30
|
|
|
30
31
|
```ts
|
|
31
32
|
import { createSQLiteRuntime } from "@tearleads/client-sdk/sqlite";
|
|
32
33
|
|
|
33
34
|
const sqliteRuntime = createSQLiteRuntime();
|
|
35
|
+
await sqliteRuntime.client.init({
|
|
36
|
+
dbName: "/app-identity.db",
|
|
37
|
+
cipher: "chacha20",
|
|
38
|
+
key: localKeys.sqliteKey,
|
|
39
|
+
persistence: "opfs-sahpool",
|
|
40
|
+
});
|
|
34
41
|
tearleads.database.configure({
|
|
35
42
|
client: sqliteRuntime.client,
|
|
36
43
|
id: sqliteRuntime.id,
|
|
@@ -53,8 +60,10 @@ for (const file of ["worker.js", "sqlite3.wasm", "sqlite3-licenses.md"]) {
|
|
|
53
60
|
}
|
|
54
61
|
```
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
SQLite defaults to `persistence: "memory"`, which loses the database on worker
|
|
64
|
+
restart or page reload. The example opts into `"opfs-sahpool"` to persist it in
|
|
65
|
+
the origin private file system (OPFS); serve the page from a secure context.
|
|
66
|
+
If OPFS is unavailable, initialization fails instead of falling back to memory.
|
|
58
67
|
|
|
59
68
|
Typecheck with `skipLibCheck: true`: the declarations of drizzle-orm and
|
|
60
69
|
loro-crdt, which the SDK's types reference, do not typecheck without it.
|
|
@@ -68,6 +77,8 @@ but cannot authorize replay of the old request.
|
|
|
68
77
|
The [developer guide](https://github.com/a2f0/tearleads/blob/main/docs/developer/client-sdk.md)
|
|
69
78
|
covers constructor options, the host contract, the local keyring, and every
|
|
70
79
|
public entry point.
|
|
80
|
+
For document editing and creation, start with the
|
|
81
|
+
[document creation guide](https://github.com/a2f0/tearleads/blob/main/docs/developer/document-creation.md).
|
|
71
82
|
|
|
72
83
|
## Publishing
|
|
73
84
|
|
|
@@ -92,26 +103,25 @@ dependencies are the packages that output imports, at the versions the
|
|
|
92
103
|
workspace pins, as caret ranges. Source maps carry their sources inline and
|
|
93
104
|
name them by file name alone.
|
|
94
105
|
|
|
95
|
-
`scripts/publishedPackage.test.ts` builds the package and checks its manifest,
|
|
96
|
-
every module imports only its own files or a declared dependency, and that
|
|
97
|
-
build path or test module ships. The smoke script imports every entry point
|
|
106
|
+
`scripts/publishedPackage.test.ts` builds the package and checks its manifest,
|
|
107
|
+
that every module imports only its own files or a declared dependency, and that
|
|
108
|
+
no build path or test module ships. The smoke script imports every entry point
|
|
98
109
|
under Bun and Node, resolves and parses the SQLite worker files, and bundles
|
|
99
|
-
the entry points for the browser. It typechecks a consumer
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
consumer's dependencies.
|
|
110
|
+
the entry points for the browser. It typechecks a consumer under `bundler` and
|
|
111
|
+
`nodenext` resolution with `skipLibCheck: true`, then again without it, failing
|
|
112
|
+
on errors in the SDK's declarations and ignoring those in drizzle-orm's and
|
|
113
|
+
loro-crdt's own. It needs the network to install the consumer's dependencies.
|
|
104
114
|
|
|
105
115
|
Merging to `main` publishes. When `packages/client-sdk` changes there,
|
|
106
116
|
`.github/workflows/client-sdk-publish.yml` publishes the version in
|
|
107
117
|
`package.json` if it is a stable `x.y.z` newer than npm's `latest`, after the
|
|
108
118
|
smoke script passes. A merge that leaves the version alone, a re-run, or a run
|
|
109
|
-
that finishes
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
that finishes after a newer release succeeds without publishing
|
|
120
|
+
(`agent-tool release plan` makes that call). Because the package ships the
|
|
121
|
+
workspace packages above, `agent-tool.json` lists it under `versions.bundles`:
|
|
122
|
+
a change to any of them also bumps the SDK's version, so it publishes too.
|
|
123
|
+
|
|
124
|
+
The workflow authenticates with
|
|
115
125
|
[npm trusted publishing](https://docs.npmjs.com/trusted-publishers) and
|
|
116
126
|
attaches provenance to each version. On npmjs.com the package's trusted
|
|
117
127
|
publisher names the `a2f0` owner, the `tearleads` repository, the
|
|
@@ -128,14 +138,13 @@ bun run publish:npm:dry-run client-sdk
|
|
|
128
138
|
bun run publish:npm client-sdk --otp 123456
|
|
129
139
|
```
|
|
130
140
|
|
|
131
|
-
`scripts/release/publishNpmPackage.sh` rebuilds into a fresh temporary
|
|
132
|
-
publishes its generated manifest with public access to
|
|
141
|
+
`scripts/release/publishNpmPackage.sh` rebuilds into a fresh temporary
|
|
142
|
+
directory, publishes its generated manifest with public access to
|
|
133
143
|
`https://registry.npmjs.org`, and removes the directory on success or failure.
|
|
134
144
|
A temporary `.npmrc` overrides any scope-specific registry without changing
|
|
135
145
|
your npm configuration. It accepts `--dry-run`, `--tag <tag>` (default
|
|
136
146
|
`latest`), and `--otp <code>`.
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`"license": "UNLICENSED"` until a license is chosen.
|
|
148
|
+
The generated manifest copies `version` from `package.json`, names this
|
|
149
|
+
repository (npm requires that of a version with provenance), and declares
|
|
150
|
+
`"license": "UNLICENSED"`.
|
|
@@ -30,11 +30,13 @@ export declare class HistoricalWrapUnavailableError extends Error {
|
|
|
30
30
|
*
|
|
31
31
|
* Scope bound: a direct user envelope is recoverable from identity keys
|
|
32
32
|
* alone, so a pristine client anchors on it. A group- or organization-
|
|
33
|
-
* addressed envelope additionally requires the
|
|
34
|
-
* Principal rotation rematerializes
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* addressed envelope additionally requires the secret key of the principal
|
|
34
|
+
* key epoch it was addressed to. Principal rotation rematerializes retained
|
|
35
|
+
* grants by wrapping only the container's current epoch, so an older epoch
|
|
36
|
+
* whose only anchor is a group wrap from before a principal key rotation needs
|
|
37
|
+
* a principal key this client cannot resolve. Such epochs fail closed with
|
|
38
|
+
* `HistoricalWrapUnavailableError` (`principal-key-unreachable`) rather than
|
|
39
|
+
* appearing to be corruption.
|
|
38
40
|
*/
|
|
39
41
|
export declare function recoverKeyringEntryFromWraps(input: {
|
|
40
42
|
containerId: string;
|
|
@@ -164,11 +164,13 @@ async function unwrapWrapsWithCurrentPolicies(input) {
|
|
|
164
164
|
*
|
|
165
165
|
* Scope bound: a direct user envelope is recoverable from identity keys
|
|
166
166
|
* alone, so a pristine client anchors on it. A group- or organization-
|
|
167
|
-
* addressed envelope additionally requires the
|
|
168
|
-
* Principal rotation rematerializes
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
167
|
+
* addressed envelope additionally requires the secret key of the principal
|
|
168
|
+
* key epoch it was addressed to. Principal rotation rematerializes retained
|
|
169
|
+
* grants by wrapping only the container's current epoch, so an older epoch
|
|
170
|
+
* whose only anchor is a group wrap from before a principal key rotation needs
|
|
171
|
+
* a principal key this client cannot resolve. Such epochs fail closed with
|
|
172
|
+
* `HistoricalWrapUnavailableError` (`principal-key-unreachable`) rather than
|
|
173
|
+
* appearing to be corruption.
|
|
172
174
|
*/
|
|
173
175
|
export async function recoverKeyringEntryFromWraps(input) {
|
|
174
176
|
const { directWraps, epochWraps, parentWraps, principalWraps } = partitionAnchorWraps(input.epoch.wraps, input.epoch.containerKeyEpochId, input.userId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyringRebuild.js","sourceRoot":"","sources":["keyringRebuild.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ContainerKekKeyringEntry,\n ContainerKeyWrap,\n} from \"@tearleads/crypto\";\nimport {\n computeContainerKekMaterialId,\n unwrapContainerKekParentWrap,\n} from \"@tearleads/crypto\";\nimport type { ContainerKekLogEpochResponse } from \"@tearleads/validators/response\";\nimport { unwrapKeyEnvelopesWithPrincipalPolicies } from \"../../../principals/principalPolicyCrypto\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport { normalizeContainerKeyWrap } from \"../readers\";\n\n/**\n * Splits an epoch's envelopes into the anchor kinds this requester could use:\n * their own direct envelope, principal envelopes their policies may resolve,\n * and parent-container envelopes. Anything else is not an identity anchor and\n * is dropped, so it never produces a misleading failure.\n */\nfunction partitionAnchorWraps(\n wraps: readonly Record<string, unknown>[],\n containerKeyEpochId: string,\n userId: string,\n): {\n directWraps: ContainerKeyWrap[];\n epochWraps: ContainerKeyWrap[];\n parentWraps: ContainerKeyWrap[];\n principalWraps: ContainerKeyWrap[];\n} {\n const forEpoch = wraps\n .map((wrap) => normalizeContainerKeyWrap(wrap))\n .filter((wrap) => wrap.containerKeyEpochId === containerKeyEpochId);\n const directWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"user\" && wrap.recipientId === userId,\n );\n const principalWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"group\",\n );\n const parentWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"container\",\n );\n return {\n directWraps,\n epochWraps: [...directWraps, ...principalWraps, ...parentWraps],\n parentWraps,\n principalWraps,\n };\n}\n\n/**\n * Opens the first parent-container envelope whose parent epoch key the caller\n * supplied. Reports whether any key was available, so a missing parent key is\n * distinguishable from a corrupt envelope.\n */\nasync function openParentAnchor(\n parentWraps: readonly ContainerKeyWrap[],\n parentKeys: ReadonlyMap<string, Uint8Array>,\n): Promise<{\n failure: unknown;\n key: Uint8Array | null;\n keyWasAvailable: boolean;\n}> {\n let failure: unknown;\n let keyWasAvailable = false;\n for (const wrap of parentWraps) {\n const parentKey = parentKeys.get(wrap.recipientKeyEpochId);\n if (!parentKey) {\n continue;\n }\n keyWasAvailable = true;\n try {\n return {\n failure: undefined,\n key: await unwrapContainerKekParentWrap({\n parentContainerId: wrap.recipientId,\n parentKeyMaterial: parentKey,\n kemCipherText: wrap.kemCipherText,\n wrappedKey: wrap.wrappedKey,\n }),\n keyWasAvailable,\n };\n } catch (error) {\n failure = error;\n }\n }\n return { failure, key: null, keyWasAvailable };\n}\n\n/**\n * Every anchor kind was tried; report the most specific reason. A wrap that\n * was actually attempted and failed is corruption; one that could not be\n * attempted names the key that was missing.\n */\nfunction throwMostSpecificAnchorFailure(input: {\n containerKeyEpoch: number;\n directFailure: unknown;\n hadParentWraps: boolean;\n parentFailure: unknown;\n parentKeyWasAvailable: boolean;\n principalFailure: unknown;\n}): never {\n if (input.directFailure !== undefined || input.parentFailure !== undefined) {\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n \"corrupt-envelope\",\n input.directFailure ?? input.parentFailure,\n );\n }\n if (input.principalFailure !== undefined) {\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n \"principal-key-unreachable\",\n input.principalFailure,\n );\n }\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n input.hadParentWraps && !input.parentKeyWasAvailable\n ? \"parent-key-unavailable\"\n : \"no-addressed-envelope\",\n );\n}\n\n/**\n * Raised when the requester holds no envelope they can open for a historical\n * epoch. Distinct from corruption: the log is intact, the caller simply is\n * not an anchor for this epoch.\n */\nexport type HistoricalWrapUnavailableReason =\n /** No envelope at this epoch is addressed to the requester at all. */\n | \"no-addressed-envelope\"\n /** A direct user envelope exists for the requester but did not open. */\n | \"corrupt-envelope\"\n /** Only principal-addressed envelopes, whose key epoch is unreachable. */\n | \"principal-key-unreachable\"\n /** Only a parent-container envelope, and the parent KEK was not supplied. */\n | \"parent-key-unavailable\";\n\nconst HISTORICAL_WRAP_UNAVAILABLE_MESSAGES: Record<\n HistoricalWrapUnavailableReason,\n (epoch: number) => string\n> = {\n \"no-addressed-envelope\": (epoch) =>\n `Container KEK log has no envelope addressed to this requester at epoch ${epoch}`,\n \"corrupt-envelope\": (epoch) =>\n `Container KEK log envelope for this requester at epoch ${epoch} could not be opened`,\n \"principal-key-unreachable\": (epoch) =>\n `Container KEK log envelope at epoch ${epoch} is addressed to a principal key epoch this client cannot resolve`,\n \"parent-key-unavailable\": (epoch) =>\n `Container KEK log envelope at epoch ${epoch} is inherited from a parent container whose KEK was not supplied`,\n};\n\nexport class HistoricalWrapUnavailableError extends Error {\n constructor(\n readonly containerKeyEpoch: number,\n readonly reason: HistoricalWrapUnavailableReason,\n cause?: unknown,\n ) {\n super(\n HISTORICAL_WRAP_UNAVAILABLE_MESSAGES[reason](containerKeyEpoch),\n cause === undefined ? undefined : { cause },\n );\n this.name = \"HistoricalWrapUnavailableError\";\n }\n}\n\n/**\n * The first candidate envelope that both decrypts AND matches the epoch's\n * material-id commitment.\n *\n * Candidates are tried ONE AT A TIME. An envelope can be AEAD-valid and still\n * carry the WRONG key — a stale or misaddressed wrap — so decrypting a batch\n * together would let that one envelope answer for the whole group and mask a\n * later envelope that would have opened the epoch. The reported failure is the\n * first real decrypt failure, since a commitment mismatch is a wrong envelope\n * rather than an unreachable one.\n */\nasync function firstCommittedAnchor(input: {\n attempt: (\n wraps: ContainerKeyWrap[],\n ) => Promise<{ failure: unknown; key: Uint8Array | null }>;\n candidates: ContainerKeyWrap[];\n containerId: string;\n containerKeyEpoch: number;\n containerKeyEpochId: string;\n}): Promise<{ failure: unknown; key: Uint8Array | null }> {\n let firstFailure: unknown;\n for (const candidate of input.candidates) {\n const result = await input.attempt([candidate]);\n if (result.key === null) {\n firstFailure ??= result.failure;\n continue;\n }\n const materialId = await computeContainerKekMaterialId({\n containerId: input.containerId,\n keyEpoch: input.containerKeyEpoch,\n keyMaterial: result.key,\n });\n if (materialId === input.containerKeyEpochId) {\n return result;\n }\n // An envelope that decrypts to the WRONG key is a misaddressed or stale\n // envelope, not an unreachable principal. Recording it keeps the reported\n // reason `corrupt-envelope`; dropping it silently would let the epoch\n // report as unaddressed, which points a repair at the wrong problem.\n //\n // Only reachable when the server files a wrap under an epoch id its\n // material does not hash to — the addressing filter already rejects a\n // wrap whose epoch id simply differs.\n firstFailure ??= new Error(\n `Container KEK envelope decrypts to material that is not epoch ${input.containerKeyEpochId}`,\n );\n }\n return { failure: firstFailure, key: null };\n}\n\n/**\n * Opens one group of envelopes with the requester's CURRENT policies, folding\n * failure into a value rather than throwing.\n *\n * The caller tries several groups in order, and a failure in one is not the\n * end of the search — so the failure has to travel back alongside the miss\n * instead of unwinding the whole recovery.\n */\nasync function unwrapWrapsWithCurrentPolicies(input: {\n execSql?: ExecSql | undefined;\n secretKey: Uint8Array;\n wraps: readonly ContainerKeyWrap[];\n}): Promise<{ failure: unknown; key: Uint8Array | null }> {\n if (input.wraps.length === 0) {\n return { failure: undefined, key: null };\n }\n try {\n return {\n failure: undefined,\n key: await unwrapKeyEnvelopesWithPrincipalPolicies({\n envelopes: input.wraps.map((wrap) => ({\n keyFingerprint: wrap.recipientKeyFingerprint,\n kemCipherText: wrap.kemCipherText,\n wrappedKey: wrap.wrappedKey,\n })),\n execSql: input.execSql,\n secretKey: input.secretKey,\n }),\n };\n } catch (error) {\n return { failure: error, key: null };\n }\n}\n\n/**\n * The severed-bridge backstop: recovers one historical epoch's KEK from the\n * requester's own retained recipient envelope served in the kek-log. Wraps\n * are written by that epoch's rotator and never deleted, so this path is\n * independent of every later rotation — it works even when the bridge chain\n * above the epoch is destroyed. The recovered key is verified against the\n * epoch id's material commitment before use.\n *\n * Scope bound: a direct user envelope is recoverable from identity keys\n * alone, so a pristine client anchors on it. A group- or organization-\n * addressed envelope additionally requires the current principal secret key.\n * Principal rotation rematerializes every retained container grant against\n * the new principal head, so a current reader never needs an older principal\n * key to use this recovery path. Unreachable principal keys fail closed with\n * `HistoricalWrapUnavailableError` rather than appearing to be corruption.\n */\nexport async function recoverKeyringEntryFromWraps(input: {\n containerId: string;\n epoch: Pick<\n ContainerKekLogEpochResponse,\n \"containerKeyEpoch\" | \"containerKeyEpochId\" | \"wraps\"\n >;\n execSql?: ExecSql | undefined;\n secretKey: Uint8Array;\n /**\n * Parent container KEKs by epoch id. An inherited-only child has no direct\n * or principal envelope of its own; its anchor is the parent-container\n * wrap, which opens under the parent epoch's KEK. Recover the parent's\n * epoch first (recursively, through the parent's own log) and pass it here.\n */\n parentKeysByEpochId?: ReadonlyMap<string, Uint8Array> | undefined;\n /**\n * The requester's user id. Direct user envelopes are matched against it, so\n * another member's envelope is never mistaken for an anchor — attempting it\n * would only produce a misleading failure.\n */\n userId: string;\n}): Promise<ContainerKekKeyringEntry> {\n const { directWraps, epochWraps, parentWraps, principalWraps } =\n partitionAnchorWraps(\n input.epoch.wraps,\n input.epoch.containerKeyEpochId,\n input.userId,\n );\n if (epochWraps.length === 0) {\n throw new HistoricalWrapUnavailableError(\n input.epoch.containerKeyEpoch,\n \"no-addressed-envelope\",\n );\n }\n\n const attempt = (wraps: typeof epochWraps) =>\n unwrapWrapsWithCurrentPolicies({\n execSql: input.execSql,\n secretKey: input.secretKey,\n wraps,\n });\n\n // A direct envelope is decryptable from identity keys alone, so its failure\n // means corruption — not an unreachable principal key. Neither failure ends\n // the search: a parent-container wrap may still anchor this epoch.\n //\n // Candidates are tried ONE AT A TIME, and each must match the epoch's\n // material-id commitment rather than merely decrypt. An envelope can be\n // AEAD-valid and still carry the WRONG key — a stale or misaddressed wrap —\n // so batching would let that one envelope answer for the whole group and\n // mask a later envelope that would have opened the epoch.\n const attemptCommitted = (candidates: ContainerKeyWrap[]) =>\n firstCommittedAnchor({\n attempt,\n candidates,\n containerId: input.containerId,\n containerKeyEpoch: input.epoch.containerKeyEpoch,\n containerKeyEpochId: input.epoch.containerKeyEpochId,\n });\n\n const direct = await attemptCommitted(directWraps);\n let keyMaterial = direct.key;\n const directFailure = direct.failure;\n let principalFailure: unknown;\n if (keyMaterial === null) {\n const principal = await attemptCommitted(principalWraps);\n keyMaterial = principal.key;\n // Preserve the current-policy failure in case no inherited anchor opens.\n principalFailure = principal.failure;\n }\n // The inherited-only path: a parent-container envelope opens under the\n // parent epoch's KEK, which the caller recovers from the parent's own log.\n const parent =\n keyMaterial === null\n ? await openParentAnchor(\n parentWraps,\n input.parentKeysByEpochId ?? new Map(),\n )\n : { failure: undefined, key: null, keyWasAvailable: false };\n keyMaterial ??= parent.key;\n const parentFailure = parent.failure;\n const parentKeyWasAvailable = parent.keyWasAvailable;\n if (keyMaterial === null) {\n throwMostSpecificAnchorFailure({\n containerKeyEpoch: input.epoch.containerKeyEpoch,\n directFailure,\n hadParentWraps: parentWraps.length > 0,\n parentFailure,\n parentKeyWasAvailable,\n principalFailure,\n });\n }\n const expectedId = await computeContainerKekMaterialId({\n containerId: input.containerId,\n keyEpoch: input.epoch.containerKeyEpoch,\n keyMaterial,\n });\n if (expectedId !== input.epoch.containerKeyEpochId) {\n throw new Error(\n `Container KEK log wrap does not match its committed epoch id at epoch ${input.epoch.containerKeyEpoch}`,\n );\n }\n return {\n containerKeyEpochId: input.epoch.containerKeyEpochId,\n keyMaterial,\n };\n}\n\nexport { fetchContainerKekLog } from \"./containerKekLogFetch\";\nexport type {\n AggregatedContainerKekLog,\n KeyringRebuildResult,\n} from \"./keyringLogWalk\";\nexport { rebuildKeyringEntriesFromLog } from \"./keyringLogWalk\";\n"]}
|
|
1
|
+
{"version":3,"file":"keyringRebuild.js","sourceRoot":"","sources":["keyringRebuild.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ContainerKekKeyringEntry,\n ContainerKeyWrap,\n} from \"@tearleads/crypto\";\nimport {\n computeContainerKekMaterialId,\n unwrapContainerKekParentWrap,\n} from \"@tearleads/crypto\";\nimport type { ContainerKekLogEpochResponse } from \"@tearleads/validators/response\";\nimport { unwrapKeyEnvelopesWithPrincipalPolicies } from \"../../../principals/principalPolicyCrypto\";\nimport type { ExecSql } from \"../../../sqlite/sqlSchema\";\nimport { normalizeContainerKeyWrap } from \"../readers\";\n\n/**\n * Splits an epoch's envelopes into the anchor kinds this requester could use:\n * their own direct envelope, principal envelopes their policies may resolve,\n * and parent-container envelopes. Anything else is not an identity anchor and\n * is dropped, so it never produces a misleading failure.\n */\nfunction partitionAnchorWraps(\n wraps: readonly Record<string, unknown>[],\n containerKeyEpochId: string,\n userId: string,\n): {\n directWraps: ContainerKeyWrap[];\n epochWraps: ContainerKeyWrap[];\n parentWraps: ContainerKeyWrap[];\n principalWraps: ContainerKeyWrap[];\n} {\n const forEpoch = wraps\n .map((wrap) => normalizeContainerKeyWrap(wrap))\n .filter((wrap) => wrap.containerKeyEpochId === containerKeyEpochId);\n const directWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"user\" && wrap.recipientId === userId,\n );\n const principalWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"group\",\n );\n const parentWraps = forEpoch.filter(\n (wrap) => wrap.recipientKind === \"container\",\n );\n return {\n directWraps,\n epochWraps: [...directWraps, ...principalWraps, ...parentWraps],\n parentWraps,\n principalWraps,\n };\n}\n\n/**\n * Opens the first parent-container envelope whose parent epoch key the caller\n * supplied. Reports whether any key was available, so a missing parent key is\n * distinguishable from a corrupt envelope.\n */\nasync function openParentAnchor(\n parentWraps: readonly ContainerKeyWrap[],\n parentKeys: ReadonlyMap<string, Uint8Array>,\n): Promise<{\n failure: unknown;\n key: Uint8Array | null;\n keyWasAvailable: boolean;\n}> {\n let failure: unknown;\n let keyWasAvailable = false;\n for (const wrap of parentWraps) {\n const parentKey = parentKeys.get(wrap.recipientKeyEpochId);\n if (!parentKey) {\n continue;\n }\n keyWasAvailable = true;\n try {\n return {\n failure: undefined,\n key: await unwrapContainerKekParentWrap({\n parentContainerId: wrap.recipientId,\n parentKeyMaterial: parentKey,\n kemCipherText: wrap.kemCipherText,\n wrappedKey: wrap.wrappedKey,\n }),\n keyWasAvailable,\n };\n } catch (error) {\n failure = error;\n }\n }\n return { failure, key: null, keyWasAvailable };\n}\n\n/**\n * Every anchor kind was tried; report the most specific reason. A wrap that\n * was actually attempted and failed is corruption; one that could not be\n * attempted names the key that was missing.\n */\nfunction throwMostSpecificAnchorFailure(input: {\n containerKeyEpoch: number;\n directFailure: unknown;\n hadParentWraps: boolean;\n parentFailure: unknown;\n parentKeyWasAvailable: boolean;\n principalFailure: unknown;\n}): never {\n if (input.directFailure !== undefined || input.parentFailure !== undefined) {\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n \"corrupt-envelope\",\n input.directFailure ?? input.parentFailure,\n );\n }\n if (input.principalFailure !== undefined) {\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n \"principal-key-unreachable\",\n input.principalFailure,\n );\n }\n throw new HistoricalWrapUnavailableError(\n input.containerKeyEpoch,\n input.hadParentWraps && !input.parentKeyWasAvailable\n ? \"parent-key-unavailable\"\n : \"no-addressed-envelope\",\n );\n}\n\n/**\n * Raised when the requester holds no envelope they can open for a historical\n * epoch. Distinct from corruption: the log is intact, the caller simply is\n * not an anchor for this epoch.\n */\nexport type HistoricalWrapUnavailableReason =\n /** No envelope at this epoch is addressed to the requester at all. */\n | \"no-addressed-envelope\"\n /** A direct user envelope exists for the requester but did not open. */\n | \"corrupt-envelope\"\n /** Only principal-addressed envelopes, whose key epoch is unreachable. */\n | \"principal-key-unreachable\"\n /** Only a parent-container envelope, and the parent KEK was not supplied. */\n | \"parent-key-unavailable\";\n\nconst HISTORICAL_WRAP_UNAVAILABLE_MESSAGES: Record<\n HistoricalWrapUnavailableReason,\n (epoch: number) => string\n> = {\n \"no-addressed-envelope\": (epoch) =>\n `Container KEK log has no envelope addressed to this requester at epoch ${epoch}`,\n \"corrupt-envelope\": (epoch) =>\n `Container KEK log envelope for this requester at epoch ${epoch} could not be opened`,\n \"principal-key-unreachable\": (epoch) =>\n `Container KEK log envelope at epoch ${epoch} is addressed to a principal key epoch this client cannot resolve`,\n \"parent-key-unavailable\": (epoch) =>\n `Container KEK log envelope at epoch ${epoch} is inherited from a parent container whose KEK was not supplied`,\n};\n\nexport class HistoricalWrapUnavailableError extends Error {\n constructor(\n readonly containerKeyEpoch: number,\n readonly reason: HistoricalWrapUnavailableReason,\n cause?: unknown,\n ) {\n super(\n HISTORICAL_WRAP_UNAVAILABLE_MESSAGES[reason](containerKeyEpoch),\n cause === undefined ? undefined : { cause },\n );\n this.name = \"HistoricalWrapUnavailableError\";\n }\n}\n\n/**\n * The first candidate envelope that both decrypts AND matches the epoch's\n * material-id commitment.\n *\n * Candidates are tried ONE AT A TIME. An envelope can be AEAD-valid and still\n * carry the WRONG key — a stale or misaddressed wrap — so decrypting a batch\n * together would let that one envelope answer for the whole group and mask a\n * later envelope that would have opened the epoch. The reported failure is the\n * first real decrypt failure, since a commitment mismatch is a wrong envelope\n * rather than an unreachable one.\n */\nasync function firstCommittedAnchor(input: {\n attempt: (\n wraps: ContainerKeyWrap[],\n ) => Promise<{ failure: unknown; key: Uint8Array | null }>;\n candidates: ContainerKeyWrap[];\n containerId: string;\n containerKeyEpoch: number;\n containerKeyEpochId: string;\n}): Promise<{ failure: unknown; key: Uint8Array | null }> {\n let firstFailure: unknown;\n for (const candidate of input.candidates) {\n const result = await input.attempt([candidate]);\n if (result.key === null) {\n firstFailure ??= result.failure;\n continue;\n }\n const materialId = await computeContainerKekMaterialId({\n containerId: input.containerId,\n keyEpoch: input.containerKeyEpoch,\n keyMaterial: result.key,\n });\n if (materialId === input.containerKeyEpochId) {\n return result;\n }\n // An envelope that decrypts to the WRONG key is a misaddressed or stale\n // envelope, not an unreachable principal. Recording it keeps the reported\n // reason `corrupt-envelope`; dropping it silently would let the epoch\n // report as unaddressed, which points a repair at the wrong problem.\n //\n // Only reachable when the server files a wrap under an epoch id its\n // material does not hash to — the addressing filter already rejects a\n // wrap whose epoch id simply differs.\n firstFailure ??= new Error(\n `Container KEK envelope decrypts to material that is not epoch ${input.containerKeyEpochId}`,\n );\n }\n return { failure: firstFailure, key: null };\n}\n\n/**\n * Opens one group of envelopes with the requester's CURRENT policies, folding\n * failure into a value rather than throwing.\n *\n * The caller tries several groups in order, and a failure in one is not the\n * end of the search — so the failure has to travel back alongside the miss\n * instead of unwinding the whole recovery.\n */\nasync function unwrapWrapsWithCurrentPolicies(input: {\n execSql?: ExecSql | undefined;\n secretKey: Uint8Array;\n wraps: readonly ContainerKeyWrap[];\n}): Promise<{ failure: unknown; key: Uint8Array | null }> {\n if (input.wraps.length === 0) {\n return { failure: undefined, key: null };\n }\n try {\n return {\n failure: undefined,\n key: await unwrapKeyEnvelopesWithPrincipalPolicies({\n envelopes: input.wraps.map((wrap) => ({\n keyFingerprint: wrap.recipientKeyFingerprint,\n kemCipherText: wrap.kemCipherText,\n wrappedKey: wrap.wrappedKey,\n })),\n execSql: input.execSql,\n secretKey: input.secretKey,\n }),\n };\n } catch (error) {\n return { failure: error, key: null };\n }\n}\n\n/**\n * The severed-bridge backstop: recovers one historical epoch's KEK from the\n * requester's own retained recipient envelope served in the kek-log. Wraps\n * are written by that epoch's rotator and never deleted, so this path is\n * independent of every later rotation — it works even when the bridge chain\n * above the epoch is destroyed. The recovered key is verified against the\n * epoch id's material commitment before use.\n *\n * Scope bound: a direct user envelope is recoverable from identity keys\n * alone, so a pristine client anchors on it. A group- or organization-\n * addressed envelope additionally requires the secret key of the principal\n * key epoch it was addressed to. Principal rotation rematerializes retained\n * grants by wrapping only the container's current epoch, so an older epoch\n * whose only anchor is a group wrap from before a principal key rotation needs\n * a principal key this client cannot resolve. Such epochs fail closed with\n * `HistoricalWrapUnavailableError` (`principal-key-unreachable`) rather than\n * appearing to be corruption.\n */\nexport async function recoverKeyringEntryFromWraps(input: {\n containerId: string;\n epoch: Pick<\n ContainerKekLogEpochResponse,\n \"containerKeyEpoch\" | \"containerKeyEpochId\" | \"wraps\"\n >;\n execSql?: ExecSql | undefined;\n secretKey: Uint8Array;\n /**\n * Parent container KEKs by epoch id. An inherited-only child has no direct\n * or principal envelope of its own; its anchor is the parent-container\n * wrap, which opens under the parent epoch's KEK. Recover the parent's\n * epoch first (recursively, through the parent's own log) and pass it here.\n */\n parentKeysByEpochId?: ReadonlyMap<string, Uint8Array> | undefined;\n /**\n * The requester's user id. Direct user envelopes are matched against it, so\n * another member's envelope is never mistaken for an anchor — attempting it\n * would only produce a misleading failure.\n */\n userId: string;\n}): Promise<ContainerKekKeyringEntry> {\n const { directWraps, epochWraps, parentWraps, principalWraps } =\n partitionAnchorWraps(\n input.epoch.wraps,\n input.epoch.containerKeyEpochId,\n input.userId,\n );\n if (epochWraps.length === 0) {\n throw new HistoricalWrapUnavailableError(\n input.epoch.containerKeyEpoch,\n \"no-addressed-envelope\",\n );\n }\n\n const attempt = (wraps: typeof epochWraps) =>\n unwrapWrapsWithCurrentPolicies({\n execSql: input.execSql,\n secretKey: input.secretKey,\n wraps,\n });\n\n // A direct envelope is decryptable from identity keys alone, so its failure\n // means corruption — not an unreachable principal key. Neither failure ends\n // the search: a parent-container wrap may still anchor this epoch.\n //\n // Candidates are tried ONE AT A TIME, and each must match the epoch's\n // material-id commitment rather than merely decrypt. An envelope can be\n // AEAD-valid and still carry the WRONG key — a stale or misaddressed wrap —\n // so batching would let that one envelope answer for the whole group and\n // mask a later envelope that would have opened the epoch.\n const attemptCommitted = (candidates: ContainerKeyWrap[]) =>\n firstCommittedAnchor({\n attempt,\n candidates,\n containerId: input.containerId,\n containerKeyEpoch: input.epoch.containerKeyEpoch,\n containerKeyEpochId: input.epoch.containerKeyEpochId,\n });\n\n const direct = await attemptCommitted(directWraps);\n let keyMaterial = direct.key;\n const directFailure = direct.failure;\n let principalFailure: unknown;\n if (keyMaterial === null) {\n const principal = await attemptCommitted(principalWraps);\n keyMaterial = principal.key;\n // Preserve the current-policy failure in case no inherited anchor opens.\n principalFailure = principal.failure;\n }\n // The inherited-only path: a parent-container envelope opens under the\n // parent epoch's KEK, which the caller recovers from the parent's own log.\n const parent =\n keyMaterial === null\n ? await openParentAnchor(\n parentWraps,\n input.parentKeysByEpochId ?? new Map(),\n )\n : { failure: undefined, key: null, keyWasAvailable: false };\n keyMaterial ??= parent.key;\n const parentFailure = parent.failure;\n const parentKeyWasAvailable = parent.keyWasAvailable;\n if (keyMaterial === null) {\n throwMostSpecificAnchorFailure({\n containerKeyEpoch: input.epoch.containerKeyEpoch,\n directFailure,\n hadParentWraps: parentWraps.length > 0,\n parentFailure,\n parentKeyWasAvailable,\n principalFailure,\n });\n }\n const expectedId = await computeContainerKekMaterialId({\n containerId: input.containerId,\n keyEpoch: input.epoch.containerKeyEpoch,\n keyMaterial,\n });\n if (expectedId !== input.epoch.containerKeyEpochId) {\n throw new Error(\n `Container KEK log wrap does not match its committed epoch id at epoch ${input.epoch.containerKeyEpoch}`,\n );\n }\n return {\n containerKeyEpochId: input.epoch.containerKeyEpochId,\n keyMaterial,\n };\n}\n\nexport { fetchContainerKekLog } from \"./containerKekLogFetch\";\nexport type {\n AggregatedContainerKekLog,\n KeyringRebuildResult,\n} from \"./keyringLogWalk\";\nexport { rebuildKeyringEntriesFromLog } from \"./keyringLogWalk\";\n"]}
|
package/package.json
CHANGED