@mindees/updates 0.1.0 → 0.3.0
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/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -135,10 +135,12 @@ function createUpdateClient(options) {
|
|
|
135
135
|
manifest: canonicalManifestJson(manifest),
|
|
136
136
|
status: "pending"
|
|
137
137
|
};
|
|
138
|
+
const fresh = await readStateOrInit();
|
|
139
|
+
if (manifest.version <= fresh.highestVersion) throw new UpdateError("VERSION_NOT_NEWER", `manifest ${manifest.id} version ${manifest.version} is not newer than ${fresh.highestVersion}`);
|
|
138
140
|
await storage.writeState({
|
|
139
|
-
...
|
|
141
|
+
...fresh,
|
|
140
142
|
generations: {
|
|
141
|
-
...
|
|
143
|
+
...fresh.generations,
|
|
142
144
|
[generation.id]: generation
|
|
143
145
|
}
|
|
144
146
|
});
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * The Pulse update client — ties the manifest, signing, and storage together into a\n * safe OTA flow:\n *\n * - **check()** — fetch + verify the signed manifest; apply the signature, expiry,\n * runtime-compatibility, and monotonic-version gates.\n * - **download()** — fetch only the assets whose hash isn't already stored, verify\n * each blob's SHA-256, and record a `pending` generation. Never mutates the live one.\n * - **apply()** — verify the generation's assets are present, then atomically flip\n * `current`, retaining `previous` + the embedded build as fallbacks.\n * - **boot()** — on startup, roll back a generation that crash-loops before it\n * confirms itself (readiness handshake), down to the embedded build.\n * - **notifyReady()** — the app calls this once it has launched successfully.\n *\n * @module\n */\n\nimport { sha256Hex } from './crypto'\nimport { applyDelta } from './delta'\nimport { UpdateError } from './errors'\nimport {\n type AssetEntry,\n allAssets,\n canonicalManifestJson,\n type PatchDescriptor,\n parseManifest,\n} from './manifest'\nimport {\n type SignedManifest,\n type TrustedKey,\n type VerifiedManifest,\n verifySignedManifest,\n} from './signing'\nimport { type GenerationMeta, initialState, type UpdateState, type UpdateStorage } from './store'\n\n/** Options for {@link createUpdateClient}. */\nexport interface UpdateClientOptions {\n /** Where blobs + state live. */\n readonly storage: UpdateStorage\n /** Public keys this app trusts to sign updates. */\n readonly trustedKeys: readonly TrustedKey[]\n /** The app's native runtime version (gates compatibility). */\n readonly runtimeVersion: string\n /** Version baked into the binary (the rollback floor). Default 0. */\n readonly embeddedVersion?: number\n /** Valid signatures required from distinct trusted keys. Default 1. */\n readonly threshold?: number\n /** Fetch the current signed manifest from the server. */\n readonly fetchManifest: () => Promise<SignedManifest>\n /** Fetch an asset's bytes. */\n readonly fetchAsset: (asset: AssetEntry) => Promise<Uint8Array>\n /** Boots into an unconfirmed generation before rolling back. Default 1. */\n readonly maxBootAttempts?: number\n /** Clock, for expiry checks + tests. Default `() => Date.now()`. */\n readonly now?: () => number\n}\n\n/** Result of {@link UpdateClient.check}. */\nexport type UpdateCheck =\n | { readonly available: true; readonly manifest: VerifiedManifest }\n | { readonly available: false; readonly reason: 'up-to-date' | 'runtime-mismatch' }\n\n/** Result of {@link UpdateClient.boot}. */\nexport interface BootResult {\n /** True when boot fell back to the embedded build after a failed generation. */\n readonly isEmergencyLaunch: boolean\n /** The active generation id after boot (`null` = embedded). */\n readonly current: string | null\n}\n\n/** The OTA update client. */\nexport interface UpdateClient {\n /** Run the boot/recovery check (call once at startup, before rendering). */\n boot(): Promise<BootResult>\n /** Check the server for a newer, valid, compatible update. */\n check(): Promise<UpdateCheck>\n /**\n * Download a verified manifest's missing assets and record a pending generation.\n * Accepts only a {@link VerifiedManifest} (from {@link UpdateClient.check}), and\n * re-asserts the freeze / runtime / anti-downgrade gates, so it is safe even if\n * called without `check()` first.\n */\n download(manifest: VerifiedManifest): Promise<GenerationMeta>\n /** Atomically make a downloaded generation the current one (applies next launch). */\n apply(generationId: string): Promise<void>\n /** Confirm the current generation launched successfully. */\n notifyReady(): Promise<void>\n /** Manually roll back the current generation to the previous / embedded one. */\n rollback(): Promise<void>\n /** The persisted state. */\n state(): Promise<UpdateState>\n /**\n * True when the most recent {@link UpdateClient.boot} (or {@link UpdateClient.rollback})\n * fell back to the embedded build after a failed update. Reflects only the latest\n * call — it does not latch across boots.\n */\n readonly isEmergencyLaunch: boolean\n}\n\n/**\n * Compute the state after rolling back the current generation: mark it `failed`,\n * promote the previous good generation to `current` (or fall back to the embedded\n * build when there is none), and **re-arm crash-loop detection** on the rolled-to\n * generation so a *second* failure falls through to the next fallback\n * (`current → previous → embedded`). The anti-downgrade floor (`highestVersion`) is\n * preserved, so the failed version is never silently re-accepted. Shared by\n * `boot()`'s crash-loop path and the manual `rollback()`.\n */\nfunction rolledBackState(st: UpdateState): UpdateState {\n const generations = { ...st.generations }\n if (st.current) {\n const failing = generations[st.current]\n if (failing) generations[st.current] = { ...failing, status: 'failed' }\n }\n const rolledTo = st.previous && generations[st.previous] ? st.previous : null\n if (rolledTo) {\n const restored = generations[rolledTo]\n if (restored) generations[rolledTo] = { ...restored, status: 'current' }\n }\n return {\n current: rolledTo,\n // The single retained fallback was just consumed; the next failure on the\n // rolled-to generation must fall through to the embedded build.\n previous: null,\n highestVersion: st.highestVersion,\n // Re-arm: a real rolled-to generation is on probation and must re-confirm via\n // notifyReady(); the embedded build (null) is always trusted, so don't arm it.\n pendingVerification: rolledTo !== null,\n bootAttempts: 0,\n generations,\n }\n}\n\n/** Create an {@link UpdateClient}. */\nexport function createUpdateClient(options: UpdateClientOptions): UpdateClient {\n const {\n storage,\n trustedKeys,\n runtimeVersion,\n embeddedVersion = 0,\n threshold = 1,\n fetchManifest,\n fetchAsset,\n maxBootAttempts = 1,\n now = () => Date.now(),\n } = options\n\n // Fail fast on misconfiguration: a silently-broken trust/rollback setup is worse\n // than a thrown error at construction. These are programmer errors, not protocol\n // errors, so they surface as TypeError rather than UpdateError.\n if (trustedKeys.length < 1) {\n throw new TypeError('createUpdateClient: trustedKeys must contain at least one key')\n }\n // Bound the threshold by distinct *public keys*: verifySignedManifest counts unique\n // keys, so a threshold above the distinct-key count is unattainable (every check()\n // would fail). Duplicate keyId aliases for one key must not inflate the ceiling.\n const distinctKeyCount = new Set(trustedKeys.map((k) => k.publicKey)).size\n if (!Number.isInteger(threshold) || threshold < 1 || threshold > distinctKeyCount) {\n throw new TypeError(\n `createUpdateClient: threshold must be an integer in [1, ${distinctKeyCount}], got ${threshold}`,\n )\n }\n if (typeof runtimeVersion !== 'string' || runtimeVersion.length === 0) {\n throw new TypeError('createUpdateClient: runtimeVersion must be a non-empty string')\n }\n if (!Number.isInteger(embeddedVersion) || embeddedVersion < 0) {\n throw new TypeError(\n `createUpdateClient: embeddedVersion must be a non-negative integer, got ${embeddedVersion}`,\n )\n }\n if (!Number.isInteger(maxBootAttempts) || maxBootAttempts < 1) {\n throw new TypeError(\n `createUpdateClient: maxBootAttempts must be a positive integer, got ${maxBootAttempts}`,\n )\n }\n\n let emergency = false\n\n const readStateOrInit = async (): Promise<UpdateState> =>\n (await storage.readState()) ?? initialState(embeddedVersion)\n\n /** Throw if the manifest has expired (freeze protection). */\n const assertNotExpired = (manifest: VerifiedManifest): void => {\n if (manifest.expires !== undefined && Date.parse(manifest.expires) <= now()) {\n throw new UpdateError(\n 'MANIFEST_EXPIRED',\n `manifest ${manifest.id} expired at ${manifest.expires}`,\n )\n }\n }\n\n /**\n * Try to reconstruct `asset` from its delta against a stored base blob. Returns the\n * verified bytes, or `null` on any failure (missing/bad delta blob, malformed delta,\n * reconstruction hash mismatch) so the caller falls back to a full fetch. The\n * `sha256Hex(result) === asset.sha256` check here is the trust boundary.\n */\n const tryReconstruct = async (\n asset: AssetEntry,\n patch: PatchDescriptor,\n ): Promise<Uint8Array | null> => {\n try {\n const deltaBytes = await fetchAsset(patch.delta)\n if (sha256Hex(deltaBytes) !== patch.delta.sha256) return null // delta blob corrupt\n const baseBytes = await storage.readBlob(patch.base)\n // Bound the allocation by the signed expected size — an honest delta always\n // reconstructs exactly asset.size bytes, so this never rejects a valid one.\n const result = applyDelta(baseBytes, deltaBytes, { maxBytes: asset.size })\n if (sha256Hex(result) !== asset.sha256) return null // reconstruction mismatch\n return result\n } catch {\n return null\n }\n }\n\n return {\n get isEmergencyLaunch() {\n return emergency\n },\n\n state: readStateOrInit,\n\n async check(): Promise<UpdateCheck> {\n const signed = await fetchManifest()\n // Throws SIGNATURE_INVALID if < threshold valid trusted signatures.\n const manifest = verifySignedManifest(signed, trustedKeys, threshold)\n assertNotExpired(manifest)\n if (manifest.runtimeVersion !== runtimeVersion) {\n return { available: false, reason: 'runtime-mismatch' }\n }\n const st = await readStateOrInit()\n if (manifest.version <= st.highestVersion) {\n return { available: false, reason: 'up-to-date' } // never downgrade\n }\n return { available: true, manifest }\n },\n\n async download(manifest): Promise<GenerationMeta> {\n // `manifest` is signature-verified (the VerifiedManifest brand), but freshness,\n // runtime compatibility, and the anti-downgrade floor depend on *now* + current\n // state — re-assert them here so download() is safe even without a prior check().\n assertNotExpired(manifest)\n if (manifest.runtimeVersion !== runtimeVersion) {\n throw new UpdateError(\n 'RUNTIME_MISMATCH',\n `manifest ${manifest.id} targets runtime ${manifest.runtimeVersion}, app is ${runtimeVersion}`,\n )\n }\n const st = await readStateOrInit()\n if (manifest.version <= st.highestVersion) {\n throw new UpdateError(\n 'VERSION_NOT_NEWER',\n `manifest ${manifest.id} version ${manifest.version} is not newer than ${st.highestVersion}`,\n )\n }\n for (const asset of allAssets(manifest)) {\n if (await storage.hasBlob(asset.sha256)) continue // content-addressed: already have it\n // Differential path: reconstruct from a delta if we already hold the base blob.\n // The post-apply SHA-256 gate below is the trust boundary, so a bad/forged\n // delta just yields null and falls through to a full fetch.\n if (asset.patch && (await storage.hasBlob(asset.patch.base))) {\n const reconstructed = await tryReconstruct(asset, asset.patch)\n if (reconstructed) {\n await storage.writeBlob(asset.sha256, reconstructed)\n continue\n }\n }\n const bytes = await fetchAsset(asset)\n const actual = sha256Hex(bytes)\n if (actual !== asset.sha256) {\n throw new UpdateError(\n 'HASH_MISMATCH',\n `asset ${asset.path}: expected ${asset.sha256}, got ${actual}`,\n )\n }\n await storage.writeBlob(asset.sha256, bytes)\n }\n const generation: GenerationMeta = {\n id: manifest.id,\n version: manifest.version,\n manifest: canonicalManifestJson(manifest),\n status: 'pending',\n }\n await storage.writeState({\n ...st,\n generations: { ...st.generations, [generation.id]: generation },\n })\n return generation\n },\n\n async apply(generationId): Promise<void> {\n const st = await readStateOrInit()\n const generation = st.generations[generationId]\n if (!generation) throw new UpdateError('GENERATION_UNKNOWN', `no generation ${generationId}`)\n // Idempotent re-apply: the requested generation is ALREADY current. Short-circuit\n // before rewriting state — re-running the flip would reset pendingVerification and\n // bootAttempts, un-confirming a generation that may have already passed its\n // readiness handshake (and re-arming crash-loop rollback against a known-good build).\n if (generationId === st.current && generation.status === 'current') return\n // Never re-activate a generation that previously failed (e.g. crash-looped).\n if (generation.status === 'failed') {\n throw new UpdateError('GENERATION_FAILED', `generation ${generationId} previously failed`)\n }\n // Never activate something that is not strictly newer than the high-water mark\n // (anti-downgrade). download()/check()/the server all enforce a `<=` floor;\n // apply() is the final gate before code goes live, so it must too — otherwise a\n // DIFFERENT bundle signed at the SAME version could laterally replace a\n // confirmed-good current generation. Re-applying the already-current generation\n // stays idempotent (allowed).\n if (\n generation.version < st.highestVersion ||\n (generation.version === st.highestVersion && generationId !== st.current)\n ) {\n throw new UpdateError(\n 'VERSION_NOT_NEWER',\n `generation ${generationId} version ${generation.version} is not newer than ${st.highestVersion}`,\n )\n }\n // All assets must be present before we make it current.\n for (const asset of allAssets(parseManifest(generation.manifest))) {\n if (!(await storage.hasBlob(asset.sha256))) {\n throw new UpdateError(\n 'ASSET_MISSING',\n `generation ${generationId} missing asset ${asset.path}`,\n )\n }\n }\n // Keep as the rollback target only a *confirmed* outgoing generation. An\n // unconfirmed `current` (still pending) never proved itself, so we retain the\n // older known-good `previous` instead of stranding it behind a suspect build.\n const previous = st.current && !st.pendingVerification ? st.current : st.previous\n const generations = { ...st.generations }\n if (st.current) {\n const outgoing = generations[st.current]\n if (outgoing) {\n generations[st.current] = {\n ...outgoing,\n status: st.current === previous ? 'previous' : 'failed',\n }\n }\n }\n generations[generationId] = { ...generation, status: 'current' }\n await storage.writeState({\n current: generationId,\n previous,\n highestVersion: Math.max(st.highestVersion, generation.version),\n pendingVerification: true,\n bootAttempts: 0,\n generations,\n })\n },\n\n async boot(): Promise<BootResult> {\n emergency = false // reflects only *this* launch — never latches across boots\n const st = await readStateOrInit()\n if (st.current !== null && st.pendingVerification) {\n const attempts = st.bootAttempts + 1\n if (attempts > maxBootAttempts) {\n // The current generation crash-looped before confirming → roll back.\n const next = rolledBackState(st)\n await storage.writeState(next)\n emergency = next.current === null\n return { isEmergencyLaunch: emergency, current: next.current }\n }\n // Give the current generation another chance to confirm this launch.\n await storage.writeState({ ...st, bootAttempts: attempts })\n return { isEmergencyLaunch: false, current: st.current }\n }\n if ((await storage.readState()) === null) await storage.writeState(st) // persist initial\n return { isEmergencyLaunch: false, current: st.current }\n },\n\n async notifyReady(): Promise<void> {\n const st = await readStateOrInit()\n if (!st.pendingVerification && st.bootAttempts === 0) return\n await storage.writeState({ ...st, pendingVerification: false, bootAttempts: 0 })\n },\n\n async rollback(): Promise<void> {\n const st = await readStateOrInit()\n if (st.current === null) return // already on the embedded build\n const next = rolledBackState(st)\n await storage.writeState(next)\n emergency = next.current === null\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * The Pulse update client — ties the manifest, signing, and storage together into a\n * safe OTA flow:\n *\n * - **check()** — fetch + verify the signed manifest; apply the signature, expiry,\n * runtime-compatibility, and monotonic-version gates.\n * - **download()** — fetch only the assets whose hash isn't already stored, verify\n * each blob's SHA-256, and record a `pending` generation. Never mutates the live one.\n * - **apply()** — verify the generation's assets are present, then atomically flip\n * `current`, retaining `previous` + the embedded build as fallbacks.\n * - **boot()** — on startup, roll back a generation that crash-loops before it\n * confirms itself (readiness handshake), down to the embedded build.\n * - **notifyReady()** — the app calls this once it has launched successfully.\n *\n * @module\n */\n\nimport { sha256Hex } from './crypto'\nimport { applyDelta } from './delta'\nimport { UpdateError } from './errors'\nimport {\n type AssetEntry,\n allAssets,\n canonicalManifestJson,\n type PatchDescriptor,\n parseManifest,\n} from './manifest'\nimport {\n type SignedManifest,\n type TrustedKey,\n type VerifiedManifest,\n verifySignedManifest,\n} from './signing'\nimport { type GenerationMeta, initialState, type UpdateState, type UpdateStorage } from './store'\n\n/** Options for {@link createUpdateClient}. */\nexport interface UpdateClientOptions {\n /** Where blobs + state live. */\n readonly storage: UpdateStorage\n /** Public keys this app trusts to sign updates. */\n readonly trustedKeys: readonly TrustedKey[]\n /** The app's native runtime version (gates compatibility). */\n readonly runtimeVersion: string\n /** Version baked into the binary (the rollback floor). Default 0. */\n readonly embeddedVersion?: number\n /** Valid signatures required from distinct trusted keys. Default 1. */\n readonly threshold?: number\n /** Fetch the current signed manifest from the server. */\n readonly fetchManifest: () => Promise<SignedManifest>\n /** Fetch an asset's bytes. */\n readonly fetchAsset: (asset: AssetEntry) => Promise<Uint8Array>\n /** Boots into an unconfirmed generation before rolling back. Default 1. */\n readonly maxBootAttempts?: number\n /** Clock, for expiry checks + tests. Default `() => Date.now()`. */\n readonly now?: () => number\n}\n\n/** Result of {@link UpdateClient.check}. */\nexport type UpdateCheck =\n | { readonly available: true; readonly manifest: VerifiedManifest }\n | { readonly available: false; readonly reason: 'up-to-date' | 'runtime-mismatch' }\n\n/** Result of {@link UpdateClient.boot}. */\nexport interface BootResult {\n /** True when boot fell back to the embedded build after a failed generation. */\n readonly isEmergencyLaunch: boolean\n /** The active generation id after boot (`null` = embedded). */\n readonly current: string | null\n}\n\n/** The OTA update client. */\nexport interface UpdateClient {\n /** Run the boot/recovery check (call once at startup, before rendering). */\n boot(): Promise<BootResult>\n /** Check the server for a newer, valid, compatible update. */\n check(): Promise<UpdateCheck>\n /**\n * Download a verified manifest's missing assets and record a pending generation.\n * Accepts only a {@link VerifiedManifest} (from {@link UpdateClient.check}), and\n * re-asserts the freeze / runtime / anti-downgrade gates, so it is safe even if\n * called without `check()` first.\n */\n download(manifest: VerifiedManifest): Promise<GenerationMeta>\n /** Atomically make a downloaded generation the current one (applies next launch). */\n apply(generationId: string): Promise<void>\n /** Confirm the current generation launched successfully. */\n notifyReady(): Promise<void>\n /** Manually roll back the current generation to the previous / embedded one. */\n rollback(): Promise<void>\n /** The persisted state. */\n state(): Promise<UpdateState>\n /**\n * True when the most recent {@link UpdateClient.boot} (or {@link UpdateClient.rollback})\n * fell back to the embedded build after a failed update. Reflects only the latest\n * call — it does not latch across boots.\n */\n readonly isEmergencyLaunch: boolean\n}\n\n/**\n * Compute the state after rolling back the current generation: mark it `failed`,\n * promote the previous good generation to `current` (or fall back to the embedded\n * build when there is none), and **re-arm crash-loop detection** on the rolled-to\n * generation so a *second* failure falls through to the next fallback\n * (`current → previous → embedded`). The anti-downgrade floor (`highestVersion`) is\n * preserved, so the failed version is never silently re-accepted. Shared by\n * `boot()`'s crash-loop path and the manual `rollback()`.\n */\nfunction rolledBackState(st: UpdateState): UpdateState {\n const generations = { ...st.generations }\n if (st.current) {\n const failing = generations[st.current]\n if (failing) generations[st.current] = { ...failing, status: 'failed' }\n }\n const rolledTo = st.previous && generations[st.previous] ? st.previous : null\n if (rolledTo) {\n const restored = generations[rolledTo]\n if (restored) generations[rolledTo] = { ...restored, status: 'current' }\n }\n return {\n current: rolledTo,\n // The single retained fallback was just consumed; the next failure on the\n // rolled-to generation must fall through to the embedded build.\n previous: null,\n highestVersion: st.highestVersion,\n // Re-arm: a real rolled-to generation is on probation and must re-confirm via\n // notifyReady(); the embedded build (null) is always trusted, so don't arm it.\n pendingVerification: rolledTo !== null,\n bootAttempts: 0,\n generations,\n }\n}\n\n/** Create an {@link UpdateClient}. */\nexport function createUpdateClient(options: UpdateClientOptions): UpdateClient {\n const {\n storage,\n trustedKeys,\n runtimeVersion,\n embeddedVersion = 0,\n threshold = 1,\n fetchManifest,\n fetchAsset,\n maxBootAttempts = 1,\n now = () => Date.now(),\n } = options\n\n // Fail fast on misconfiguration: a silently-broken trust/rollback setup is worse\n // than a thrown error at construction. These are programmer errors, not protocol\n // errors, so they surface as TypeError rather than UpdateError.\n if (trustedKeys.length < 1) {\n throw new TypeError('createUpdateClient: trustedKeys must contain at least one key')\n }\n // Bound the threshold by distinct *public keys*: verifySignedManifest counts unique\n // keys, so a threshold above the distinct-key count is unattainable (every check()\n // would fail). Duplicate keyId aliases for one key must not inflate the ceiling.\n const distinctKeyCount = new Set(trustedKeys.map((k) => k.publicKey)).size\n if (!Number.isInteger(threshold) || threshold < 1 || threshold > distinctKeyCount) {\n throw new TypeError(\n `createUpdateClient: threshold must be an integer in [1, ${distinctKeyCount}], got ${threshold}`,\n )\n }\n if (typeof runtimeVersion !== 'string' || runtimeVersion.length === 0) {\n throw new TypeError('createUpdateClient: runtimeVersion must be a non-empty string')\n }\n if (!Number.isInteger(embeddedVersion) || embeddedVersion < 0) {\n throw new TypeError(\n `createUpdateClient: embeddedVersion must be a non-negative integer, got ${embeddedVersion}`,\n )\n }\n if (!Number.isInteger(maxBootAttempts) || maxBootAttempts < 1) {\n throw new TypeError(\n `createUpdateClient: maxBootAttempts must be a positive integer, got ${maxBootAttempts}`,\n )\n }\n\n let emergency = false\n\n const readStateOrInit = async (): Promise<UpdateState> =>\n (await storage.readState()) ?? initialState(embeddedVersion)\n\n /** Throw if the manifest has expired (freeze protection). */\n const assertNotExpired = (manifest: VerifiedManifest): void => {\n if (manifest.expires !== undefined && Date.parse(manifest.expires) <= now()) {\n throw new UpdateError(\n 'MANIFEST_EXPIRED',\n `manifest ${manifest.id} expired at ${manifest.expires}`,\n )\n }\n }\n\n /**\n * Try to reconstruct `asset` from its delta against a stored base blob. Returns the\n * verified bytes, or `null` on any failure (missing/bad delta blob, malformed delta,\n * reconstruction hash mismatch) so the caller falls back to a full fetch. The\n * `sha256Hex(result) === asset.sha256` check here is the trust boundary.\n */\n const tryReconstruct = async (\n asset: AssetEntry,\n patch: PatchDescriptor,\n ): Promise<Uint8Array | null> => {\n try {\n const deltaBytes = await fetchAsset(patch.delta)\n if (sha256Hex(deltaBytes) !== patch.delta.sha256) return null // delta blob corrupt\n const baseBytes = await storage.readBlob(patch.base)\n // Bound the allocation by the signed expected size — an honest delta always\n // reconstructs exactly asset.size bytes, so this never rejects a valid one.\n const result = applyDelta(baseBytes, deltaBytes, { maxBytes: asset.size })\n if (sha256Hex(result) !== asset.sha256) return null // reconstruction mismatch\n return result\n } catch {\n return null\n }\n }\n\n return {\n get isEmergencyLaunch() {\n return emergency\n },\n\n state: readStateOrInit,\n\n async check(): Promise<UpdateCheck> {\n const signed = await fetchManifest()\n // Throws SIGNATURE_INVALID if < threshold valid trusted signatures.\n const manifest = verifySignedManifest(signed, trustedKeys, threshold)\n assertNotExpired(manifest)\n if (manifest.runtimeVersion !== runtimeVersion) {\n return { available: false, reason: 'runtime-mismatch' }\n }\n const st = await readStateOrInit()\n if (manifest.version <= st.highestVersion) {\n return { available: false, reason: 'up-to-date' } // never downgrade\n }\n return { available: true, manifest }\n },\n\n async download(manifest): Promise<GenerationMeta> {\n // `manifest` is signature-verified (the VerifiedManifest brand), but freshness,\n // runtime compatibility, and the anti-downgrade floor depend on *now* + current\n // state — re-assert them here so download() is safe even without a prior check().\n assertNotExpired(manifest)\n if (manifest.runtimeVersion !== runtimeVersion) {\n throw new UpdateError(\n 'RUNTIME_MISMATCH',\n `manifest ${manifest.id} targets runtime ${manifest.runtimeVersion}, app is ${runtimeVersion}`,\n )\n }\n const st = await readStateOrInit()\n if (manifest.version <= st.highestVersion) {\n throw new UpdateError(\n 'VERSION_NOT_NEWER',\n `manifest ${manifest.id} version ${manifest.version} is not newer than ${st.highestVersion}`,\n )\n }\n for (const asset of allAssets(manifest)) {\n if (await storage.hasBlob(asset.sha256)) continue // content-addressed: already have it\n // Differential path: reconstruct from a delta if we already hold the base blob.\n // The post-apply SHA-256 gate below is the trust boundary, so a bad/forged\n // delta just yields null and falls through to a full fetch.\n if (asset.patch && (await storage.hasBlob(asset.patch.base))) {\n const reconstructed = await tryReconstruct(asset, asset.patch)\n if (reconstructed) {\n await storage.writeBlob(asset.sha256, reconstructed)\n continue\n }\n }\n const bytes = await fetchAsset(asset)\n const actual = sha256Hex(bytes)\n if (actual !== asset.sha256) {\n throw new UpdateError(\n 'HASH_MISMATCH',\n `asset ${asset.path}: expected ${asset.sha256}, got ${actual}`,\n )\n }\n await storage.writeBlob(asset.sha256, bytes)\n }\n const generation: GenerationMeta = {\n id: manifest.id,\n version: manifest.version,\n manifest: canonicalManifestJson(manifest),\n status: 'pending',\n }\n // Re-read state HERE, not the pre-transfer snapshot: the asset fetches above can await\n // long enough for a concurrent apply()/boot()/rollback() to advance state. Spreading the\n // stale `st` would clobber those writes — including regressing the anti-downgrade\n // `highestVersion` floor. Merge the new generation into the FRESH snapshot, and re-assert\n // the floor (another writer may have already moved past this version).\n const fresh = await readStateOrInit()\n if (manifest.version <= fresh.highestVersion) {\n throw new UpdateError(\n 'VERSION_NOT_NEWER',\n `manifest ${manifest.id} version ${manifest.version} is not newer than ${fresh.highestVersion}`,\n )\n }\n await storage.writeState({\n ...fresh,\n generations: { ...fresh.generations, [generation.id]: generation },\n })\n return generation\n },\n\n async apply(generationId): Promise<void> {\n const st = await readStateOrInit()\n const generation = st.generations[generationId]\n if (!generation) throw new UpdateError('GENERATION_UNKNOWN', `no generation ${generationId}`)\n // Idempotent re-apply: the requested generation is ALREADY current. Short-circuit\n // before rewriting state — re-running the flip would reset pendingVerification and\n // bootAttempts, un-confirming a generation that may have already passed its\n // readiness handshake (and re-arming crash-loop rollback against a known-good build).\n if (generationId === st.current && generation.status === 'current') return\n // Never re-activate a generation that previously failed (e.g. crash-looped).\n if (generation.status === 'failed') {\n throw new UpdateError('GENERATION_FAILED', `generation ${generationId} previously failed`)\n }\n // Never activate something that is not strictly newer than the high-water mark\n // (anti-downgrade). download()/check()/the server all enforce a `<=` floor;\n // apply() is the final gate before code goes live, so it must too — otherwise a\n // DIFFERENT bundle signed at the SAME version could laterally replace a\n // confirmed-good current generation. Re-applying the already-current generation\n // stays idempotent (allowed).\n if (\n generation.version < st.highestVersion ||\n (generation.version === st.highestVersion && generationId !== st.current)\n ) {\n throw new UpdateError(\n 'VERSION_NOT_NEWER',\n `generation ${generationId} version ${generation.version} is not newer than ${st.highestVersion}`,\n )\n }\n // All assets must be present before we make it current.\n for (const asset of allAssets(parseManifest(generation.manifest))) {\n if (!(await storage.hasBlob(asset.sha256))) {\n throw new UpdateError(\n 'ASSET_MISSING',\n `generation ${generationId} missing asset ${asset.path}`,\n )\n }\n }\n // Keep as the rollback target only a *confirmed* outgoing generation. An\n // unconfirmed `current` (still pending) never proved itself, so we retain the\n // older known-good `previous` instead of stranding it behind a suspect build.\n const previous = st.current && !st.pendingVerification ? st.current : st.previous\n const generations = { ...st.generations }\n if (st.current) {\n const outgoing = generations[st.current]\n if (outgoing) {\n generations[st.current] = {\n ...outgoing,\n status: st.current === previous ? 'previous' : 'failed',\n }\n }\n }\n generations[generationId] = { ...generation, status: 'current' }\n await storage.writeState({\n current: generationId,\n previous,\n highestVersion: Math.max(st.highestVersion, generation.version),\n pendingVerification: true,\n bootAttempts: 0,\n generations,\n })\n },\n\n async boot(): Promise<BootResult> {\n emergency = false // reflects only *this* launch — never latches across boots\n const st = await readStateOrInit()\n if (st.current !== null && st.pendingVerification) {\n const attempts = st.bootAttempts + 1\n if (attempts > maxBootAttempts) {\n // The current generation crash-looped before confirming → roll back.\n const next = rolledBackState(st)\n await storage.writeState(next)\n emergency = next.current === null\n return { isEmergencyLaunch: emergency, current: next.current }\n }\n // Give the current generation another chance to confirm this launch.\n await storage.writeState({ ...st, bootAttempts: attempts })\n return { isEmergencyLaunch: false, current: st.current }\n }\n if ((await storage.readState()) === null) await storage.writeState(st) // persist initial\n return { isEmergencyLaunch: false, current: st.current }\n },\n\n async notifyReady(): Promise<void> {\n const st = await readStateOrInit()\n if (!st.pendingVerification && st.bootAttempts === 0) return\n await storage.writeState({ ...st, pendingVerification: false, bootAttempts: 0 })\n },\n\n async rollback(): Promise<void> {\n const st = await readStateOrInit()\n if (st.current === null) return // already on the embedded build\n const next = rolledBackState(st)\n await storage.writeState(next)\n emergency = next.current === null\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
|
|
|
11
11
|
/** The npm package name. */
|
|
12
12
|
declare const name = "@mindees/updates";
|
|
13
13
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
-
declare const VERSION = "0.
|
|
14
|
+
declare const VERSION = "0.3.0";
|
|
15
15
|
/** Current maturity. See the repository `STATUS.md`. */
|
|
16
16
|
declare const maturity: Maturity;
|
|
17
17
|
/**
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
|
10
10
|
/** The npm package name. */
|
|
11
11
|
const name = "@mindees/updates";
|
|
12
12
|
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
13
|
-
const VERSION = "0.
|
|
13
|
+
const VERSION = "0.3.0";
|
|
14
14
|
/** Current maturity. See the repository `STATUS.md`. */
|
|
15
15
|
const maturity = "experimental";
|
|
16
16
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/updates` (Pulse) — signed OTA updates.\n *\n * Pulse ships a versioned, hash-addressed {@link UpdateManifest}, Ed25519\n * {@link signManifest signing}/{@link verifySignedManifest verification} (threshold +\n * key rotation), a content-addressed {@link UpdateStorage store}, an\n * {@link createUpdateClient update client} with atomic generations + crash-loop\n * rollback, differential bundle diffing, a reference update server, and SDUI.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/updates'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/updates` (Pulse) — signed OTA updates.\n *\n * Pulse ships a versioned, hash-addressed {@link UpdateManifest}, Ed25519\n * {@link signManifest signing}/{@link verifySignedManifest verification} (threshold +\n * key rotation), a content-addressed {@link UpdateStorage store}, an\n * {@link createUpdateClient update client} with atomic generations + crash-loop\n * rollback, differential bundle diffing, a reference update server, and SDUI.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/updates'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.3.0'\n\n/** Current maturity. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport {\n type BootResult,\n createUpdateClient,\n type UpdateCheck,\n type UpdateClient,\n type UpdateClientOptions,\n} from './client'\nexport {\n fromHex,\n generateKeypair,\n getPublicKey,\n type Keypair,\n sha256Hex,\n sign,\n toHex,\n utf8,\n verify,\n} from './crypto'\nexport { type ApplyDeltaOptions, applyDelta, diff } from './delta'\nexport { UpdateError, type UpdateErrorCode } from './errors'\nexport {\n type AssetEntry,\n allAssets,\n canonicalManifestJson,\n type PatchDescriptor,\n parseManifest,\n type UpdateManifest,\n} from './manifest'\nexport {\n type SignatureEntry,\n type SignedManifest,\n type Signer,\n signManifest,\n type TrustedKey,\n type VerifiedManifest,\n verifySignedManifest,\n} from './signing'\nexport {\n createMemoryStorage,\n type GenerationMeta,\n type GenerationStatus,\n initialState,\n type UpdateState,\n type UpdateStorage,\n} from './store'\n\n/**\n * 🔬 Research track — not implemented. A sandboxed WASM module runtime for shipping\n * signed, capability-secure feature modules at runtime. Throws\n * {@link NotImplementedError}; the working path today is signed JS/asset updates\n * (above).\n *\n * @experimental\n */\nexport function createWasmModuleRuntime(): never {\n throw new NotImplementedError('WASM Component-Model module runtime for OTA updates')\n}\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC;;;;;;;;;AAwDnF,SAAgB,0BAAiC;CAC/C,MAAM,IAAI,oBAAoB,qDAAqD;AACrF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindees/updates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MindeesNative Pulse - signed over-the-air (OTA) updates: hash-addressed manifests, Ed25519 signing, content-addressed storage, atomic generations with crash-loop rollback.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@noble/curves": "2.2.0",
|
|
35
35
|
"@noble/hashes": "2.2.0",
|
|
36
|
-
"@mindees/core": "0.
|
|
36
|
+
"@mindees/core": "0.3.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"fast-check": "4.8.0"
|