@skrr-ai/auth-core 0.1.3 → 0.1.5
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/cjs/configRoot.d.ts +50 -0
- package/dist/cjs/configRoot.js +67 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +21 -1
- package/dist/cjs/releaseKeys.d.ts +87 -0
- package/dist/cjs/releaseKeys.js +94 -0
- package/dist/cjs/releaseManifest.d.ts +161 -0
- package/dist/cjs/releaseManifest.js +235 -0
- package/dist/esm/configRoot.d.ts +50 -0
- package/dist/esm/configRoot.js +59 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/releaseKeys.d.ts +87 -0
- package/dist/esm/releaseKeys.js +91 -0
- package/dist/esm/releaseManifest.d.ts +161 -0
- package/dist/esm/releaseManifest.js +227 -0
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type ReleaseKey } from './releaseKeys.js';
|
|
2
|
+
export type { ReleaseKey } from './releaseKeys.js';
|
|
3
|
+
/**
|
|
4
|
+
* One entry in a v2 multi-signature envelope. `sig` is a base64 ed25519
|
|
5
|
+
* signature over the EXACT SAME bytes as the v1 scalar `sig` (buildArtifact/
|
|
6
|
+
* ManifestMessage) — v2 is an envelope change, not a wire-format change. The
|
|
7
|
+
* `keyId` is an ADVISORY label only: verification trial-verifies every sig
|
|
8
|
+
* against every pinned key and NEVER consults keyId, so a mislabeled keyId can
|
|
9
|
+
* neither grant nor withhold trust (this structurally defeats keyId
|
|
10
|
+
* substitution). It exists for operator diagnostics ("which key signed this?").
|
|
11
|
+
*/
|
|
12
|
+
export interface ManifestSignature {
|
|
13
|
+
keyId: string;
|
|
14
|
+
/** base64 ed25519 signature over the same bytes as the v1 `sig`. */
|
|
15
|
+
sig: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* v-delta (optional): a differential-update patch that transforms a specific
|
|
19
|
+
* PRIOR version's binary into this release's binary — a brotli-compressed
|
|
20
|
+
* fossil delta. UNSIGNED, exactly like `keyId`/`size`/`sigs` (NOT part of
|
|
21
|
+
* buildArtifactMessage/buildManifestMessage), so it never changes the signed
|
|
22
|
+
* bytes and #782's envelope invariant holds. Trust comes SOLELY from verifying
|
|
23
|
+
* the RECONSTRUCTED binary against the signed `sha256` + ed25519 gates: a
|
|
24
|
+
* tampered/garbage delta yields a non-matching binary and the daemon falls back
|
|
25
|
+
* to the full signed download. `sha256` here is TRANSPORT integrity of the patch
|
|
26
|
+
* file only — NEVER an install gate.
|
|
27
|
+
*/
|
|
28
|
+
export interface DeltaDescriptor {
|
|
29
|
+
/** Absolute public URL of the brotli-compressed fossil delta patch. */
|
|
30
|
+
url: string;
|
|
31
|
+
/** Lowercase hex sha256 of the patch file — transport integrity, not trust. */
|
|
32
|
+
sha256: string;
|
|
33
|
+
/** Size of the patch file in bytes. */
|
|
34
|
+
size: number;
|
|
35
|
+
}
|
|
36
|
+
/** One downloadable, immutable binary for a single `<platform>-<arch>` target. */
|
|
37
|
+
export interface PlatformArtifact {
|
|
38
|
+
/** Bare filename, e.g. `oversky-darwin-arm64`. */
|
|
39
|
+
file: string;
|
|
40
|
+
/** Absolute public URL under the CloudFront feed. */
|
|
41
|
+
url: string;
|
|
42
|
+
/** Lowercase hex sha256 of the binary bytes. */
|
|
43
|
+
sha256: string;
|
|
44
|
+
/** Size in bytes. */
|
|
45
|
+
size: number;
|
|
46
|
+
/** base64 ed25519 signature over buildArtifactMessage(...) — the PRIMARY key. */
|
|
47
|
+
sig: string;
|
|
48
|
+
/**
|
|
49
|
+
* v2 (optional): EVERY signature (primary + rotation key) over the same
|
|
50
|
+
* bytes as `sig`. Emitted ONLY during a key-rotation window (>1 signing key);
|
|
51
|
+
* absent in steady single-key state, so a normal manifest is byte-identical
|
|
52
|
+
* to v1. When present it is the authoritative set — see verifyWithKeysMulti.
|
|
53
|
+
*/
|
|
54
|
+
sigs?: ManifestSignature[];
|
|
55
|
+
/**
|
|
56
|
+
* v-delta (optional, UNSIGNED): differential-update patches keyed by the
|
|
57
|
+
* from-version they apply to (e.g. `"0.8.0"`). Absent in steady state. See
|
|
58
|
+
* DeltaDescriptor — the reconstructed binary is verified against `sha256`
|
|
59
|
+
* above, so deltas add NO trust surface.
|
|
60
|
+
*/
|
|
61
|
+
deltas?: Record<string, DeltaDescriptor>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The signed release manifest. `platforms` is keyed by `<platform>-<arch>`
|
|
65
|
+
* (e.g. `darwin-arm64`, `linux-x64`). The top-level `sig` covers
|
|
66
|
+
* buildManifestMessage(manifest-without-sig).
|
|
67
|
+
*/
|
|
68
|
+
export interface DaemonVersionManifest<TPolicy = unknown> {
|
|
69
|
+
schemaVersion: number;
|
|
70
|
+
version: string;
|
|
71
|
+
minimum: string;
|
|
72
|
+
forceUpdate: boolean;
|
|
73
|
+
keyId: string;
|
|
74
|
+
platforms: Record<string, PlatformArtifact>;
|
|
75
|
+
/**
|
|
76
|
+
* base64 ed25519 signature over buildManifestMessage(this-without-sig) — the
|
|
77
|
+
* PRIMARY key. Kept populated even in v2 so an OLD (v1-only) daemon still
|
|
78
|
+
* verifies against the key the lagging fleet trusts.
|
|
79
|
+
*/
|
|
80
|
+
sig: string;
|
|
81
|
+
/**
|
|
82
|
+
* v2 (optional): EVERY manifest signature (primary + rotation key). Emitted
|
|
83
|
+
* ONLY during a rotation window; when present it is authoritative — see
|
|
84
|
+
* verifyWithKeysMulti. `schemaVersion` stays a reader hint (unsigned, inert);
|
|
85
|
+
* v2 is detected by `Array.isArray(sigs)`, not by the version number.
|
|
86
|
+
*/
|
|
87
|
+
sigs?: ManifestSignature[];
|
|
88
|
+
/**
|
|
89
|
+
* Independently signed release decision envelope. It is intentionally not
|
|
90
|
+
* part of the v1 manifest message so existing daemons can ignore it; newer
|
|
91
|
+
* daemons verify this nested signature before honouring compatibility or
|
|
92
|
+
* rollout controls.
|
|
93
|
+
*/
|
|
94
|
+
policy?: TPolicy;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Canonical bytes signed for a single platform artifact. Newline-joined, fixed
|
|
98
|
+
* field order, `v1` tag. `sha256` is lowercased so the message is stable
|
|
99
|
+
* regardless of the hex casing the caller supplies.
|
|
100
|
+
*/
|
|
101
|
+
export declare function buildArtifactMessage(a: {
|
|
102
|
+
platform: string;
|
|
103
|
+
version: string;
|
|
104
|
+
sha256: string;
|
|
105
|
+
url: string;
|
|
106
|
+
}): string;
|
|
107
|
+
/**
|
|
108
|
+
* Canonical bytes signed for the whole manifest (excluding the top-level
|
|
109
|
+
* `sig`). Platform keys are sorted so the message is independent of JSON key
|
|
110
|
+
* order; each contributes `<platform>=<sha256-lower>`. `forceUpdate` is
|
|
111
|
+
* serialized strictly (`String(forceUpdate === true)` → `"true"`/`"false"`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildManifestMessage(m: {
|
|
114
|
+
version: string;
|
|
115
|
+
minimum: string;
|
|
116
|
+
forceUpdate: boolean;
|
|
117
|
+
platforms: Record<string, {
|
|
118
|
+
sha256: string;
|
|
119
|
+
}>;
|
|
120
|
+
}): string;
|
|
121
|
+
/**
|
|
122
|
+
* Discriminated verification result:
|
|
123
|
+
* - `{ ok: true }` — signature verified against a pinned key.
|
|
124
|
+
* - `{ ok: false, reason }` — keys are present but nothing verified (FAIL-CLOSED).
|
|
125
|
+
* - `{ disabled: true }` — no keys pinned; verification is intentionally off.
|
|
126
|
+
*/
|
|
127
|
+
export type VerifyResult = {
|
|
128
|
+
ok: true;
|
|
129
|
+
} | {
|
|
130
|
+
ok: false;
|
|
131
|
+
reason: string;
|
|
132
|
+
} | {
|
|
133
|
+
disabled: true;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Upper bound on signatures we will trial-verify per artifact/manifest. A
|
|
137
|
+
* rotation window needs at most 2–3 (current + next). `sigs[]` is NOT part of
|
|
138
|
+
* the signed bytes, so an attacker can pad it; this cap bounds the verify work
|
|
139
|
+
* they can force to O(MAX_SIGS × pinnedKeys) — a few hundred microseconds even
|
|
140
|
+
* at the cap. A valid signature placed beyond the cap is treated as absent.
|
|
141
|
+
*/
|
|
142
|
+
export declare const MAX_SIGS = 8;
|
|
143
|
+
/**
|
|
144
|
+
* Verify a single platform artifact's `sig` over buildArtifactMessage(...).
|
|
145
|
+
* `platform` is the `<platform>-<arch>` key; `version` is the manifest version
|
|
146
|
+
* the artifact belongs to. Defaults to the baked-in pinned keys.
|
|
147
|
+
*/
|
|
148
|
+
export declare function verifyArtifact(platform: string, version: string, artifact: PlatformArtifact, keys?: readonly ReleaseKey[]): VerifyResult;
|
|
149
|
+
/**
|
|
150
|
+
* Verify the manifest's top-level `sig` over buildManifestMessage(this).
|
|
151
|
+
* Defaults to the baked-in pinned keys.
|
|
152
|
+
*/
|
|
153
|
+
export declare function verifyManifest(manifest: DaemonVersionManifest, keys?: readonly ReleaseKey[]): VerifyResult;
|
|
154
|
+
/**
|
|
155
|
+
* Look up a delta patch that transforms `fromVersion`'s binary into this
|
|
156
|
+
* artifact's binary. Returns null when none is advertised or the descriptor is
|
|
157
|
+
* malformed — the caller then does a full download. PURE; validates shape only
|
|
158
|
+
* (the delta is UNSIGNED, so trust comes from verifying the RECONSTRUCTED binary
|
|
159
|
+
* against the signed `artifact.sha256`, not from this metadata).
|
|
160
|
+
*/
|
|
161
|
+
export declare function getDeltaFor(artifact: PlatformArtifact, fromVersion: string): DeltaDescriptor | null;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release-manifest.ts — signed daemon release manifest: types, canonical
|
|
3
|
+
* signature messages, and offline ed25519 verification.
|
|
4
|
+
*
|
|
5
|
+
* PURE: depends only on `node:crypto` (plus the sibling `release-keys.ts` for
|
|
6
|
+
* the pinned trust anchors). No Electron, no logger, no fs — so it is
|
|
7
|
+
* unit-testable in isolation and safe to import from the CI release signer.
|
|
8
|
+
*
|
|
9
|
+
* SINGLE SOURCE OF TRUTH: `buildArtifactMessage` and `buildManifestMessage`
|
|
10
|
+
* define the exact bytes that get signed. The CI signer MUST import these
|
|
11
|
+
* functions rather than reimplement them — that is what eliminates
|
|
12
|
+
* signer/verifier drift. Any change here is a wire-format change and must bump
|
|
13
|
+
* the embedded `v1` tag.
|
|
14
|
+
*
|
|
15
|
+
* Mechanics mirror `desktop/src/main/updater/policySignature.ts`: the public
|
|
16
|
+
* key is base64 DER-SPKI, verified with `crypto.verify(null, ...)` (ed25519
|
|
17
|
+
* takes no digest algorithm).
|
|
18
|
+
*
|
|
19
|
+
* WHY IT LIVES IN auth-core NOW
|
|
20
|
+
*
|
|
21
|
+
* It lived in `daemon/src/release-manifest.ts`, and `skyCodeChannels.ts` beside
|
|
22
|
+
* it recorded the reason for stopping there: "the installer, the signature
|
|
23
|
+
* verification and the promotion machinery stay in the daemon, which is the
|
|
24
|
+
* only process that performs them." That premise held exactly until the CLI
|
|
25
|
+
* had to install the daemon — `skrr daemon install` on a machine with no
|
|
26
|
+
* `skrrd` cannot ask the daemon to verify the daemon.
|
|
27
|
+
*
|
|
28
|
+
* So this is the same road that move was on, not a reversal of it. A MIRROR in
|
|
29
|
+
* the CLI is the alternative and is the wrong one, with precedent: OSK-3894
|
|
30
|
+
* shipped a mirrored harness-trust table and OSK-3897 removed it, because a
|
|
31
|
+
* mirror proves two copies agree rather than proving there is one copy. For a
|
|
32
|
+
* TRUST ROOT that argument is not stylistic — a second copy of the pinned keys
|
|
33
|
+
* is a second thing that can be edited, and the edit that matters (emptying the
|
|
34
|
+
* array) disables verification silently.
|
|
35
|
+
*
|
|
36
|
+
* `SignedReleasePolicy` did NOT move. It is the daemon's own decision envelope,
|
|
37
|
+
* so the manifest is generic over it and the daemon narrows it back.
|
|
38
|
+
*/
|
|
39
|
+
import { createPublicKey, verify as verifyEd25519 } from 'node:crypto';
|
|
40
|
+
import { OVERSKY_RELEASE_PUBLIC_KEYS } from './releaseKeys.js';
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Canonical signature messages — THE contract. Do not reformat casually.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Canonical bytes signed for a single platform artifact. Newline-joined, fixed
|
|
46
|
+
* field order, `v1` tag. `sha256` is lowercased so the message is stable
|
|
47
|
+
* regardless of the hex casing the caller supplies.
|
|
48
|
+
*/
|
|
49
|
+
export function buildArtifactMessage(a) {
|
|
50
|
+
return [
|
|
51
|
+
'oversky-daemon-artifact',
|
|
52
|
+
'v1',
|
|
53
|
+
a.platform,
|
|
54
|
+
a.version,
|
|
55
|
+
a.sha256.toLowerCase(),
|
|
56
|
+
a.url,
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Canonical bytes signed for the whole manifest (excluding the top-level
|
|
61
|
+
* `sig`). Platform keys are sorted so the message is independent of JSON key
|
|
62
|
+
* order; each contributes `<platform>=<sha256-lower>`. `forceUpdate` is
|
|
63
|
+
* serialized strictly (`String(forceUpdate === true)` → `"true"`/`"false"`).
|
|
64
|
+
*/
|
|
65
|
+
export function buildManifestMessage(m) {
|
|
66
|
+
return [
|
|
67
|
+
'oversky-daemon-manifest',
|
|
68
|
+
'v1',
|
|
69
|
+
m.version,
|
|
70
|
+
m.minimum,
|
|
71
|
+
String(m.forceUpdate === true),
|
|
72
|
+
...Object.keys(m.platforms)
|
|
73
|
+
.sort()
|
|
74
|
+
.map((p) => p + '=' + m.platforms[p].sha256.toLowerCase()),
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Try `sigB64` against every pinned key (supports key rotation: current +
|
|
79
|
+
* next). Never throws — a malformed key or signature is just a failed attempt.
|
|
80
|
+
* Empty key set ⇒ `{ disabled: true }` so the caller can warn-and-proceed
|
|
81
|
+
* (mirrors desktop `MANIFEST_PUBLIC_KEY=''`).
|
|
82
|
+
*/
|
|
83
|
+
function verifyWithKeys(message, sigB64, keys) {
|
|
84
|
+
if (keys.length === 0)
|
|
85
|
+
return { disabled: true };
|
|
86
|
+
if (!sigB64)
|
|
87
|
+
return { ok: false, reason: 'missing signature' };
|
|
88
|
+
const msg = Buffer.from(message, 'utf8');
|
|
89
|
+
const sig = Buffer.from(sigB64, 'base64');
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
try {
|
|
92
|
+
const key = createPublicKey({
|
|
93
|
+
key: Buffer.from(k.publicKeySpkiB64, 'base64'),
|
|
94
|
+
format: 'der',
|
|
95
|
+
type: 'spki',
|
|
96
|
+
});
|
|
97
|
+
if (verifyEd25519(null, msg, key, sig)) {
|
|
98
|
+
return { ok: true };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Malformed key material or verify error → try the next pinned key.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, reason: 'signature did not verify against any pinned key' };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Upper bound on signatures we will trial-verify per artifact/manifest. A
|
|
109
|
+
* rotation window needs at most 2–3 (current + next). `sigs[]` is NOT part of
|
|
110
|
+
* the signed bytes, so an attacker can pad it; this cap bounds the verify work
|
|
111
|
+
* they can force to O(MAX_SIGS × pinnedKeys) — a few hundred microseconds even
|
|
112
|
+
* at the cap. A valid signature placed beyond the cap is treated as absent.
|
|
113
|
+
*/
|
|
114
|
+
export const MAX_SIGS = 8;
|
|
115
|
+
/**
|
|
116
|
+
* v2 acceptance: `{ ok: true }` if ANY provided signature verifies against ANY
|
|
117
|
+
* pinned key. This composes the two rotation axes — verifyWithKeys already
|
|
118
|
+
* loops every pinned KEY (key rotation); this adds the every-SIGNATURE loop —
|
|
119
|
+
* so a release dual-signed by [old, new] verifies for a daemon pinning only
|
|
120
|
+
* [old], only [new], or both. That is what makes rotation zero-downtime.
|
|
121
|
+
*
|
|
122
|
+
* Posture is identical to v1:
|
|
123
|
+
* - empty pinned-key set ⇒ `{ disabled: true }` (fail-OPEN, unconfigured);
|
|
124
|
+
* - keys present but nothing verifies ⇒ `{ ok: false }` (fail-CLOSED);
|
|
125
|
+
* and it never throws (per-key errors are swallowed inside verifyWithKeys).
|
|
126
|
+
*
|
|
127
|
+
* Security: `sigs[]` is attacker-malleable (not in the signed bytes), so trust
|
|
128
|
+
* comes SOLELY from a real ed25519 verify against a pinned key. Padding with
|
|
129
|
+
* garbage is inert; stripping every entry is at worst a denial-of-update
|
|
130
|
+
* (liveness), never a forged accept. keyId is never consulted → no keyId
|
|
131
|
+
* substitution. Each sig independently binds version/minimum/forceUpdate/
|
|
132
|
+
* platform-shas, so cross-manifest splicing is blocked exactly as in v1.
|
|
133
|
+
*/
|
|
134
|
+
function verifyWithKeysMulti(message, sigs, keys) {
|
|
135
|
+
if (keys.length === 0)
|
|
136
|
+
return { disabled: true };
|
|
137
|
+
let sawSignature = false;
|
|
138
|
+
// Bound attacker-supplied work; a real rotation never needs more than a few.
|
|
139
|
+
for (const entry of sigs.slice(0, MAX_SIGS)) {
|
|
140
|
+
const sigB64 = typeof entry?.sig === 'string' ? entry.sig : '';
|
|
141
|
+
if (!sigB64)
|
|
142
|
+
continue;
|
|
143
|
+
sawSignature = true;
|
|
144
|
+
const r = verifyWithKeys(message, sigB64, keys); // keys non-empty ⇒ never {disabled}
|
|
145
|
+
if ('ok' in r && r.ok)
|
|
146
|
+
return { ok: true };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
reason: sawSignature
|
|
151
|
+
? 'no provided signature verified against any pinned key'
|
|
152
|
+
: 'missing signature',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Verify a single platform artifact's `sig` over buildArtifactMessage(...).
|
|
157
|
+
* `platform` is the `<platform>-<arch>` key; `version` is the manifest version
|
|
158
|
+
* the artifact belongs to. Defaults to the baked-in pinned keys.
|
|
159
|
+
*/
|
|
160
|
+
export function verifyArtifact(platform, version, artifact, keys = OVERSKY_RELEASE_PUBLIC_KEYS) {
|
|
161
|
+
let message;
|
|
162
|
+
try {
|
|
163
|
+
message = buildArtifactMessage({
|
|
164
|
+
platform,
|
|
165
|
+
version,
|
|
166
|
+
sha256: artifact.sha256,
|
|
167
|
+
url: artifact.url,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
// A malformed artifact (missing string fields) must fail-closed as a clean
|
|
172
|
+
// verification failure, never an uncaught throw.
|
|
173
|
+
return { ok: false, reason: `malformed artifact: ${err.message}` };
|
|
174
|
+
}
|
|
175
|
+
// v2: a present `sigs[]` is authoritative (any-of-N). Otherwise the v1 scalar
|
|
176
|
+
// path — byte-for-byte the prior behavior.
|
|
177
|
+
if (Array.isArray(artifact.sigs) && artifact.sigs.length > 0) {
|
|
178
|
+
return verifyWithKeysMulti(message, artifact.sigs, keys);
|
|
179
|
+
}
|
|
180
|
+
return verifyWithKeys(message, artifact.sig, keys);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Verify the manifest's top-level `sig` over buildManifestMessage(this).
|
|
184
|
+
* Defaults to the baked-in pinned keys.
|
|
185
|
+
*/
|
|
186
|
+
export function verifyManifest(manifest, keys = OVERSKY_RELEASE_PUBLIC_KEYS) {
|
|
187
|
+
let message;
|
|
188
|
+
try {
|
|
189
|
+
message = buildManifestMessage(manifest);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
// A malformed manifest (e.g. a platform entry missing sha256) must
|
|
193
|
+
// fail-closed as a clean verification failure, never an uncaught throw.
|
|
194
|
+
return { ok: false, reason: `malformed manifest: ${err.message}` };
|
|
195
|
+
}
|
|
196
|
+
// v2: a present `sigs[]` is authoritative (any-of-N). Otherwise the v1 scalar
|
|
197
|
+
// path — byte-for-byte the prior behavior.
|
|
198
|
+
if (Array.isArray(manifest.sigs) && manifest.sigs.length > 0) {
|
|
199
|
+
return verifyWithKeysMulti(message, manifest.sigs, keys);
|
|
200
|
+
}
|
|
201
|
+
return verifyWithKeys(message, manifest.sig, keys);
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Differential updates (delta metadata lookup) — UNSIGNED, optimization only.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
/**
|
|
207
|
+
* Look up a delta patch that transforms `fromVersion`'s binary into this
|
|
208
|
+
* artifact's binary. Returns null when none is advertised or the descriptor is
|
|
209
|
+
* malformed — the caller then does a full download. PURE; validates shape only
|
|
210
|
+
* (the delta is UNSIGNED, so trust comes from verifying the RECONSTRUCTED binary
|
|
211
|
+
* against the signed `artifact.sha256`, not from this metadata).
|
|
212
|
+
*/
|
|
213
|
+
export function getDeltaFor(artifact, fromVersion) {
|
|
214
|
+
const deltas = artifact.deltas;
|
|
215
|
+
if (!deltas || typeof deltas !== 'object')
|
|
216
|
+
return null;
|
|
217
|
+
const d = deltas[fromVersion];
|
|
218
|
+
if (!d ||
|
|
219
|
+
typeof d.url !== 'string' ||
|
|
220
|
+
typeof d.sha256 !== 'string' ||
|
|
221
|
+
typeof d.size !== 'number' ||
|
|
222
|
+
!Number.isFinite(d.size) ||
|
|
223
|
+
d.size <= 0) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return d;
|
|
227
|
+
}
|
package/package.json
CHANGED