@tinycloud/replica 0.2.4-beta.2 → 0.3.0-beta.4
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 +29 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +7 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +20 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/kv-storage-testing.d.ts +83 -0
- package/dist/kv-storage-testing.js +1145 -0
- package/dist/kv-storage-testing.js.map +1 -0
- package/dist/kv-storage.d.ts +155 -0
- package/dist/kv-storage.js +2587 -0
- package/dist/kv-storage.js.map +1 -0
- package/dist/kv-storage.worker.618d3a20b6f3f54ed78b68f1dc244dddc3daba46e91e9ae2b577685cc9339315.js +18891 -0
- package/dist/kv-storage.worker.d.ts +2 -0
- package/dist/kv-storage.worker.js +18891 -0
- package/dist/kv-storage.worker.js.map +1 -0
- package/dist/profile-gate.cjs +560 -0
- package/dist/profile-gate.cjs.map +1 -0
- package/dist/profile-gate.d.cts +89 -0
- package/dist/profile-gate.d.ts +89 -0
- package/dist/profile-gate.js +517 -0
- package/dist/profile-gate.js.map +1 -0
- package/dist/replica.worker.js +444 -171
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +926 -239
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +77 -8
- package/dist/sqlite.d.ts +77 -8
- package/dist/sqlite.js +914 -229
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-f3-kYLQZ.d.cts → types-CQ61VnsP.d.cts} +1 -1
- package/dist/{types-f3-kYLQZ.d.ts → types-CQ61VnsP.d.ts} +1 -1
- package/package.json +20 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profile-gate/index.ts","../src/profile-gate/gateFile.ts","../../sdk-services/src/kv/replication/grantPolicyConstants.ts","../../sdk-services/src/kv/replication/contractConstants.ts","../src/errors.ts","../src/profile-gate/saltedHash.ts"],"sourcesContent":["export {\n advanceInactiveGate,\n assertGateAccess,\n clearGateIntent,\n denyGate,\n deferredGateToken,\n establishSessionStamp,\n identityHashOf,\n grantInstallHashOf,\n principalHashOf,\n readFirstInstall,\n resumeDeferredGate,\n readGateSnapshot,\n readLogoutGate,\n REPLICA_DENY_FLOOR_FILE,\n recordFirstInstall,\n recordGrantInstall,\n restoreSessionStamp,\n} from \"./gateFile.js\";\nexport type { DeferredEraseToken } from \"./gateFile.js\";\n","/**\n * Node/CLI lifecycle gate file (`replica-gate.json`, CONTRACT.md §2). One file per profile holds the\n * account gate (salted principal hash, no plaintext principal), the outstanding purge intents and\n * the device-level first-install anchors.\n *\n * Callers hold the profile lock around every call here. Writes go to a temp file that is fsynced\n * and renamed over the target, so a crash leaves either the old or the new state. Every salted hash\n * is derived before the file is read or written, never inside a held transaction.\n */\nimport { randomBytes, randomUUID } from \"node:crypto\";\nimport { mkdir, open, readFile, rename, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n ReplicaAccountGate,\n ReplicaDenyInput,\n ReplicaDenyOutcome,\n ReplicaGateAccess,\n ReplicaGateReason,\n ReplicaPurgeIntent,\n ReplicaSessionStamp,\n} from \"@tinycloud/sdk-services/kv/replication\";\nimport { REPLICA_GATE_FILE } from \"@tinycloud/sdk-services/kv/replication/contractConstants\";\nimport { gatedError } from \"../errors\";\nimport { saltedHash } from \"./saltedHash\";\n\nconst GATE_FILE_VERSION = 1;\n/**\n * Durable deny floor beside the gate, written before each deny commits. It survives loss of the\n * gate file: a missing gate, or a gate behind this floor, fails closed instead of being recreated.\n */\nexport const REPLICA_DENY_FLOOR_FILE = \"replica-deny-floor.json\";\nconst DENY_FLOOR_FILE = REPLICA_DENY_FLOOR_FILE;\n\ninterface DenyFloor {\n epoch: number;\n deniedAt: number;\n}\n\nexport interface NodeGateState extends ReplicaAccountGate {\n intents: ReplicaPurgeIntent[];\n /** Identity key hash to first-install time (ms). Cleared by every committed deny. */\n firstInstall: Record<string, number>;\n /** Salted identity+CID hash to immutable first-install time; survives targeted partition purge. */\n grantInstall: Record<string, number>;\n /** Identity key hash to generation; advanced once with a newly queued pending intent. */\n pendingGenerations: Record<string, number>;\n /** Deferred revoke blocks recovery from erasing these already-durable intents. */\n eraseDeferred: boolean;\n /** Salted original principal, retained after resume to recognize idempotent retries. */\n deferredPrincipalHash: string | null;\n}\n\nfunction gateRefusal(gate: ReplicaGateReason, message: string) {\n return gatedError(gate, message);\n}\n\n/** Lowercase hex HMAC-SHA-256(salt, lower-cased principal DID). The only principal form stored. */\nexport function principalHashOf(salt: string, principal: string): Promise<string> {\n return saltedHash(salt, principal.toLowerCase());\n}\n\n/** Salted opaque identity id, derived before a guarded data operation. */\nexport function identityHashOf(salt: string, identityKey: string): Promise<string> {\n return saltedHash(salt, identityKey);\n}\n\n/** One account identity and signed CID, without persisting a plaintext CID in gate metadata. */\nexport function grantInstallHashOf(salt: string, identityKeyHash: string, cid: string): Promise<string> {\n return saltedHash(salt, `grantInstall\\0${identityKeyHash}\\0${cid}`);\n}\nfunction parseState(text: string): NodeGateState {\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file is not valid JSON.\");\n }\n if (typeof raw !== \"object\" || raw === null) throw gateRefusal(\"gate_unavailable\", \"The replica gate file is malformed.\");\n const record = raw as Record<string, unknown>;\n if (record.v !== GATE_FILE_VERSION) throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an unknown version.\");\n if (typeof record.incarnation !== \"string\" || record.incarnation.length === 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has no incarnation.\");\n }\n // Canonical base64url that decodes to at least 32 bytes.\n if (\n typeof record.salt !== \"string\" ||\n Buffer.from(record.salt, \"base64url\").toString(\"base64url\") !== record.salt ||\n Buffer.from(record.salt, \"base64url\").length < 32\n ) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid salt.\");\n }\n if (record.principalHash !== null &&\n (typeof record.principalHash !== \"string\" || !/^[0-9a-f]{64}$/.test(record.principalHash))) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid principal hash.\");\n }\n if (typeof record.lifecycleEpoch !== \"number\" || !Number.isSafeInteger(record.lifecycleEpoch) || record.lifecycleEpoch < 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid lifecycle epoch.\");\n }\n if (record.deniedAt !== null && (typeof record.deniedAt !== \"number\" ||\n !Number.isSafeInteger(record.deniedAt) || record.deniedAt < 0)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid deniedAt.\");\n }\n if (!Array.isArray(record.intents)) throw gateRefusal(\"gate_unavailable\", \"The replica gate file has no intents list.\");\n for (const intent of record.intents) {\n if (typeof intent !== \"object\" || intent === null || typeof intent.id !== \"string\" ||\n !/^[0-9a-f]{64}$/.test(intent.principalHash) || !Number.isSafeInteger(intent.recordedAt) ||\n intent.recordedAt < 0 ||\n (intent.kind === \"pending\"\n ? !/^[0-9a-f]{64}$/.test(intent.identityKeyHash) || intent.id !== `pending:${intent.identityKeyHash}`\n : intent.kind !== \"partition\" || typeof intent.partitionId !== \"string\" ||\n intent.partitionId.length === 0 || intent.id !== intent.partitionId)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid purge intent.\");\n }\n }\n const firstInstall = record.firstInstall;\n if (typeof firstInstall !== \"object\" || firstInstall === null || Array.isArray(firstInstall)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has no valid firstInstall map.\");\n }\n for (const [hash, value] of Object.entries(firstInstall)) {\n if (!/^[0-9a-f]{64}$/.test(hash) || typeof value !== \"number\" ||\n !Number.isSafeInteger(value) || value < 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid firstInstall entry.\");\n }\n }\n const grantInstall = Object.prototype.hasOwnProperty.call(record, \"grantInstall\") ? record.grantInstall : {};\n if (typeof grantInstall !== \"object\" || grantInstall === null || Array.isArray(grantInstall)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate has an invalid grantInstall map.\");\n }\n for (const [hash, installedAt] of Object.entries(grantInstall)) {\n if (!/^[0-9a-f]{64}$/.test(hash) || !Number.isSafeInteger(installedAt) || (installedAt as number) < 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate has an invalid grant install time.\");\n }\n }\n const pendingGenerations = record.pendingGenerations;\n if (typeof pendingGenerations !== \"object\" || pendingGenerations === null || Array.isArray(pendingGenerations)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid pending generation map.\");\n }\n for (const [hash, generation] of Object.entries(pendingGenerations)) {\n if (!/^[0-9a-f]{64}$/.test(hash) || typeof generation !== \"number\" ||\n !Number.isSafeInteger(generation) || generation < 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file has an invalid pending generation.\");\n }\n }\n const eraseDeferred = Object.prototype.hasOwnProperty.call(record, \"eraseDeferred\") ? record.eraseDeferred : false;\n const deferredPrincipalHash = Object.prototype.hasOwnProperty.call(record, \"deferredPrincipalHash\")\n ? record.deferredPrincipalHash : null;\n if (typeof eraseDeferred !== \"boolean\" ||\n (deferredPrincipalHash !== null && (typeof deferredPrincipalHash !== \"string\" ||\n !/^[0-9a-f]{64}$/.test(deferredPrincipalHash))) ||\n (eraseDeferred && (record.principalHash !== null || deferredPrincipalHash === null))) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate has an invalid deferred erase state.\");\n }\n return {\n incarnation: record.incarnation,\n salt: record.salt,\n principalHash: record.principalHash,\n lifecycleEpoch: record.lifecycleEpoch,\n deniedAt: record.deniedAt,\n intents: record.intents as ReplicaPurgeIntent[],\n firstInstall: firstInstall as Record<string, number>,\n grantInstall: grantInstall as Record<string, number>,\n pendingGenerations: pendingGenerations as Record<string, number>,\n eraseDeferred,\n deferredPrincipalHash,\n };\n}\n\nfunction isNotFound(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"ENOENT\";\n}\n\n/** The gate file exactly as stored, without the deny-floor check. */\nasync function readStoredState(profileDir: string): Promise<NodeGateState | undefined> {\n let text: string;\n try {\n text = await readFile(join(profileDir, REPLICA_GATE_FILE), \"utf8\");\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw gateRefusal(\"gate_unavailable\", \"The replica gate file cannot be read.\");\n }\n return parseState(text);\n}\n\n/** The last committed deny floor, or `undefined` before the first deny. Malformed fails closed. */\nasync function readDenyFloor(profileDir: string): Promise<DenyFloor | undefined> {\n let record: unknown;\n try {\n record = JSON.parse(await readFile(join(profileDir, DENY_FLOOR_FILE), \"utf8\"));\n } catch (error) {\n if (isNotFound(error)) return undefined;\n throw gateRefusal(\"gate_unavailable\", \"The replica deny floor cannot be read.\");\n }\n if (typeof record !== \"object\" || record === null || Array.isArray(record) || Object.keys(record).length !== 2 ||\n !(\"epoch\" in record) || !(\"deniedAt\" in record) ||\n !Number.isSafeInteger(record.epoch) || (record.epoch as number) < 1 ||\n !Number.isSafeInteger(record.deniedAt) || (record.deniedAt as number) < 0) {\n throw gateRefusal(\"gate_unavailable\", \"The replica deny floor is malformed.\");\n }\n return { epoch: record.epoch as number, deniedAt: record.deniedAt as number };\n}\n\n/**\n * A lost or rolled-back gate after a deny stays closed. No replica reset rewrites the gate, so the\n * recovery is a fresh profile; network access is unaffected.\n */\nconst LOST_GATE_RECOVERY =\n \"Local replicas stay disabled for this profile; to start over, delete it (`tc profile delete <name>`; \" +\n \"for the default profile, switch the default first with `tc profile switch <other>`) and sign in again.\";\n\n/** An interrupted deny wrote its floor but not the gate; `tc auth logout` finishes it. */\nconst INTERRUPTED_SIGN_OUT =\n \"A previous sign-out was interrupted; re-running `tc auth logout` completes it.\";\n\nfunction behindFloor(state: NodeGateState, floor: DenyFloor): boolean {\n return state.lifecycleEpoch < floor.epoch || state.deniedAt === null || state.deniedAt < floor.deniedAt;\n}\n\n/**\n * The stored state, or `undefined` when no gate file exists and no deny ever committed. Never\n * creates the file. After a deny, a missing gate or one behind the floor fails closed.\n */\nasync function readState(profileDir: string): Promise<NodeGateState | undefined> {\n const state = await readStoredState(profileDir);\n const floor = await readDenyFloor(profileDir);\n if (floor === undefined) return state;\n if (state === undefined) {\n throw gateRefusal(\"gate_unavailable\", `The replica gate is missing after a sign-out. ${LOST_GATE_RECOVERY}`);\n }\n if (behindFloor(state, floor)) {\n throw gateRefusal(\"gate_unavailable\", `The replica gate is behind its last sign-out. ${INTERRUPTED_SIGN_OUT}`);\n }\n return state;\n}\n\n/** Control-only snapshot. In particular, a missing gate is never minted by a pending read. */\nexport function readGateSnapshot(profileDir: string): Promise<NodeGateState | undefined> {\n return readState(profileDir);\n}\n\n/**\n * Logout's read: the stored gate past the floor check, and whether it lags that floor (an\n * interrupted deny). A missing gate after a deny still fails closed. Never creates the file.\n */\nexport async function readLogoutGate(\n profileDir: string,\n): Promise<{ state: NodeGateState; floorLagging: boolean } | undefined> {\n const state = await readStoredState(profileDir);\n const floor = await readDenyFloor(profileDir);\n if (state === undefined) {\n if (floor !== undefined) {\n throw gateRefusal(\"gate_unavailable\", `The replica gate is missing after a sign-out. ${LOST_GATE_RECOVERY}`);\n }\n return undefined;\n }\n return { state, floorLagging: floor !== undefined && behindFloor(state, floor) };\n}\n\n/**\n * Complete an interrupted sign-out of an inactive gate: past both the gate and its floor, floor\n * first, changing only the epoch and denial time. Anything but an inactive gate is `stamp_stale`.\n * The caller holds the profile lock.\n */\nexport async function advanceInactiveGate(profileDir: string): Promise<NodeGateState> {\n const state = await readStoredState(profileDir);\n const floor = await readDenyFloor(profileDir);\n if (state === undefined || state.principalHash !== null || state.eraseDeferred) {\n throw gateRefusal(\"stamp_stale\", \"Only an inactive replica gate can be advanced past its floor.\");\n }\n const lifecycleEpoch = Math.max(state.lifecycleEpoch, floor?.epoch ?? 0) + 1;\n const deniedAt = Math.max(Date.now(), (state.deniedAt ?? -1) + 1, (floor?.deniedAt ?? -1) + 1);\n if (!Number.isSafeInteger(lifecycleEpoch) || !Number.isSafeInteger(deniedAt)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica deny floor cannot advance.\");\n }\n const next: NodeGateState = { ...state, lifecycleEpoch, deniedAt };\n await writeAtomic(profileDir, DENY_FLOOR_FILE, JSON.stringify({ epoch: lifecycleEpoch, deniedAt }));\n await writeState(profileDir, next);\n return next;\n}\n\n/** Call under the profile lock; caller derives both hashes before entering its data section. */\nexport async function assertGateAccess(\n profileDir: string,\n access: ReplicaGateAccess,\n salt: string,\n): Promise<NodeGateState> {\n if (access.stamp === undefined) {\n throw gateRefusal(\"not_established\", \"The replica session has no stamp.\");\n }\n const state = await readState(profileDir);\n if (state === undefined || state.salt !== salt) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate is unavailable or was replaced.\");\n }\n if (state.incarnation !== access.incarnation) {\n throw gateRefusal(\"stamp_stale\", \"The replica gate was replaced.\");\n }\n if (state.lifecycleEpoch !== access.stamp.lifecycleEpoch || state.incarnation !== access.stamp.incarnation) {\n throw gateRefusal(\"stamp_stale\", \"The replica session stamp has changed.\");\n }\n if (state.principalHash === null || state.principalHash !== access.principalHash) {\n throw gateRefusal(\"principal_mismatch\", \"The replica belongs to a different account.\");\n }\n if (state.intents.some((intent) =>\n (intent.kind === \"partition\" && intent.partitionId === access.partitionId) ||\n (intent.kind === \"pending\" && intent.identityKeyHash === access.identityKeyHash)\n )) {\n throw gateRefusal(\"purge_pending\", \"This replica has a pending erase.\");\n }\n return state;\n}\n\n/** Erase precedes this operation. Keeping generations makes replay and stale-store fencing durable. */\nexport async function clearGateIntent(profileDir: string, id: string): Promise<void> {\n const state = await readState(profileDir);\n if (state === undefined || !state.intents.some((intent) => intent.id === id)) return;\n if (state.eraseDeferred) throw gateRefusal(\"purge_pending\", \"Deferred replica erase has not been resumed.\");\n await writeState(profileDir, { ...state, intents: state.intents.filter((intent) => intent.id !== id) });\n}\n\n/** temp + fsync + rename + directory fsync: a crash leaves either the old or the new file. */\nasync function writeAtomic(profileDir: string, name: string, body: string): Promise<void> {\n await mkdir(profileDir, { recursive: true });\n const target = join(profileDir, name);\n const temp = `${target}.${randomUUID()}.tmp`;\n try {\n const handle = await open(temp, \"w\", 0o600);\n try {\n await handle.writeFile(body);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(temp, target);\n const directory = await open(profileDir, \"r\");\n try {\n await directory.sync();\n } finally {\n await directory.close();\n }\n } catch (error) {\n await rm(temp, { force: true });\n throw error;\n }\n}\n\nfunction writeState(profileDir: string, state: NodeGateState): Promise<void> {\n return writeAtomic(profileDir, REPLICA_GATE_FILE, JSON.stringify({ v: GATE_FILE_VERSION, ...state }));\n}\n\nfunction freshState(): NodeGateState {\n return {\n incarnation: randomUUID(),\n salt: randomBytes(32).toString(\"base64url\"),\n principalHash: null,\n lifecycleEpoch: 0,\n deniedAt: null,\n intents: [],\n firstInstall: {},\n grantInstall: {},\n pendingGenerations: {},\n eraseDeferred: false,\n deferredPrincipalHash: null,\n };\n}\n\n/** Reads the gate, creating it (fresh incarnation and salt) when absent. Fails closed on a corrupt file. */\nexport async function readGateState(profileDir: string): Promise<NodeGateState> {\n const existing = await readState(profileDir);\n if (existing !== undefined) return existing;\n const created = freshState();\n await writeState(profileDir, created);\n return created;\n}\n\n/** The device-level first-install anchor for one identity, or `undefined` when none is recorded. */\nexport async function readFirstInstall(profileDir: string, identityKeyHash: string): Promise<number | undefined> {\n const state = await readState(profileDir);\n if (state?.eraseDeferred) throw gateRefusal(\"purge_pending\", \"Deferred replica erase has not been resumed.\");\n return state?.firstInstall[identityKeyHash];\n}\n\n/** Records the first-install anchor only when absent. Returns the stored value. */\nexport async function recordFirstInstall(profileDir: string, identityKeyHash: string, atMs: number): Promise<number> {\n if (!/^[0-9a-f]{64}$/.test(identityKeyHash) || !Number.isSafeInteger(atMs) || atMs < 0) {\n throw gateRefusal(\"gate_unavailable\", \"A salted identity hash and finite first-install time are required.\");\n }\n const state = await readGateState(profileDir);\n if (state.eraseDeferred) throw gateRefusal(\"purge_pending\", \"Deferred replica erase has not been resumed.\");\n const existing = state.firstInstall[identityKeyHash];\n if (existing !== undefined) return existing;\n await writeState(profileDir, { ...state, firstInstall: { ...state.firstInstall, [identityKeyHash]: atMs } });\n return atMs;\n}\n\n/** A per-CID install floor, persisted before committing a SQLite grant install. Call under the profile lock. */\nexport async function recordGrantInstall(profileDir: string, hash: string, atMs: number): Promise<number> {\n if (!/^[0-9a-f]{64}$/.test(hash) || !Number.isSafeInteger(atMs) || atMs < 0) {\n throw gateRefusal(\"gate_unavailable\", \"A salted grant hash and finite install time are required.\");\n }\n const state = await readState(profileDir);\n if (state === undefined || state.principalHash === null) {\n throw gateRefusal(\"gate_unavailable\", \"The replica account gate is not established.\");\n }\n if (state.eraseDeferred || state.intents.length > 0) {\n throw gateRefusal(\"purge_pending\", \"A purge is pending for this profile.\");\n }\n const previous = state.grantInstall[hash];\n if (previous !== undefined && previous <= atMs) return previous;\n await writeState(profileDir, { ...state, grantInstall: { ...state.grantInstall, [hash]: atMs } });\n return atMs;\n}\n\n/**\n * `establishSessionStamp` (fresh sign-in only). Rejects `purge_pending` while any intent is\n * outstanding, then `stamp_stale` when `siweIssuedAt` (ms) is not after `deniedAt`. A null gate\n * principal is set to the caller's (no epoch bump); the same principal returns the current stamp;\n * another principal is `principal_mismatch`. Writes only when the principal is first set.\n */\nexport async function establishSessionStamp(\n profileDir: string,\n principal: string,\n o: { siweIssuedAt: number },\n): Promise<ReplicaSessionStamp> {\n if (!Number.isSafeInteger(o?.siweIssuedAt) || o.siweIssuedAt < 0) {\n throw gateRefusal(\"not_established\", \"A finite signed SIWE issued-at time is required.\");\n }\n const issuedAt = Math.min(o.siweIssuedAt, Date.now());\n const state = await readGateState(profileDir);\n if (state.eraseDeferred || state.intents.length > 0) throw gateRefusal(\"purge_pending\", \"A purge is pending for this profile.\");\n if (state.deniedAt !== null && issuedAt <= state.deniedAt) {\n throw gateRefusal(\"stamp_stale\", \"The sign-in predates the last sign-out.\");\n }\n const hash = await principalHashOf(state.salt, principal);\n if (state.principalHash === null) {\n await writeState(profileDir, { ...state, principalHash: hash, deferredPrincipalHash: null });\n } else if (state.principalHash !== hash) {\n throw gateRefusal(\"principal_mismatch\", \"The profile belongs to another account.\");\n }\n return { incarnation: state.incarnation, lifecycleEpoch: state.lifecycleEpoch };\n}\n\n/**\n * `restoreSessionStamp`: validates the persisted stamp and never writes. An absent gate file never\n * gets created here; with no gate, a persisted stamp is stale and no stamp is not established.\n */\nexport async function restoreSessionStamp(\n profileDir: string,\n principal: string,\n persisted: ReplicaSessionStamp | undefined,\n): Promise<ReplicaSessionStamp> {\n const state = await readState(profileDir);\n if (state !== undefined && (state.eraseDeferred || state.intents.length > 0)) {\n throw gateRefusal(\"purge_pending\", \"A purge is pending for this profile.\");\n }\n if (persisted === undefined) throw gateRefusal(\"not_established\", \"The session has no replica stamp.\");\n if (\n typeof persisted !== \"object\" || persisted === null || Array.isArray(persisted) ||\n Object.keys(persisted).length !== 2 ||\n !Object.prototype.hasOwnProperty.call(persisted, \"incarnation\") ||\n !Object.prototype.hasOwnProperty.call(persisted, \"lifecycleEpoch\") ||\n typeof persisted.incarnation !== \"string\" || persisted.incarnation.length === 0 ||\n !Number.isSafeInteger(persisted.lifecycleEpoch) || persisted.lifecycleEpoch < 0\n ) {\n throw gateRefusal(\"stamp_stale\", \"The persisted replica stamp is malformed.\");\n }\n if (state === undefined) throw gateRefusal(\"stamp_stale\", \"The replica gate no longer exists.\");\n if (persisted.incarnation !== state.incarnation || persisted.lifecycleEpoch !== state.lifecycleEpoch) {\n throw gateRefusal(\"stamp_stale\", \"The session stamp is from an earlier sign-in.\");\n }\n if (state.principalHash !== (await principalHashOf(state.salt, principal))) {\n throw gateRefusal(\"principal_mismatch\", \"The profile belongs to another account.\");\n }\n return { incarnation: persisted.incarnation, lifecycleEpoch: persisted.lifecycleEpoch };\n}\n\n/**\n * `deny` step 1. If `input.principal` does not match the gate, returns `stale` and writes nothing.\n * Otherwise one atomic rewrite clears the principal, increments epoch and pending generations\n * for new pending intents, records the deduplicated intents and clears first-install anchors.\n * A failed write returns `unguarded`. The caller holds the (reentrant) profile lock throughout.\n */\nexport async function denyGate(\n profileDir: string,\n input: ReplicaDenyInput,\n collectIntents: (principalHash: string) => Promise<readonly ReplicaPurgeIntent[]>,\n precomputed?: { salt: string; principalHash: string },\n options?: { deferErase?: boolean },\n): Promise<ReplicaDenyOutcome> {\n if (input.reason === \"account_switch\" && input.next === undefined) {\n throw new TypeError(\"An account switch needs the incoming principal.\");\n }\n if (options?.deferErase === true && input.reason !== \"sign_out\") {\n throw new TypeError(\"Deferred erase is only available for sign-out.\");\n }\n try {\n // Read past the floor check: a deny may always move a gate that fell behind its floor forward.\n const state = await readStoredState(profileDir);\n const floor = await readDenyFloor(profileDir);\n if (state === undefined && floor !== undefined) {\n throw gateRefusal(\"gate_unavailable\", `The replica gate is missing after a sign-out. ${LOST_GATE_RECOVERY}`);\n }\n if (state === undefined || state.principalHash === null) return { status: \"stale\" };\n if (precomputed !== undefined && state.salt !== precomputed.salt) {\n throw gateRefusal(\"gate_unavailable\", \"The replica gate salt changed before denial.\");\n }\n if (state.principalHash !== (precomputed?.principalHash ?? await principalHashOf(state.salt, input.principal))) {\n return { status: \"stale\" };\n }\n const intents = await collectIntents(state.principalHash);\n const known = new Set(state.intents.map((intent) => intent.id));\n const added: ReplicaPurgeIntent[] = [];\n for (const intent of intents) {\n if (known.has(intent.id)) continue;\n known.add(intent.id);\n added.push(intent);\n }\n const pendingGenerations = { ...state.pendingGenerations };\n for (const intent of added) {\n if (intent.kind !== \"pending\") continue;\n const hash = intent.identityKeyHash;\n if (!/^[0-9a-f]{64}$/.test(hash) || intent.id !== `pending:${hash}` ||\n !Number.isSafeInteger((pendingGenerations[hash] ?? 0) + 1)) {\n throw gateRefusal(\"gate_unavailable\", \"A pending purge intent has an invalid identity generation.\");\n }\n pendingGenerations[hash] = (pendingGenerations[hash] ?? 0) + 1;\n }\n const merged = [...state.intents, ...added];\n const lifecycleEpoch = Math.max(state.lifecycleEpoch, floor?.epoch ?? 0) + 1;\n const deniedAt = Math.max(Date.now(), (state.deniedAt ?? -1) + 1, (floor?.deniedAt ?? -1) + 1);\n if (!Number.isSafeInteger(lifecycleEpoch) || !Number.isSafeInteger(deniedAt)) {\n throw gateRefusal(\"gate_unavailable\", \"The replica deny floor cannot advance.\");\n }\n const next: NodeGateState = {\n ...state,\n principalHash: null,\n lifecycleEpoch,\n deniedAt,\n intents: merged,\n firstInstall: {},\n grantInstall: {},\n pendingGenerations,\n eraseDeferred: options?.deferErase === true,\n deferredPrincipalHash: options?.deferErase === true ? state.principalHash : null,\n };\n // The floor commits first, so a crash between the two writes leaves the gate behind it: closed.\n await writeAtomic(profileDir, DENY_FLOOR_FILE, JSON.stringify({ epoch: lifecycleEpoch, deniedAt }));\n await writeState(profileDir, next);\n return { status: \"denied\", lifecycleEpoch: next.lifecycleEpoch, intents: merged.length };\n } catch (error) {\n const code = typeof error === \"object\" && error !== null && \"code\" in error && typeof error.code === \"string\"\n ? error.code\n : \"STORAGE_ERROR\";\n return { status: \"unguarded\", code };\n }\n}\n\n/** Local fencing token; not a bearer capability. Caller holds the profile lock. */\nexport interface DeferredEraseToken {\n deniedEpoch: number;\n principalHash: string;\n}\n\n/** Read-only inspection of a deferred denial. A missing gate never creates one. */\nexport async function deferredGateToken(profileDir: string): Promise<DeferredEraseToken | null> {\n const state = await readState(profileDir);\n return state?.eraseDeferred\n ? { deniedEpoch: state.lifecycleEpoch, principalHash: state.deferredPrincipalHash! }\n : null;\n}\n\n/** Resume only the denial identified by the same durable epoch and original principal. */\nexport async function resumeDeferredGate(profileDir: string, token: DeferredEraseToken): Promise<boolean> {\n const state = await readState(profileDir);\n if (state === undefined) throw gateRefusal(\"gate_unavailable\", \"The replica gate no longer exists.\");\n if (!Number.isSafeInteger(token?.deniedEpoch) || token.deniedEpoch < 0 ||\n typeof token.principalHash !== \"string\" || !/^[0-9a-f]{64}$/.test(token.principalHash) ||\n state.lifecycleEpoch !== token.deniedEpoch || state.deferredPrincipalHash !== token.principalHash ||\n state.principalHash !== null) {\n throw gateRefusal(\"stamp_stale\", \"The deferred erase token is stale or belongs to another account.\");\n }\n if (!state.eraseDeferred) return false;\n await writeState(profileDir, { ...state, eraseDeferred: false });\n return true;\n}\n","/** Grant lifetime constants without browser storage identifiers. */\nconst HOUR_MS = 60 * 60 * 1000;\nconst DAY_MS = 24 * HOUR_MS;\n\n/** Default device-grant lifetime from install (24 h). All durations are milliseconds. */\nexport const DEFAULT_GRANT_TTL_MS = DAY_MS;\n/** `grantTtlMs` above this (7 d) is a config error: TypeError at construction. */\nexport const MAX_GRANT_TTL_MS = 7 * DAY_MS;\n/** Consent deadline = consent anchor + this window (7 d). */\nexport const CONSENT_WINDOW_MS = 7 * DAY_MS;\n/** Silent renewal starts when remaining effective lifetime < this fraction of the effective lifetime. */\nexport const RENEW_BELOW_FRACTION = 0.25;\n","/**\n * Frozen constants of the shared replica contract (TC-858 Phase 2, see CONTRACT.md).\n *\n * Exported via `@tinycloud/sdk-services/kv/replication/contractConstants`, never from the\n * package root. Runtime grant policy imports the numeric-only `grantPolicyConstants` subpath\n * instead, so the default web bundle does not carry browser storage identifiers.\n */\nexport {\n DEFAULT_GRANT_TTL_MS,\n MAX_GRANT_TTL_MS,\n CONSENT_WINDOW_MS,\n RENEW_BELOW_FRACTION,\n} from \"./grantPolicyConstants\";\n\n/** Error codes the contract adds or gives new meaning. Values equal keys. */\nexport const ReplicaContractCode = {\n /**\n * The lifecycle gate refused the operation: the handle's session stamp or principal no longer\n * matches the gate, a purge intent covers the partition, or the logout marker is set. The error\n * carries `gate: ReplicaGateReason`. Read-through answers from the network; nothing is erased.\n */\n REPLICA_GATED: \"REPLICA_GATED\",\n /**\n * Reserved for handles whose partition was erased (purge or revocation) after they opened. The\n * gate check runs first, so a handle that is both gated and erased reports REPLICA_GATED.\n */\n RESET_REQUIRED: \"RESET_REQUIRED\",\n /** A page was refused by quota admission (`maxBytes` or the free-space floor). Nothing committed. */\n STORAGE_BUDGET: \"STORAGE_BUDGET\",\n /** The platform store refused the write (IndexedDB `QuotaExceededError`, ENOSPC). Nothing committed. */\n STORAGE_FULL: \"STORAGE_FULL\",\n /** The consent deadline has passed: no grant may be minted, renewed or served until re-consent. */\n CONSENT_EXPIRED: \"CONSENT_EXPIRED\",\n /** The installed grant's effective expiry (not necessarily its signed `exp`) has passed. */\n GRANT_EXPIRED: \"GRANT_EXPIRED\",\n} as const;\nexport type ReplicaContractCode = (typeof ReplicaContractCode)[keyof typeof ReplicaContractCode];\n\n/** Browser free-space floor in bytes (64 MiB, `navigator.storage.estimate()`); not applied on node/CLI. */\nexport const DEFAULT_MIN_FREE_BYTES = 64 * 1024 * 1024;\n\n/** The single per-origin IndexedDB database of the browser `KVReplicaStorage` adapter. */\nexport const REPLICA_KV_DATABASE = \"tinycloud-replica-kv\";\n/** Origin database schema: v2 destructively replaces v1 plaintext replica-owned stores. */\nexport const REPLICA_KV_SCHEMA_VERSION = 2;\n\n/**\n * Object-store names in {@link REPLICA_KV_DATABASE}. Every store uses out-of-line keys (no\n * `keyPath`, no autoIncrement) so the schema upgrade can create control stores without knowing\n * their record shapes. Data stores are owned by the adapter (S1); control stores by the lifecycle\n * gate (S3) and quota ledger (S5).\n */\nexport const REPLICA_KV_STORES = {\n /** Data: partition meta (opaque fences, sealed config/grants), private gate-HMAC partition id. */\n meta: \"meta\",\n /** Data: sealed KV entries, key `[privatePartitionId, accountHmac(path)]`. */\n entries: \"entries\",\n /** Data: AES-GCM content blobs, key `[privatePartitionId, accountHmac(hash)]`. */\n blobs: \"blobs\",\n /** Data: AES-sealed pending state, key private gate-HMAC `identityKeyHash`. */\n pending: \"pending\",\n /** Data: sealed owner device, private nonextractable key records and opaque install anchors. */\n device: \"device\",\n /** Control: the account gate, singleton key `\"account\"`, value `ReplicaAccountGate`. */\n gate: \"gate\",\n /** Control: partition index keyed by private `partitionId`; nonunique `byLookupId` index. */\n partitions: \"partitions\",\n /** Control: purge intents, key `ReplicaPurgeIntent.id`, value `ReplicaPurgeIntent`. */\n purgeIntents: \"purgeIntents\",\n /** Control: quota ledger, keys owned by the ledger (S5). */\n quota: \"quota\",\n} as const;\n\n/** Synchronous, cross-tab `localStorage` marker set when the deny transaction and fallback delete both failed. */\nexport const REPLICA_LOGOUT_MARKER = \"tinycloud-replica:logout-pending\";\n\n/** Origin-wide deny floor outside the deletable replica database; JSON `{ epoch, deniedAt }`. */\nexport const REPLICA_DENY_TOMBSTONE = \"tinycloud-replica:deny-tombstone\";\n\n/** BroadcastChannel name used for lifecycle nudges and peer-commit notifications. */\nexport const REPLICA_KV_CHANNEL = \"tinycloud-replica-kv\";\n\n/** Web Lock name prefix for pending-write read-modify-write; suffix is the private gate-HMAC identity id. */\nexport const REPLICA_PENDING_LOCK_PREFIX = \"tinycloud-replica-kv:pending:\";\n\n/** Node/CLI gate file name. CLI places it at `<profile>/replica-gate.json`. */\nexport const REPLICA_GATE_FILE = \"replica-gate.json\";\n","/**\n * Typed replica errors. Every failure a caller can act on has its own code;\n * hosts (CLI, worker) map codes to their own exit codes or UI states.\n */\nexport const ReplicaErrorCode = {\n /** Another process holds the sync lease. */\n BUSY: \"REPLICA_BUSY\",\n /** No replica with this name exists. */\n NOT_FOUND: \"REPLICA_NOT_FOUND\",\n /** The replica exists but its stored configuration differs from the request. */\n CONFIG_MISMATCH: \"REPLICA_CONFIG_MISMATCH\",\n /** The runtime has no supported durable store (e.g. Node < 22.13). */\n RUNTIME_UNSUPPORTED: \"RUNTIME_UNSUPPORTED\",\n /** A required open() argument is missing or malformed (e.g. `principal`). */\n INVALID_ARGUMENT: \"REPLICA_INVALID_ARGUMENT\",\n /** The local store failed (I/O, corruption, schema). */\n STORAGE_ERROR: \"STORAGE_ERROR\",\n /** The local disk is full. */\n STORAGE_FULL: \"STORAGE_FULL\",\n /** The key is outside the replica's prefix. */\n NOT_COVERED: \"NOT_COVERED\",\n /** Syncing the secrets space or the vault namespace needs an explicit opt-in. */\n SECRETS_OPT_IN_REQUIRED: \"SECRETS_OPT_IN_REQUIRED\",\n /** No grant is installed, or none covers the requested scope. */\n GRANT_MISSING: \"GRANT_MISSING\",\n /** The grant bytes are not a verifiable UCAN, or its signature is wrong. */\n GRANT_INVALID: \"GRANT_INVALID\",\n /** SIWE/CACAO and other non-UCAN grants cannot be used for a replica. */\n GRANT_FORMAT_UNSUPPORTED: \"GRANT_FORMAT_UNSUPPORTED\",\n /** The grant was issued to a different device. */\n GRANT_AUDIENCE_MISMATCH: \"GRANT_AUDIENCE_MISMATCH\",\n /** The grant lacks `tinycloud.kv/sync` or `tinycloud.kv/get` on the prefix. */\n GRANT_NOT_COVERING: \"GRANT_NOT_COVERING\",\n GRANT_NOT_YET_VALID: \"GRANT_NOT_YET_VALID\",\n GRANT_EXPIRED: \"GRANT_EXPIRED\",\n /** The configured node does not advertise whole-space KV sync support. */\n REPLICATION_ROOT_UNSUPPORTED: \"REPLICATION_ROOT_UNSUPPORTED\",\n GRANT_REVOKED: \"GRANT_REVOKED\",\n /** The node refused the invocation for a reason other than revocation. */\n GRANT_UNAUTHORIZED: \"GRANT_UNAUTHORIZED\",\n /** The node refused the retention grant. */\n RETENTION_GRANT_REFUSED: \"RETENTION_GRANT_REFUSED\",\n /** The node could not be reached. */\n NETWORK_ERROR: \"NETWORK_ERROR\",\n /** The node answered with an unexpected status. */\n NODE_ERROR: \"NODE_ERROR\",\n /** The node's answer violates the wire contract. */\n PROTOCOL_ERROR: \"PROTOCOL_ERROR\",\n /** The node asked for a fresh bootstrap (410). Handled inside sync. */\n RESET_REQUIRED: \"RESET_REQUIRED\",\n /** The feed now comes from a different node than the one pinned. */\n SOURCE_CHANGED: \"SOURCE_CHANGED\",\n /** The feed delivered a key outside the replica's prefix. */\n SCOPE_VIOLATION: \"SCOPE_VIOLATION\",\n /** Fetched bytes do not hash to the ETag the node attested for them. */\n CONTENT_MISMATCH: \"CONTENT_MISMATCH\",\n /** A stored blob no longer hashes to its name. */\n INTEGRITY_ERROR: \"INTEGRITY_ERROR\",\n /** The replica handle was closed; in-flight calls settle with this code. */\n CLOSED: \"REPLICA_CLOSED\",\n /** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */\n GATED: \"REPLICA_GATED\",\n /** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */\n STORAGE_BUDGET: \"STORAGE_BUDGET\",\n /** The consent deadline passed: the grant is not served until the user consents again. */\n CONSENT_EXPIRED: \"CONSENT_EXPIRED\",\n} as const;\n\nexport type ReplicaErrorCode = (typeof ReplicaErrorCode)[keyof typeof ReplicaErrorCode];\n\n/**\n * The package ships separate entry bundles (`.`, `./sqlite`), each with its own\n * copy of this class, so identity is a registered symbol, not `instanceof`.\n */\nconst REPLICA_ERROR = Symbol.for(\"tinycloud.replica.error\");\n\nexport class ReplicaError extends Error {\n readonly [REPLICA_ERROR] = true;\n readonly code: ReplicaErrorCode;\n readonly detail?: Record<string, unknown>;\n\n constructor(code: ReplicaErrorCode, message: string, detail?: Record<string, unknown>, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"ReplicaError\";\n this.code = code;\n if (detail !== undefined) this.detail = detail;\n }\n}\n\nexport function isReplicaError(error: unknown, code?: ReplicaErrorCode): error is ReplicaError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as { [REPLICA_ERROR]?: unknown })[REPLICA_ERROR] === true &&\n (code === undefined || (error as ReplicaError).code === code)\n );\n}\n\n/**\n * A lifecycle-gate refusal in the shared contract's shape:\n * `Error & { code: \"REPLICA_GATED\"; gate }`, with `gate` also in `detail`\n * so it survives the worker boundary.\n */\nexport function gatedError(gate: string, message: string): ReplicaError & { gate: string } {\n return Object.assign(new ReplicaError(ReplicaErrorCode.GATED, message, { gate }), { gate });\n}\n","/**\n * `SaltedHash` (CONTRACT.md §1 \"Identifiers\"): lowercase hex HMAC-SHA-256,\n * keyed with the base64url-decoded per-incarnation salt, over the UTF-8\n * material. The one implementation every salted replica id uses — browser\n * partition ids and identity-key hashes, and the gate's principal hash on\n * both platforms. WebCrypto, so it runs in pages, workers, Node and Bun; it\n * is async, so callers derive ids before opening any IndexedDB transaction.\n */\nimport type { SaltedHash } from \"@tinycloud/sdk-services/kv/replication\";\n\nfunction base64UrlBytes(text: string): Uint8Array {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new TypeError(\"The replica gate salt is not base64url.\");\n const padded = text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((text.length + 3) % 4);\n return Uint8Array.from(globalThis.atob(padded), (char) => char.charCodeAt(0));\n}\n\nexport const saltedHash: SaltedHash = async (salt, material) => {\n const key = await globalThis.crypto.subtle.importKey(\"raw\", base64UrlBytes(salt), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n const mac = new Uint8Array(await globalThis.crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(material)));\n let hex = \"\";\n for (const byte of mac) hex += byte.toString(16).padStart(2, \"0\");\n return hex;\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ReplicaAccountGate, ReplicaPurgeIntent, ReplicaGateAccess, ReplicaDenyInput, ReplicaDenyOutcome, ReplicaSessionStamp } from '@tinycloud/sdk-services/kv/replication';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Durable deny floor beside the gate, written before each deny commits. It survives loss of the
|
|
5
|
+
* gate file: a missing gate, or a gate behind this floor, fails closed instead of being recreated.
|
|
6
|
+
*/
|
|
7
|
+
declare const REPLICA_DENY_FLOOR_FILE = "replica-deny-floor.json";
|
|
8
|
+
interface NodeGateState extends ReplicaAccountGate {
|
|
9
|
+
intents: ReplicaPurgeIntent[];
|
|
10
|
+
/** Identity key hash to first-install time (ms). Cleared by every committed deny. */
|
|
11
|
+
firstInstall: Record<string, number>;
|
|
12
|
+
/** Salted identity+CID hash to immutable first-install time; survives targeted partition purge. */
|
|
13
|
+
grantInstall: Record<string, number>;
|
|
14
|
+
/** Identity key hash to generation; advanced once with a newly queued pending intent. */
|
|
15
|
+
pendingGenerations: Record<string, number>;
|
|
16
|
+
/** Deferred revoke blocks recovery from erasing these already-durable intents. */
|
|
17
|
+
eraseDeferred: boolean;
|
|
18
|
+
/** Salted original principal, retained after resume to recognize idempotent retries. */
|
|
19
|
+
deferredPrincipalHash: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Lowercase hex HMAC-SHA-256(salt, lower-cased principal DID). The only principal form stored. */
|
|
22
|
+
declare function principalHashOf(salt: string, principal: string): Promise<string>;
|
|
23
|
+
/** Salted opaque identity id, derived before a guarded data operation. */
|
|
24
|
+
declare function identityHashOf(salt: string, identityKey: string): Promise<string>;
|
|
25
|
+
/** One account identity and signed CID, without persisting a plaintext CID in gate metadata. */
|
|
26
|
+
declare function grantInstallHashOf(salt: string, identityKeyHash: string, cid: string): Promise<string>;
|
|
27
|
+
/** Control-only snapshot. In particular, a missing gate is never minted by a pending read. */
|
|
28
|
+
declare function readGateSnapshot(profileDir: string): Promise<NodeGateState | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Logout's read: the stored gate past the floor check, and whether it lags that floor (an
|
|
31
|
+
* interrupted deny). A missing gate after a deny still fails closed. Never creates the file.
|
|
32
|
+
*/
|
|
33
|
+
declare function readLogoutGate(profileDir: string): Promise<{
|
|
34
|
+
state: NodeGateState;
|
|
35
|
+
floorLagging: boolean;
|
|
36
|
+
} | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Complete an interrupted sign-out of an inactive gate: past both the gate and its floor, floor
|
|
39
|
+
* first, changing only the epoch and denial time. Anything but an inactive gate is `stamp_stale`.
|
|
40
|
+
* The caller holds the profile lock.
|
|
41
|
+
*/
|
|
42
|
+
declare function advanceInactiveGate(profileDir: string): Promise<NodeGateState>;
|
|
43
|
+
/** Call under the profile lock; caller derives both hashes before entering its data section. */
|
|
44
|
+
declare function assertGateAccess(profileDir: string, access: ReplicaGateAccess, salt: string): Promise<NodeGateState>;
|
|
45
|
+
/** Erase precedes this operation. Keeping generations makes replay and stale-store fencing durable. */
|
|
46
|
+
declare function clearGateIntent(profileDir: string, id: string): Promise<void>;
|
|
47
|
+
/** The device-level first-install anchor for one identity, or `undefined` when none is recorded. */
|
|
48
|
+
declare function readFirstInstall(profileDir: string, identityKeyHash: string): Promise<number | undefined>;
|
|
49
|
+
/** Records the first-install anchor only when absent. Returns the stored value. */
|
|
50
|
+
declare function recordFirstInstall(profileDir: string, identityKeyHash: string, atMs: number): Promise<number>;
|
|
51
|
+
/** A per-CID install floor, persisted before committing a SQLite grant install. Call under the profile lock. */
|
|
52
|
+
declare function recordGrantInstall(profileDir: string, hash: string, atMs: number): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* `establishSessionStamp` (fresh sign-in only). Rejects `purge_pending` while any intent is
|
|
55
|
+
* outstanding, then `stamp_stale` when `siweIssuedAt` (ms) is not after `deniedAt`. A null gate
|
|
56
|
+
* principal is set to the caller's (no epoch bump); the same principal returns the current stamp;
|
|
57
|
+
* another principal is `principal_mismatch`. Writes only when the principal is first set.
|
|
58
|
+
*/
|
|
59
|
+
declare function establishSessionStamp(profileDir: string, principal: string, o: {
|
|
60
|
+
siweIssuedAt: number;
|
|
61
|
+
}): Promise<ReplicaSessionStamp>;
|
|
62
|
+
/**
|
|
63
|
+
* `restoreSessionStamp`: validates the persisted stamp and never writes. An absent gate file never
|
|
64
|
+
* gets created here; with no gate, a persisted stamp is stale and no stamp is not established.
|
|
65
|
+
*/
|
|
66
|
+
declare function restoreSessionStamp(profileDir: string, principal: string, persisted: ReplicaSessionStamp | undefined): Promise<ReplicaSessionStamp>;
|
|
67
|
+
/**
|
|
68
|
+
* `deny` step 1. If `input.principal` does not match the gate, returns `stale` and writes nothing.
|
|
69
|
+
* Otherwise one atomic rewrite clears the principal, increments epoch and pending generations
|
|
70
|
+
* for new pending intents, records the deduplicated intents and clears first-install anchors.
|
|
71
|
+
* A failed write returns `unguarded`. The caller holds the (reentrant) profile lock throughout.
|
|
72
|
+
*/
|
|
73
|
+
declare function denyGate(profileDir: string, input: ReplicaDenyInput, collectIntents: (principalHash: string) => Promise<readonly ReplicaPurgeIntent[]>, precomputed?: {
|
|
74
|
+
salt: string;
|
|
75
|
+
principalHash: string;
|
|
76
|
+
}, options?: {
|
|
77
|
+
deferErase?: boolean;
|
|
78
|
+
}): Promise<ReplicaDenyOutcome>;
|
|
79
|
+
/** Local fencing token; not a bearer capability. Caller holds the profile lock. */
|
|
80
|
+
interface DeferredEraseToken {
|
|
81
|
+
deniedEpoch: number;
|
|
82
|
+
principalHash: string;
|
|
83
|
+
}
|
|
84
|
+
/** Read-only inspection of a deferred denial. A missing gate never creates one. */
|
|
85
|
+
declare function deferredGateToken(profileDir: string): Promise<DeferredEraseToken | null>;
|
|
86
|
+
/** Resume only the denial identified by the same durable epoch and original principal. */
|
|
87
|
+
declare function resumeDeferredGate(profileDir: string, token: DeferredEraseToken): Promise<boolean>;
|
|
88
|
+
|
|
89
|
+
export { type DeferredEraseToken, REPLICA_DENY_FLOOR_FILE, advanceInactiveGate, assertGateAccess, clearGateIntent, deferredGateToken, denyGate, establishSessionStamp, grantInstallHashOf, identityHashOf, principalHashOf, readFirstInstall, readGateSnapshot, readLogoutGate, recordFirstInstall, recordGrantInstall, restoreSessionStamp, resumeDeferredGate };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ReplicaAccountGate, ReplicaPurgeIntent, ReplicaGateAccess, ReplicaDenyInput, ReplicaDenyOutcome, ReplicaSessionStamp } from '@tinycloud/sdk-services/kv/replication';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Durable deny floor beside the gate, written before each deny commits. It survives loss of the
|
|
5
|
+
* gate file: a missing gate, or a gate behind this floor, fails closed instead of being recreated.
|
|
6
|
+
*/
|
|
7
|
+
declare const REPLICA_DENY_FLOOR_FILE = "replica-deny-floor.json";
|
|
8
|
+
interface NodeGateState extends ReplicaAccountGate {
|
|
9
|
+
intents: ReplicaPurgeIntent[];
|
|
10
|
+
/** Identity key hash to first-install time (ms). Cleared by every committed deny. */
|
|
11
|
+
firstInstall: Record<string, number>;
|
|
12
|
+
/** Salted identity+CID hash to immutable first-install time; survives targeted partition purge. */
|
|
13
|
+
grantInstall: Record<string, number>;
|
|
14
|
+
/** Identity key hash to generation; advanced once with a newly queued pending intent. */
|
|
15
|
+
pendingGenerations: Record<string, number>;
|
|
16
|
+
/** Deferred revoke blocks recovery from erasing these already-durable intents. */
|
|
17
|
+
eraseDeferred: boolean;
|
|
18
|
+
/** Salted original principal, retained after resume to recognize idempotent retries. */
|
|
19
|
+
deferredPrincipalHash: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Lowercase hex HMAC-SHA-256(salt, lower-cased principal DID). The only principal form stored. */
|
|
22
|
+
declare function principalHashOf(salt: string, principal: string): Promise<string>;
|
|
23
|
+
/** Salted opaque identity id, derived before a guarded data operation. */
|
|
24
|
+
declare function identityHashOf(salt: string, identityKey: string): Promise<string>;
|
|
25
|
+
/** One account identity and signed CID, without persisting a plaintext CID in gate metadata. */
|
|
26
|
+
declare function grantInstallHashOf(salt: string, identityKeyHash: string, cid: string): Promise<string>;
|
|
27
|
+
/** Control-only snapshot. In particular, a missing gate is never minted by a pending read. */
|
|
28
|
+
declare function readGateSnapshot(profileDir: string): Promise<NodeGateState | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Logout's read: the stored gate past the floor check, and whether it lags that floor (an
|
|
31
|
+
* interrupted deny). A missing gate after a deny still fails closed. Never creates the file.
|
|
32
|
+
*/
|
|
33
|
+
declare function readLogoutGate(profileDir: string): Promise<{
|
|
34
|
+
state: NodeGateState;
|
|
35
|
+
floorLagging: boolean;
|
|
36
|
+
} | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Complete an interrupted sign-out of an inactive gate: past both the gate and its floor, floor
|
|
39
|
+
* first, changing only the epoch and denial time. Anything but an inactive gate is `stamp_stale`.
|
|
40
|
+
* The caller holds the profile lock.
|
|
41
|
+
*/
|
|
42
|
+
declare function advanceInactiveGate(profileDir: string): Promise<NodeGateState>;
|
|
43
|
+
/** Call under the profile lock; caller derives both hashes before entering its data section. */
|
|
44
|
+
declare function assertGateAccess(profileDir: string, access: ReplicaGateAccess, salt: string): Promise<NodeGateState>;
|
|
45
|
+
/** Erase precedes this operation. Keeping generations makes replay and stale-store fencing durable. */
|
|
46
|
+
declare function clearGateIntent(profileDir: string, id: string): Promise<void>;
|
|
47
|
+
/** The device-level first-install anchor for one identity, or `undefined` when none is recorded. */
|
|
48
|
+
declare function readFirstInstall(profileDir: string, identityKeyHash: string): Promise<number | undefined>;
|
|
49
|
+
/** Records the first-install anchor only when absent. Returns the stored value. */
|
|
50
|
+
declare function recordFirstInstall(profileDir: string, identityKeyHash: string, atMs: number): Promise<number>;
|
|
51
|
+
/** A per-CID install floor, persisted before committing a SQLite grant install. Call under the profile lock. */
|
|
52
|
+
declare function recordGrantInstall(profileDir: string, hash: string, atMs: number): Promise<number>;
|
|
53
|
+
/**
|
|
54
|
+
* `establishSessionStamp` (fresh sign-in only). Rejects `purge_pending` while any intent is
|
|
55
|
+
* outstanding, then `stamp_stale` when `siweIssuedAt` (ms) is not after `deniedAt`. A null gate
|
|
56
|
+
* principal is set to the caller's (no epoch bump); the same principal returns the current stamp;
|
|
57
|
+
* another principal is `principal_mismatch`. Writes only when the principal is first set.
|
|
58
|
+
*/
|
|
59
|
+
declare function establishSessionStamp(profileDir: string, principal: string, o: {
|
|
60
|
+
siweIssuedAt: number;
|
|
61
|
+
}): Promise<ReplicaSessionStamp>;
|
|
62
|
+
/**
|
|
63
|
+
* `restoreSessionStamp`: validates the persisted stamp and never writes. An absent gate file never
|
|
64
|
+
* gets created here; with no gate, a persisted stamp is stale and no stamp is not established.
|
|
65
|
+
*/
|
|
66
|
+
declare function restoreSessionStamp(profileDir: string, principal: string, persisted: ReplicaSessionStamp | undefined): Promise<ReplicaSessionStamp>;
|
|
67
|
+
/**
|
|
68
|
+
* `deny` step 1. If `input.principal` does not match the gate, returns `stale` and writes nothing.
|
|
69
|
+
* Otherwise one atomic rewrite clears the principal, increments epoch and pending generations
|
|
70
|
+
* for new pending intents, records the deduplicated intents and clears first-install anchors.
|
|
71
|
+
* A failed write returns `unguarded`. The caller holds the (reentrant) profile lock throughout.
|
|
72
|
+
*/
|
|
73
|
+
declare function denyGate(profileDir: string, input: ReplicaDenyInput, collectIntents: (principalHash: string) => Promise<readonly ReplicaPurgeIntent[]>, precomputed?: {
|
|
74
|
+
salt: string;
|
|
75
|
+
principalHash: string;
|
|
76
|
+
}, options?: {
|
|
77
|
+
deferErase?: boolean;
|
|
78
|
+
}): Promise<ReplicaDenyOutcome>;
|
|
79
|
+
/** Local fencing token; not a bearer capability. Caller holds the profile lock. */
|
|
80
|
+
interface DeferredEraseToken {
|
|
81
|
+
deniedEpoch: number;
|
|
82
|
+
principalHash: string;
|
|
83
|
+
}
|
|
84
|
+
/** Read-only inspection of a deferred denial. A missing gate never creates one. */
|
|
85
|
+
declare function deferredGateToken(profileDir: string): Promise<DeferredEraseToken | null>;
|
|
86
|
+
/** Resume only the denial identified by the same durable epoch and original principal. */
|
|
87
|
+
declare function resumeDeferredGate(profileDir: string, token: DeferredEraseToken): Promise<boolean>;
|
|
88
|
+
|
|
89
|
+
export { type DeferredEraseToken, REPLICA_DENY_FLOOR_FILE, advanceInactiveGate, assertGateAccess, clearGateIntent, deferredGateToken, denyGate, establishSessionStamp, grantInstallHashOf, identityHashOf, principalHashOf, readFirstInstall, readGateSnapshot, readLogoutGate, recordFirstInstall, recordGrantInstall, restoreSessionStamp, resumeDeferredGate };
|