@mindees/updates 0.1.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/LICENSE +31 -0
- package/README.md +125 -0
- package/dist/client.d.ts +74 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +227 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto.d.ts +42 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +63 -0
- package/dist/crypto.js.map +1 -0
- package/dist/delta.d.ts +50 -0
- package/dist/delta.d.ts.map +1 -0
- package/dist/delta.js +238 -0
- package/dist/delta.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +83 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +134 -0
- package/dist/manifest.js.map +1 -0
- package/dist/sdui.d.ts +80 -0
- package/dist/sdui.d.ts.map +1 -0
- package/dist/sdui.js +275 -0
- package/dist/sdui.js.map +1 -0
- package/dist/server.d.ts +83 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +109 -0
- package/dist/server.js.map +1 -0
- package/dist/signing.d.ts +54 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +64 -0
- package/dist/signing.js.map +1 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +55 -0
- package/dist/store.js.map +1 -0
- package/package.json +45 -0
package/dist/delta.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/delta.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure-TS differential (byte-level) delta codec for Pulse — ship only the bytes that
|
|
4
|
+
* changed between two versions of an asset, instead of the whole file.
|
|
5
|
+
*
|
|
6
|
+
* Content-addressing (the {@link "./store".UpdateStorage}) already avoids
|
|
7
|
+
* re-downloading **unchanged** files; this closes the common case of a *changed* file
|
|
8
|
+
* (e.g. a multi-MB bundle edited by a few KB). The workload is asymmetric, so the API
|
|
9
|
+
* is too:
|
|
10
|
+
*
|
|
11
|
+
* - {@link diff} runs **build/server-side** (slowness acceptable): a Rabin-Karp
|
|
12
|
+
* rolling-hash matcher emitting `COPY`/`INSERT` ops.
|
|
13
|
+
* - {@link applyDelta} is the only **on-device** piece: a tight, allocation-bounded
|
|
14
|
+
* loop with no decompressor, byte-identical on Node, browsers, and Hermes/RN.
|
|
15
|
+
*
|
|
16
|
+
* The delta is itself a SHA-256-addressable blob, and the reconstructed result is
|
|
17
|
+
* always hash-verified by the caller, so a bad or forged delta can never install
|
|
18
|
+
* unverified bytes — it just wastes CPU and falls back to a full download. See
|
|
19
|
+
* `docs/adr/0009-pulse-differential-diff.md`.
|
|
20
|
+
*
|
|
21
|
+
* Wire format: `[version:1][targetLength:varint][ op* ]`, each op a varint tag whose
|
|
22
|
+
* low bit is the kind — `COPY = varint(len*2) + varint(zigzag(offset − expected))`,
|
|
23
|
+
* `INSERT = varint(len*2 + 1) + len raw bytes`. Varints are unsigned LEB128 computed
|
|
24
|
+
* with `%`/`Math.floor` (53-bit safe, not 32-bit bit-ops).
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Compute a delta that transforms `base` into `target`. Pure, deterministic, and
|
|
30
|
+
* dependency-free. Intended for build/server-side use; the device only runs
|
|
31
|
+
* {@link applyDelta}. The result always satisfies
|
|
32
|
+
* `applyDelta(base, diff(base, target))` deep-equals `target`.
|
|
33
|
+
*/
|
|
34
|
+
declare function diff(base: Uint8Array, target: Uint8Array): Uint8Array;
|
|
35
|
+
/** Options for {@link applyDelta}. */
|
|
36
|
+
interface ApplyDeltaOptions {
|
|
37
|
+
/** Reject a reconstructed target larger than this many bytes. Default 256 MiB. */
|
|
38
|
+
readonly maxBytes?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply a {@link diff} delta to `base`, reconstructing the target. The delta is
|
|
42
|
+
* treated as **fully untrusted**: malformed input or any out-of-bounds COPY/INSERT
|
|
43
|
+
* throws {@link UpdateError} (`DELTA_INVALID`) rather than reading out of range, and
|
|
44
|
+
* the target length is capped (`maxBytes`) against a decompression-bomb. Callers must
|
|
45
|
+
* still verify the returned bytes against the expected SHA-256.
|
|
46
|
+
*/
|
|
47
|
+
declare function applyDelta(base: Uint8Array, delta: Uint8Array, options?: ApplyDeltaOptions): Uint8Array;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ApplyDeltaOptions, applyDelta, diff };
|
|
50
|
+
//# sourceMappingURL=delta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.d.ts","names":[],"sources":["../src/delta.ts"],"mappings":";;AA2HA;;;;;;;;;;;;;;AAAsE;AAkFtE;;;;AAEmB;AAUnB;;;;;;;;;;;iBA9FgB,IAAA,CAAK,IAAA,EAAM,UAAA,EAAY,MAAA,EAAQ,UAAA,GAAa,UAAA;;UAkF3C,iBAAA;EAef;EAAA,SAbS,QAAQ;AAAA;AAcN;;;;;;;AAAA,iBAJG,UAAA,CACd,IAAA,EAAM,UAAA,EACN,KAAA,EAAO,UAAA,EACP,OAAA,GAAU,iBAAA,GACT,UAAA"}
|
package/dist/delta.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { UpdateError } from "./errors.js";
|
|
2
|
+
//#region src/delta.ts
|
|
3
|
+
/**
|
|
4
|
+
* Pure-TS differential (byte-level) delta codec for Pulse — ship only the bytes that
|
|
5
|
+
* changed between two versions of an asset, instead of the whole file.
|
|
6
|
+
*
|
|
7
|
+
* Content-addressing (the {@link "./store".UpdateStorage}) already avoids
|
|
8
|
+
* re-downloading **unchanged** files; this closes the common case of a *changed* file
|
|
9
|
+
* (e.g. a multi-MB bundle edited by a few KB). The workload is asymmetric, so the API
|
|
10
|
+
* is too:
|
|
11
|
+
*
|
|
12
|
+
* - {@link diff} runs **build/server-side** (slowness acceptable): a Rabin-Karp
|
|
13
|
+
* rolling-hash matcher emitting `COPY`/`INSERT` ops.
|
|
14
|
+
* - {@link applyDelta} is the only **on-device** piece: a tight, allocation-bounded
|
|
15
|
+
* loop with no decompressor, byte-identical on Node, browsers, and Hermes/RN.
|
|
16
|
+
*
|
|
17
|
+
* The delta is itself a SHA-256-addressable blob, and the reconstructed result is
|
|
18
|
+
* always hash-verified by the caller, so a bad or forged delta can never install
|
|
19
|
+
* unverified bytes — it just wastes CPU and falls back to a full download. See
|
|
20
|
+
* `docs/adr/0009-pulse-differential-diff.md`.
|
|
21
|
+
*
|
|
22
|
+
* Wire format: `[version:1][targetLength:varint][ op* ]`, each op a varint tag whose
|
|
23
|
+
* low bit is the kind — `COPY = varint(len*2) + varint(zigzag(offset − expected))`,
|
|
24
|
+
* `INSERT = varint(len*2 + 1) + len raw bytes`. Varints are unsigned LEB128 computed
|
|
25
|
+
* with `%`/`Math.floor` (53-bit safe, not 32-bit bit-ops).
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/** Block size for the rolling-hash matcher, and therefore the minimum match length. */
|
|
30
|
+
const BLOCK_SIZE = 64;
|
|
31
|
+
/** Delta wire-format version (first byte of every delta). */
|
|
32
|
+
const FORMAT_VERSION = 1;
|
|
33
|
+
/** Default ceiling on a reconstructed target's size (anti-decompression-bomb): 256 MiB. */
|
|
34
|
+
const DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
|
|
35
|
+
/** Multiplier for the polynomial rolling hash (a prime; collisions are byte-confirmed). */
|
|
36
|
+
const HASH_BASE = 1000003;
|
|
37
|
+
/** Zig-zag map a signed integer to a non-negative one (53-bit safe). */
|
|
38
|
+
function zigzag(n) {
|
|
39
|
+
return n >= 0 ? n * 2 : n * -2 - 1;
|
|
40
|
+
}
|
|
41
|
+
/** Inverse of {@link zigzag}. */
|
|
42
|
+
function unzigzag(u) {
|
|
43
|
+
return u % 2 === 0 ? u / 2 : -(u + 1) / 2;
|
|
44
|
+
}
|
|
45
|
+
/** A growable byte buffer with LEB128 varint + raw-range writers. */
|
|
46
|
+
var ByteWriter = class {
|
|
47
|
+
buf = new Uint8Array(1024);
|
|
48
|
+
len = 0;
|
|
49
|
+
ensure(extra) {
|
|
50
|
+
if (this.len + extra <= this.buf.length) return;
|
|
51
|
+
let cap = this.buf.length;
|
|
52
|
+
while (cap < this.len + extra) cap *= 2;
|
|
53
|
+
const next = new Uint8Array(cap);
|
|
54
|
+
next.set(this.buf.subarray(0, this.len));
|
|
55
|
+
this.buf = next;
|
|
56
|
+
}
|
|
57
|
+
byte(b) {
|
|
58
|
+
this.ensure(1);
|
|
59
|
+
this.buf[this.len++] = b & 255;
|
|
60
|
+
}
|
|
61
|
+
/** Write a non-negative integer (< 2^53) as an unsigned LEB128 varint. */
|
|
62
|
+
varint(value) {
|
|
63
|
+
this.ensure(8);
|
|
64
|
+
let v = value;
|
|
65
|
+
while (v >= 128) {
|
|
66
|
+
this.buf[this.len++] = v % 128 | 128;
|
|
67
|
+
v = Math.floor(v / 128);
|
|
68
|
+
}
|
|
69
|
+
this.buf[this.len++] = v;
|
|
70
|
+
}
|
|
71
|
+
/** Append `src[start, end)` verbatim. */
|
|
72
|
+
range(src, start, end) {
|
|
73
|
+
const n = end - start;
|
|
74
|
+
this.ensure(n);
|
|
75
|
+
this.buf.set(src.subarray(start, end), this.len);
|
|
76
|
+
this.len += n;
|
|
77
|
+
}
|
|
78
|
+
finish() {
|
|
79
|
+
return this.buf.slice(0, this.len);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** Polynomial hash of `buf[start, start+len)` (mod 2^32). Leading byte has the highest weight. */
|
|
83
|
+
function hashWindow(buf, start, len) {
|
|
84
|
+
let h = 0;
|
|
85
|
+
for (let k = 0; k < len; k++) h = Math.imul(h, HASH_BASE) + buf[start + k] >>> 0;
|
|
86
|
+
return h;
|
|
87
|
+
}
|
|
88
|
+
/** `HASH_BASE ^ exp` mod 2^32. */
|
|
89
|
+
function powMod(exp) {
|
|
90
|
+
let r = 1;
|
|
91
|
+
for (let i = 0; i < exp; i++) r = Math.imul(r, HASH_BASE) >>> 0;
|
|
92
|
+
return r;
|
|
93
|
+
}
|
|
94
|
+
/** Roll a window hash forward by one byte: drop `outByte` (leading), add `inByte` (trailing). */
|
|
95
|
+
function rollHash(h, outByte, inByte, basePow) {
|
|
96
|
+
const removed = h - Math.imul(outByte, basePow) >>> 0;
|
|
97
|
+
return Math.imul(removed, HASH_BASE) + inByte >>> 0;
|
|
98
|
+
}
|
|
99
|
+
/** Whether `a[ai, ai+len)` equals `b[bi, bi+len)`. */
|
|
100
|
+
function equalRange(a, ai, b, bi, len) {
|
|
101
|
+
for (let k = 0; k < len; k++) if (a[ai + k] !== b[bi + k]) return false;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Compute a delta that transforms `base` into `target`. Pure, deterministic, and
|
|
106
|
+
* dependency-free. Intended for build/server-side use; the device only runs
|
|
107
|
+
* {@link applyDelta}. The result always satisfies
|
|
108
|
+
* `applyDelta(base, diff(base, target))` deep-equals `target`.
|
|
109
|
+
*/
|
|
110
|
+
function diff(base, target) {
|
|
111
|
+
const w = new ByteWriter();
|
|
112
|
+
w.byte(FORMAT_VERSION);
|
|
113
|
+
w.varint(target.length);
|
|
114
|
+
const B = BLOCK_SIZE;
|
|
115
|
+
if (base.length < B || target.length < B) {
|
|
116
|
+
if (target.length > 0) {
|
|
117
|
+
w.varint(target.length * 2 + 1);
|
|
118
|
+
w.range(target, 0, target.length);
|
|
119
|
+
}
|
|
120
|
+
return w.finish();
|
|
121
|
+
}
|
|
122
|
+
const index = /* @__PURE__ */ new Map();
|
|
123
|
+
for (let i = 0; i + B <= base.length; i += B) {
|
|
124
|
+
const h = hashWindow(base, i, B);
|
|
125
|
+
const list = index.get(h);
|
|
126
|
+
if (list) list.push(i);
|
|
127
|
+
else index.set(h, [i]);
|
|
128
|
+
}
|
|
129
|
+
const basePow = powMod(B - 1);
|
|
130
|
+
let expected = 0;
|
|
131
|
+
let pendingStart = 0;
|
|
132
|
+
let s = 0;
|
|
133
|
+
let h = hashWindow(target, 0, B);
|
|
134
|
+
const flushInsert = (end) => {
|
|
135
|
+
if (end > pendingStart) {
|
|
136
|
+
w.varint((end - pendingStart) * 2 + 1);
|
|
137
|
+
w.range(target, pendingStart, end);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
while (s + B <= target.length) {
|
|
141
|
+
const candidates = index.get(h);
|
|
142
|
+
let matched = false;
|
|
143
|
+
if (candidates) for (const o of candidates) {
|
|
144
|
+
if (!equalRange(target, s, base, o, B)) continue;
|
|
145
|
+
let len = B;
|
|
146
|
+
while (s + len < target.length && o + len < base.length && target[s + len] === base[o + len]) len++;
|
|
147
|
+
let ss = s;
|
|
148
|
+
let oo = o;
|
|
149
|
+
while (ss > pendingStart && oo > 0 && target[ss - 1] === base[oo - 1]) {
|
|
150
|
+
ss--;
|
|
151
|
+
oo--;
|
|
152
|
+
len++;
|
|
153
|
+
}
|
|
154
|
+
flushInsert(ss);
|
|
155
|
+
w.varint(len * 2);
|
|
156
|
+
w.varint(zigzag(oo - expected));
|
|
157
|
+
expected = oo + len;
|
|
158
|
+
s = ss + len;
|
|
159
|
+
pendingStart = s;
|
|
160
|
+
matched = true;
|
|
161
|
+
if (s + B <= target.length) h = hashWindow(target, s, B);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (matched) continue;
|
|
165
|
+
const next = s + 1;
|
|
166
|
+
if (next + B <= target.length) h = rollHash(h, target[s], target[next + B - 1], basePow);
|
|
167
|
+
s = next;
|
|
168
|
+
}
|
|
169
|
+
flushInsert(target.length);
|
|
170
|
+
return w.finish();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Apply a {@link diff} delta to `base`, reconstructing the target. The delta is
|
|
174
|
+
* treated as **fully untrusted**: malformed input or any out-of-bounds COPY/INSERT
|
|
175
|
+
* throws {@link UpdateError} (`DELTA_INVALID`) rather than reading out of range, and
|
|
176
|
+
* the target length is capped (`maxBytes`) against a decompression-bomb. Callers must
|
|
177
|
+
* still verify the returned bytes against the expected SHA-256.
|
|
178
|
+
*/
|
|
179
|
+
function applyDelta(base, delta, options) {
|
|
180
|
+
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
181
|
+
let p = 0;
|
|
182
|
+
const readByte = () => {
|
|
183
|
+
if (p >= delta.length) throw invalid("unexpected end of delta");
|
|
184
|
+
return delta[p++];
|
|
185
|
+
};
|
|
186
|
+
const readVarint = () => {
|
|
187
|
+
let result = 0;
|
|
188
|
+
let shift = 1;
|
|
189
|
+
let b;
|
|
190
|
+
do {
|
|
191
|
+
b = readByte();
|
|
192
|
+
result += (b & 127) * shift;
|
|
193
|
+
shift *= 128;
|
|
194
|
+
} while (b >= 128);
|
|
195
|
+
if (!Number.isSafeInteger(result)) throw invalid("varint too large");
|
|
196
|
+
return result;
|
|
197
|
+
};
|
|
198
|
+
const version = readByte();
|
|
199
|
+
if (version !== FORMAT_VERSION) throw invalid(`unsupported delta version ${version}`);
|
|
200
|
+
const targetLen = readVarint();
|
|
201
|
+
if (targetLen > maxBytes) throw invalid(`target length ${targetLen} exceeds max ${maxBytes}`);
|
|
202
|
+
let out;
|
|
203
|
+
try {
|
|
204
|
+
out = new Uint8Array(targetLen);
|
|
205
|
+
} catch {
|
|
206
|
+
throw invalid(`cannot allocate ${targetLen} bytes`);
|
|
207
|
+
}
|
|
208
|
+
let pos = 0;
|
|
209
|
+
let expected = 0;
|
|
210
|
+
while (p < delta.length) {
|
|
211
|
+
const tag = readVarint();
|
|
212
|
+
const kind = tag % 2;
|
|
213
|
+
const len = (tag - kind) / 2;
|
|
214
|
+
if (kind === 0) {
|
|
215
|
+
const off = expected + unzigzag(readVarint());
|
|
216
|
+
if (off < 0 || off + len > base.length) throw invalid("copy out of base bounds");
|
|
217
|
+
if (pos + len > targetLen) throw invalid("copy overflows target");
|
|
218
|
+
out.set(base.subarray(off, off + len), pos);
|
|
219
|
+
pos += len;
|
|
220
|
+
expected = off + len;
|
|
221
|
+
} else {
|
|
222
|
+
if (pos + len > targetLen) throw invalid("insert overflows target");
|
|
223
|
+
if (p + len > delta.length) throw invalid("insert exceeds delta");
|
|
224
|
+
out.set(delta.subarray(p, p + len), pos);
|
|
225
|
+
p += len;
|
|
226
|
+
pos += len;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (pos !== targetLen) throw invalid(`reconstructed ${pos} bytes, expected ${targetLen}`);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function invalid(detail) {
|
|
233
|
+
return new UpdateError("DELTA_INVALID", `invalid delta: ${detail}`);
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
export { applyDelta, diff };
|
|
237
|
+
|
|
238
|
+
//# sourceMappingURL=delta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.js","names":[],"sources":["../src/delta.ts"],"sourcesContent":["/**\n * Pure-TS differential (byte-level) delta codec for Pulse — ship only the bytes that\n * changed between two versions of an asset, instead of the whole file.\n *\n * Content-addressing (the {@link \"./store\".UpdateStorage}) already avoids\n * re-downloading **unchanged** files; this closes the common case of a *changed* file\n * (e.g. a multi-MB bundle edited by a few KB). The workload is asymmetric, so the API\n * is too:\n *\n * - {@link diff} runs **build/server-side** (slowness acceptable): a Rabin-Karp\n * rolling-hash matcher emitting `COPY`/`INSERT` ops.\n * - {@link applyDelta} is the only **on-device** piece: a tight, allocation-bounded\n * loop with no decompressor, byte-identical on Node, browsers, and Hermes/RN.\n *\n * The delta is itself a SHA-256-addressable blob, and the reconstructed result is\n * always hash-verified by the caller, so a bad or forged delta can never install\n * unverified bytes — it just wastes CPU and falls back to a full download. See\n * `docs/adr/0009-pulse-differential-diff.md`.\n *\n * Wire format: `[version:1][targetLength:varint][ op* ]`, each op a varint tag whose\n * low bit is the kind — `COPY = varint(len*2) + varint(zigzag(offset − expected))`,\n * `INSERT = varint(len*2 + 1) + len raw bytes`. Varints are unsigned LEB128 computed\n * with `%`/`Math.floor` (53-bit safe, not 32-bit bit-ops).\n *\n * @module\n */\n\nimport { UpdateError } from './errors'\n\n/** Block size for the rolling-hash matcher, and therefore the minimum match length. */\nconst BLOCK_SIZE = 64\n/** Delta wire-format version (first byte of every delta). */\nconst FORMAT_VERSION = 1\n/** Default ceiling on a reconstructed target's size (anti-decompression-bomb): 256 MiB. */\nconst DEFAULT_MAX_BYTES = 256 * 1024 * 1024\n/** Multiplier for the polynomial rolling hash (a prime; collisions are byte-confirmed). */\nconst HASH_BASE = 1000003\n\n/** Zig-zag map a signed integer to a non-negative one (53-bit safe). */\nfunction zigzag(n: number): number {\n return n >= 0 ? n * 2 : n * -2 - 1\n}\n\n/** Inverse of {@link zigzag}. */\nfunction unzigzag(u: number): number {\n return u % 2 === 0 ? u / 2 : -(u + 1) / 2\n}\n\n/** A growable byte buffer with LEB128 varint + raw-range writers. */\nclass ByteWriter {\n private buf = new Uint8Array(1024)\n private len = 0\n\n private ensure(extra: number): void {\n if (this.len + extra <= this.buf.length) return\n let cap = this.buf.length\n while (cap < this.len + extra) cap *= 2\n const next = new Uint8Array(cap)\n next.set(this.buf.subarray(0, this.len))\n this.buf = next\n }\n\n byte(b: number): void {\n this.ensure(1)\n this.buf[this.len++] = b & 0xff\n }\n\n /** Write a non-negative integer (< 2^53) as an unsigned LEB128 varint. */\n varint(value: number): void {\n this.ensure(8)\n let v = value\n while (v >= 0x80) {\n this.buf[this.len++] = (v % 0x80) | 0x80\n v = Math.floor(v / 0x80)\n }\n this.buf[this.len++] = v\n }\n\n /** Append `src[start, end)` verbatim. */\n range(src: Uint8Array, start: number, end: number): void {\n const n = end - start\n this.ensure(n)\n this.buf.set(src.subarray(start, end), this.len)\n this.len += n\n }\n\n finish(): Uint8Array {\n return this.buf.slice(0, this.len)\n }\n}\n\n/** Polynomial hash of `buf[start, start+len)` (mod 2^32). Leading byte has the highest weight. */\nfunction hashWindow(buf: Uint8Array, start: number, len: number): number {\n let h = 0\n for (let k = 0; k < len; k++) h = (Math.imul(h, HASH_BASE) + (buf[start + k] as number)) >>> 0\n return h\n}\n\n/** `HASH_BASE ^ exp` mod 2^32. */\nfunction powMod(exp: number): number {\n let r = 1\n for (let i = 0; i < exp; i++) r = Math.imul(r, HASH_BASE) >>> 0\n return r\n}\n\n/** Roll a window hash forward by one byte: drop `outByte` (leading), add `inByte` (trailing). */\nfunction rollHash(h: number, outByte: number, inByte: number, basePow: number): number {\n const removed = (h - Math.imul(outByte, basePow)) >>> 0\n return (Math.imul(removed, HASH_BASE) + inByte) >>> 0\n}\n\n/** Whether `a[ai, ai+len)` equals `b[bi, bi+len)`. */\nfunction equalRange(a: Uint8Array, ai: number, b: Uint8Array, bi: number, len: number): boolean {\n for (let k = 0; k < len; k++) if (a[ai + k] !== b[bi + k]) return false\n return true\n}\n\n/**\n * Compute a delta that transforms `base` into `target`. Pure, deterministic, and\n * dependency-free. Intended for build/server-side use; the device only runs\n * {@link applyDelta}. The result always satisfies\n * `applyDelta(base, diff(base, target))` deep-equals `target`.\n */\nexport function diff(base: Uint8Array, target: Uint8Array): Uint8Array {\n const w = new ByteWriter()\n w.byte(FORMAT_VERSION)\n w.varint(target.length)\n\n const B = BLOCK_SIZE\n // No usable base blocks (or a tiny target) → the whole target is one INSERT.\n if (base.length < B || target.length < B) {\n if (target.length > 0) {\n w.varint(target.length * 2 + 1)\n w.range(target, 0, target.length)\n }\n return w.finish()\n }\n\n // Index non-overlapping B-byte base blocks by rolling hash.\n const index = new Map<number, number[]>()\n for (let i = 0; i + B <= base.length; i += B) {\n const h = hashWindow(base, i, B)\n const list = index.get(h)\n if (list) list.push(i)\n else index.set(h, [i])\n }\n const basePow = powMod(B - 1)\n\n let expected = 0 // base offset where a contiguous next copy would begin\n let pendingStart = 0 // start of the current run of unmatched (INSERT) target bytes\n let s = 0\n let h = hashWindow(target, 0, B)\n\n const flushInsert = (end: number): void => {\n if (end > pendingStart) {\n w.varint((end - pendingStart) * 2 + 1)\n w.range(target, pendingStart, end)\n }\n }\n\n while (s + B <= target.length) {\n const candidates = index.get(h)\n let matched = false\n if (candidates) {\n for (const o of candidates) {\n if (!equalRange(target, s, base, o, B)) continue // confirm — the hash may collide\n let len = B\n while (\n s + len < target.length &&\n o + len < base.length &&\n target[s + len] === base[o + len]\n ) {\n len++\n }\n // Extend left, reclaiming bytes from the pending INSERT run.\n let ss = s\n let oo = o\n while (ss > pendingStart && oo > 0 && target[ss - 1] === base[oo - 1]) {\n ss--\n oo--\n len++\n }\n flushInsert(ss)\n w.varint(len * 2) // COPY\n w.varint(zigzag(oo - expected))\n expected = oo + len\n s = ss + len\n pendingStart = s\n matched = true\n if (s + B <= target.length) h = hashWindow(target, s, B)\n break\n }\n }\n if (matched) continue\n const next = s + 1\n if (next + B <= target.length) {\n h = rollHash(h, target[s] as number, target[next + B - 1] as number, basePow)\n }\n s = next\n }\n flushInsert(target.length)\n return w.finish()\n}\n\n/** Options for {@link applyDelta}. */\nexport interface ApplyDeltaOptions {\n /** Reject a reconstructed target larger than this many bytes. Default 256 MiB. */\n readonly maxBytes?: number\n}\n\n/**\n * Apply a {@link diff} delta to `base`, reconstructing the target. The delta is\n * treated as **fully untrusted**: malformed input or any out-of-bounds COPY/INSERT\n * throws {@link UpdateError} (`DELTA_INVALID`) rather than reading out of range, and\n * the target length is capped (`maxBytes`) against a decompression-bomb. Callers must\n * still verify the returned bytes against the expected SHA-256.\n */\nexport function applyDelta(\n base: Uint8Array,\n delta: Uint8Array,\n options?: ApplyDeltaOptions,\n): Uint8Array {\n const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES\n let p = 0\n\n const readByte = (): number => {\n if (p >= delta.length) throw invalid('unexpected end of delta')\n return delta[p++] as number\n }\n const readVarint = (): number => {\n let result = 0\n let shift = 1\n let b: number\n do {\n b = readByte()\n result += (b & 0x7f) * shift\n shift *= 0x80\n } while (b >= 0x80)\n // Reject anything outside the 53-bit safe-integer range (a forged delta could\n // otherwise decode a length the rest of the contract can't reason about).\n if (!Number.isSafeInteger(result)) throw invalid('varint too large')\n return result\n }\n\n const version = readByte()\n if (version !== FORMAT_VERSION) throw invalid(`unsupported delta version ${version}`)\n const targetLen = readVarint()\n if (targetLen > maxBytes) throw invalid(`target length ${targetLen} exceeds max ${maxBytes}`)\n\n // Allocation can still fail above the cap (a large custom maxBytes, or a tight\n // Hermes/RN heap) — surface it as DELTA_INVALID, never a raw RangeError.\n let out: Uint8Array\n try {\n out = new Uint8Array(targetLen)\n } catch {\n throw invalid(`cannot allocate ${targetLen} bytes`)\n }\n let pos = 0\n let expected = 0\n while (p < delta.length) {\n const tag = readVarint()\n const kind = tag % 2\n const len = (tag - kind) / 2\n if (kind === 0) {\n const off = expected + unzigzag(readVarint())\n if (off < 0 || off + len > base.length) throw invalid('copy out of base bounds')\n if (pos + len > targetLen) throw invalid('copy overflows target')\n out.set(base.subarray(off, off + len), pos)\n pos += len\n expected = off + len\n } else {\n if (pos + len > targetLen) throw invalid('insert overflows target')\n if (p + len > delta.length) throw invalid('insert exceeds delta')\n out.set(delta.subarray(p, p + len), pos)\n p += len\n pos += len\n }\n }\n if (pos !== targetLen) throw invalid(`reconstructed ${pos} bytes, expected ${targetLen}`)\n return out\n}\n\nfunction invalid(detail: string): UpdateError {\n return new UpdateError('DELTA_INVALID', `invalid delta: ${detail}`)\n}\n"],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Errors for `@mindees/updates` (Pulse).
|
|
4
|
+
*
|
|
5
|
+
* Every failure carries a stable {@link UpdateErrorCode} so callers can branch on
|
|
6
|
+
* the cause (e.g. distinguish a tampered bundle from a stale manifest) without
|
|
7
|
+
* string-matching messages.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/** Stable code identifying why an OTA update operation failed. */
|
|
12
|
+
type UpdateErrorCode = /** The manifest JSON is missing required fields or has the wrong shape. */'MANIFEST_MALFORMED' /** Fewer than `threshold` valid signatures from distinct trusted keys. */ | 'SIGNATURE_INVALID' /** A downloaded asset's SHA-256 does not match the manifest. */ | 'HASH_MISMATCH' /** A differential delta is malformed or out of bounds (see `delta.ts`). */ | 'DELTA_INVALID' /** `manifest.expires` is in the past (stale / freeze-attack protection). */ | 'MANIFEST_EXPIRED' /** The manifest's `runtimeVersion` does not match the app (native-incompatibility gate). */ | 'RUNTIME_MISMATCH' /** The manifest/generation is not strictly newer than what is applied (anti-downgrade). */ | 'VERSION_NOT_NEWER' /** An asset a generation needs is not present in the store. */ | 'ASSET_MISSING' /** `apply()`/`rollback()` referenced a generation id that does not exist. */ | 'GENERATION_UNKNOWN' /** `apply()` referenced a generation previously marked failed (cannot be re-activated). */ | 'GENERATION_FAILED';
|
|
13
|
+
/** An OTA update error carrying a stable {@link UpdateErrorCode}. */
|
|
14
|
+
declare class UpdateError extends Error {
|
|
15
|
+
/** Stable, machine-readable cause. */
|
|
16
|
+
readonly code: UpdateErrorCode;
|
|
17
|
+
constructor(code: UpdateErrorCode, message: string);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { UpdateError, UpdateErrorCode };
|
|
21
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;AAWA;;;;AAA2B;AAuB3B;;;;KAvBY,eAAA;;cAuBC,WAAA,SAAoB,KAAA;;WAEtB,IAAA,EAAM,eAAA;cAEH,IAAA,EAAM,eAAA,EAAiB,OAAA;AAAA"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/** An OTA update error carrying a stable {@link UpdateErrorCode}. */
|
|
3
|
+
var UpdateError = class extends Error {
|
|
4
|
+
/** Stable, machine-readable cause. */
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "UpdateError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { UpdateError };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Errors for `@mindees/updates` (Pulse).\n *\n * Every failure carries a stable {@link UpdateErrorCode} so callers can branch on\n * the cause (e.g. distinguish a tampered bundle from a stale manifest) without\n * string-matching messages.\n *\n * @module\n */\n\n/** Stable code identifying why an OTA update operation failed. */\nexport type UpdateErrorCode =\n /** The manifest JSON is missing required fields or has the wrong shape. */\n | 'MANIFEST_MALFORMED'\n /** Fewer than `threshold` valid signatures from distinct trusted keys. */\n | 'SIGNATURE_INVALID'\n /** A downloaded asset's SHA-256 does not match the manifest. */\n | 'HASH_MISMATCH'\n /** A differential delta is malformed or out of bounds (see `delta.ts`). */\n | 'DELTA_INVALID'\n /** `manifest.expires` is in the past (stale / freeze-attack protection). */\n | 'MANIFEST_EXPIRED'\n /** The manifest's `runtimeVersion` does not match the app (native-incompatibility gate). */\n | 'RUNTIME_MISMATCH'\n /** The manifest/generation is not strictly newer than what is applied (anti-downgrade). */\n | 'VERSION_NOT_NEWER'\n /** An asset a generation needs is not present in the store. */\n | 'ASSET_MISSING'\n /** `apply()`/`rollback()` referenced a generation id that does not exist. */\n | 'GENERATION_UNKNOWN'\n /** `apply()` referenced a generation previously marked failed (cannot be re-activated). */\n | 'GENERATION_FAILED'\n\n/** An OTA update error carrying a stable {@link UpdateErrorCode}. */\nexport class UpdateError extends Error {\n /** Stable, machine-readable cause. */\n readonly code: UpdateErrorCode\n\n constructor(code: UpdateErrorCode, message: string) {\n super(message)\n this.name = 'UpdateError'\n this.code = code\n }\n}\n"],"mappings":";;AAkCA,IAAa,cAAb,cAAiC,MAAM;;CAErC;CAEA,YAAY,MAAuB,SAAiB;EAClD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AssetEntry, PatchDescriptor, UpdateManifest, allAssets, canonicalManifestJson, parseManifest } from "./manifest.js";
|
|
2
|
+
import { SignatureEntry, SignedManifest, Signer, TrustedKey, VerifiedManifest, signManifest, verifySignedManifest } from "./signing.js";
|
|
3
|
+
import { GenerationMeta, GenerationStatus, UpdateState, UpdateStorage, createMemoryStorage, initialState } from "./store.js";
|
|
4
|
+
import { BootResult, UpdateCheck, UpdateClient, UpdateClientOptions, createUpdateClient } from "./client.js";
|
|
5
|
+
import { Keypair, fromHex, generateKeypair, getPublicKey, sha256Hex, sign, toHex, utf8, verify } from "./crypto.js";
|
|
6
|
+
import { ApplyDeltaOptions, applyDelta, diff } from "./delta.js";
|
|
7
|
+
import { UpdateError, UpdateErrorCode } from "./errors.js";
|
|
8
|
+
import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@mindees/core";
|
|
9
|
+
|
|
10
|
+
//#region src/index.d.ts
|
|
11
|
+
/** The npm package name. */
|
|
12
|
+
declare const name = "@mindees/updates";
|
|
13
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
14
|
+
declare const VERSION = "0.1.0";
|
|
15
|
+
/** Current maturity. See the repository `STATUS.md`. */
|
|
16
|
+
declare const maturity: Maturity;
|
|
17
|
+
/**
|
|
18
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
19
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
20
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
21
|
+
*/
|
|
22
|
+
declare const info: PackageInfo;
|
|
23
|
+
/**
|
|
24
|
+
* 🔬 Research track — not implemented. A sandboxed WASM module runtime for shipping
|
|
25
|
+
* signed, capability-secure feature modules at runtime. Throws
|
|
26
|
+
* {@link NotImplementedError}; the working path today is signed JS/asset updates
|
|
27
|
+
* (above).
|
|
28
|
+
*
|
|
29
|
+
* @experimental
|
|
30
|
+
*/
|
|
31
|
+
declare function createWasmModuleRuntime(): never;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { type ApplyDeltaOptions, type AssetEntry, type BootResult, type GenerationMeta, type GenerationStatus, type Keypair, type Maturity, NotImplementedError, type PackageInfo, type PatchDescriptor, type SignatureEntry, type SignedManifest, type Signer, type TrustedKey, type UpdateCheck, type UpdateClient, type UpdateClientOptions, UpdateError, type UpdateErrorCode, type UpdateManifest, type UpdateState, type UpdateStorage, VERSION, type VerifiedManifest, allAssets, applyDelta, canonicalManifestJson, createMemoryStorage, createUpdateClient, createWasmModuleRuntime, diff, fromHex, generateKeypair, getPublicKey, info, initialState, maturity, name, notImplemented, parseManifest, sha256Hex, sign, signManifest, toHex, utf8, verify, verifySignedManifest };
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;cAgBa,IAAA;AAGb;AAAA,cAAa,OAAA;;cAGA,QAAA,EAAU,QAAyB;AAH5B;AAGpB;;;;AAHoB,cAUP,IAAA,EAAM,WAAiE;;;AAwD7C;;;;;;iBAAvB,uBAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fromHex, generateKeypair, getPublicKey, sha256Hex, sign, toHex, utf8, verify } from "./crypto.js";
|
|
2
|
+
import { UpdateError } from "./errors.js";
|
|
3
|
+
import { applyDelta, diff } from "./delta.js";
|
|
4
|
+
import { allAssets, canonicalManifestJson, parseManifest } from "./manifest.js";
|
|
5
|
+
import { signManifest, verifySignedManifest } from "./signing.js";
|
|
6
|
+
import { createMemoryStorage, initialState } from "./store.js";
|
|
7
|
+
import { createUpdateClient } from "./client.js";
|
|
8
|
+
import { NotImplementedError, notImplemented } from "@mindees/core";
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
/** The npm package name. */
|
|
11
|
+
const name = "@mindees/updates";
|
|
12
|
+
/** The package version. All `@mindees/*` packages share one locked version line. */
|
|
13
|
+
const VERSION = "0.1.0";
|
|
14
|
+
/** Current maturity. See the repository `STATUS.md`. */
|
|
15
|
+
const maturity = "experimental";
|
|
16
|
+
/**
|
|
17
|
+
* Static identity + maturity metadata for this package. Frozen so the
|
|
18
|
+
* self-reported identity tooling introspects cannot be mutated at runtime,
|
|
19
|
+
* matching the `readonly` fields of {@link PackageInfo}.
|
|
20
|
+
*/
|
|
21
|
+
const info = Object.freeze({
|
|
22
|
+
name,
|
|
23
|
+
version: VERSION,
|
|
24
|
+
maturity
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* 🔬 Research track — not implemented. A sandboxed WASM module runtime for shipping
|
|
28
|
+
* signed, capability-secure feature modules at runtime. Throws
|
|
29
|
+
* {@link NotImplementedError}; the working path today is signed JS/asset updates
|
|
30
|
+
* (above).
|
|
31
|
+
*
|
|
32
|
+
* @experimental
|
|
33
|
+
*/
|
|
34
|
+
function createWasmModuleRuntime() {
|
|
35
|
+
throw new NotImplementedError("WASM Component-Model module runtime for OTA updates");
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { NotImplementedError, UpdateError, VERSION, allAssets, applyDelta, canonicalManifestJson, createMemoryStorage, createUpdateClient, createWasmModuleRuntime, diff, fromHex, generateKeypair, getPublicKey, info, initialState, maturity, name, notImplemented, parseManifest, sha256Hex, sign, signManifest, toHex, utf8, verify, verifySignedManifest };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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.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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//#region src/manifest.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The Pulse update manifest: a versioned description of a bundle's files, each
|
|
4
|
+
* addressed by SHA-256. One signature over the manifest's canonical bytes
|
|
5
|
+
* transitively secures every listed file.
|
|
6
|
+
*
|
|
7
|
+
* This module owns the manifest types, a **deterministic** serializer
|
|
8
|
+
* ({@link canonicalManifestJson}) used as the signing input, and a validating
|
|
9
|
+
* parser ({@link parseManifest}) for untrusted input.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/** One file in an update, addressed by content hash. */
|
|
14
|
+
interface AssetEntry {
|
|
15
|
+
/** Logical path of the file within the bundle (e.g. `"index.js"`). */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** Size in bytes. */
|
|
18
|
+
readonly size: number;
|
|
19
|
+
/** Lowercase hex SHA-256 of the file's bytes. */
|
|
20
|
+
readonly sha256: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional differential-download hint: reconstruct this asset by applying a delta
|
|
23
|
+
* to a base blob the client likely already has, instead of fetching it whole. Purely
|
|
24
|
+
* an optimization — the reconstructed bytes are still verified against
|
|
25
|
+
* {@link AssetEntry.sha256}, so a bad or forged delta can never install unverified
|
|
26
|
+
* bytes (the client falls back to a full download). See `delta.ts`.
|
|
27
|
+
*/
|
|
28
|
+
readonly patch?: PatchDescriptor;
|
|
29
|
+
}
|
|
30
|
+
/** A differential-download descriptor (see {@link AssetEntry.patch}). */
|
|
31
|
+
interface PatchDescriptor {
|
|
32
|
+
/** Lowercase hex SHA-256 of the base blob the delta applies to. */
|
|
33
|
+
readonly base: string;
|
|
34
|
+
/** The delta blob, itself a content-addressed {@link AssetEntry} (never nested again). */
|
|
35
|
+
readonly delta: AssetEntry;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A versioned description of an update's files. Because every {@link AssetEntry}
|
|
39
|
+
* carries its own SHA-256, a single signature over the manifest secures the whole
|
|
40
|
+
* bundle: verify the signature, then verify each downloaded file against its hash.
|
|
41
|
+
*/
|
|
42
|
+
interface UpdateManifest {
|
|
43
|
+
/** Manifest schema version. */
|
|
44
|
+
readonly schema: 1;
|
|
45
|
+
/** Unique id for this update (used as the generation id on the device). */
|
|
46
|
+
readonly id: string;
|
|
47
|
+
/** Monotonic version; a strictly higher value is newer. Drives rollback protection. */
|
|
48
|
+
readonly version: number;
|
|
49
|
+
/** Native-compatibility token; must match the app's runtime version exactly. */
|
|
50
|
+
readonly runtimeVersion: string;
|
|
51
|
+
/** ISO-8601 creation timestamp. */
|
|
52
|
+
readonly createdAt: string;
|
|
53
|
+
/** Optional ISO-8601 expiry; a past value makes the manifest stale (rejected). */
|
|
54
|
+
readonly expires?: string;
|
|
55
|
+
/** The entry-point asset to launch (typically the JS bundle). */
|
|
56
|
+
readonly launchAsset: AssetEntry;
|
|
57
|
+
/** Additional assets (images, fonts, …). The launch asset need not be repeated here. */
|
|
58
|
+
readonly assets: readonly AssetEntry[];
|
|
59
|
+
/** Free-form string metadata (channel, release notes, …). */
|
|
60
|
+
readonly metadata?: Readonly<Record<string, string>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Every distinct file the manifest references: the launch asset followed by the
|
|
64
|
+
* remaining assets, de-duplicated by SHA-256 (so a launch asset also listed in
|
|
65
|
+
* `assets` is only downloaded/verified once).
|
|
66
|
+
*/
|
|
67
|
+
declare function allAssets(manifest: UpdateManifest): AssetEntry[];
|
|
68
|
+
/**
|
|
69
|
+
* Serialize a manifest to **canonical** JSON: object keys sorted recursively,
|
|
70
|
+
* compact (no whitespace), `undefined` fields omitted. The same manifest always
|
|
71
|
+
* produces byte-identical output, so signing is reproducible. All numeric fields
|
|
72
|
+
* are integers (no float-formatting ambiguity).
|
|
73
|
+
*/
|
|
74
|
+
declare function canonicalManifestJson(manifest: UpdateManifest): string;
|
|
75
|
+
/**
|
|
76
|
+
* Validate that `input` is a well-formed {@link UpdateManifest} and return it
|
|
77
|
+
* typed. Throws {@link UpdateError} (`MANIFEST_MALFORMED`) for invalid JSON or any
|
|
78
|
+
* shape violation. Used on untrusted input, so validation is strict.
|
|
79
|
+
*/
|
|
80
|
+
declare function parseManifest(input: string): UpdateManifest;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { AssetEntry, PatchDescriptor, UpdateManifest, allAssets, canonicalManifestJson, parseManifest };
|
|
83
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","names":[],"sources":["../src/manifest.ts"],"mappings":";;AAeA;;;;;;;;;;AAckC;AAAA,UAdjB,UAAA;EAkBe;EAAA,SAhBrB,IAAA;EAoBiB;EAAA,SAlBjB,IAAA;EAkBA;EAAA,SAhBA,MAAA;EAgBiB;AAAA;AAQ5B;;;;;EAR4B,SARjB,KAAA,GAAQ,eAAe;AAAA;;UAIjB,eAAA;EAcN;EAAA,SAZA,IAAA;EAgBA;EAAA,SAdA,KAAA,EAAO,UAAU;AAAA;;;;;;UAQX,cAAA;EAkBK;EAAA,SAhBX,MAAA;EAgB0B;EAAA,SAd1B,EAAA;EAsBK;EAAA,SApBL,OAAA;;WAEA,cAAA;EAkByB;EAAA,SAhBzB,SAAA;EAgB0C;EAAA,SAd1C,OAAA;EAcoD;EAAA,SAZpD,WAAA,EAAa,UAAA;EA6Ba;EAAA,SA3B1B,MAAA,WAAiB,UAAA;EA2BoB;EAAA,SAzBrC,QAAA,GAAW,QAAA,CAAS,MAAA;AAAA;;;;AAkD6B;;iBA1C5C,SAAA,CAAU,QAAA,EAAU,cAAA,GAAiB,UAAU;;;;;;;iBAiB/C,qBAAA,CAAsB,QAAwB,EAAd,cAAc;;;;;;iBAyB9C,aAAA,CAAc,KAAA,WAAgB,cAAc"}
|