@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.
@@ -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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"}
@@ -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"}
@@ -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"}