@loreai/gateway 0.14.0 → 0.14.1
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/bin.cjs +27 -0
- package/dist/index.cjs +1042 -0
- package/dist/index.d.cts +21 -0
- package/package.json +10 -10
- package/dist/index.js +0 -50087
- package/src/auth.ts +0 -133
- package/src/batch-queue.ts +0 -575
- package/src/cache-analytics.ts +0 -344
- package/src/cli/agents.ts +0 -107
- package/src/cli/bin.ts +0 -11
- package/src/cli/help.ts +0 -55
- package/src/cli/lib/binary.ts +0 -353
- package/src/cli/lib/bspatch.ts +0 -306
- package/src/cli/lib/delta-upgrade.ts +0 -790
- package/src/cli/lib/errors.ts +0 -48
- package/src/cli/lib/ghcr.ts +0 -389
- package/src/cli/lib/patch-cache.ts +0 -342
- package/src/cli/lib/upgrade.ts +0 -454
- package/src/cli/lib/version-check.ts +0 -385
- package/src/cli/main.ts +0 -152
- package/src/cli/run.ts +0 -181
- package/src/cli/start.ts +0 -82
- package/src/cli/upgrade.ts +0 -311
- package/src/cli/version.ts +0 -22
- package/src/compaction.ts +0 -195
- package/src/config.ts +0 -199
- package/src/idle.ts +0 -240
- package/src/index.ts +0 -41
- package/src/llm-adapter.ts +0 -182
- package/src/pipeline.ts +0 -1681
- package/src/recall.ts +0 -433
- package/src/recorder.ts +0 -192
- package/src/server.ts +0 -250
- package/src/session.ts +0 -207
- package/src/stream/anthropic.ts +0 -708
- package/src/temporal-adapter.ts +0 -310
- package/src/translate/anthropic.ts +0 -469
- package/src/translate/openai.ts +0 -536
- package/src/translate/types.ts +0 -222
- package/src/worker-model.ts +0 -408
|
@@ -1,790 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Delta Upgrade Module
|
|
3
|
-
*
|
|
4
|
-
* Discovers and applies binary delta patches for CLI self-upgrades.
|
|
5
|
-
* Instead of downloading the full ~30 MB gzipped binary, downloads
|
|
6
|
-
* tiny patches (50-500 KB) and applies them to the currently installed
|
|
7
|
-
* binary using the TRDIFF10 format (zig-bsdiff with zstd compression).
|
|
8
|
-
*
|
|
9
|
-
* Supports two channels:
|
|
10
|
-
* - **Stable**: patches stored as GitHub Release assets with predictable names
|
|
11
|
-
* - **Nightly**: patches stored in GHCR with `:patch-<version>` tags
|
|
12
|
-
*
|
|
13
|
-
* Falls back to full download when:
|
|
14
|
-
* - No patch is available (404)
|
|
15
|
-
* - Chain of patches exceeds 60% of the full download size
|
|
16
|
-
* - Chain exceeds the maximum depth (10 steps)
|
|
17
|
-
* - Any error occurs during patch download or application
|
|
18
|
-
*
|
|
19
|
-
* Adapted from Sentry CLI's delta-upgrade.ts for Lore.
|
|
20
|
-
* All Sentry SDK telemetry removed — uses plain logging.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { unlinkSync } from "node:fs";
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
GITHUB_RELEASES_URL,
|
|
27
|
-
getPlatformBinaryName,
|
|
28
|
-
getUserAgent,
|
|
29
|
-
isDowngrade,
|
|
30
|
-
isNightlyVersion,
|
|
31
|
-
} from "./binary";
|
|
32
|
-
import { applyPatch } from "./bspatch";
|
|
33
|
-
import { VERSION } from "../version";
|
|
34
|
-
import {
|
|
35
|
-
downloadLayerBlob,
|
|
36
|
-
fetchManifest,
|
|
37
|
-
getAnonymousToken,
|
|
38
|
-
listTags,
|
|
39
|
-
type OciManifest,
|
|
40
|
-
} from "./ghcr";
|
|
41
|
-
import { loadCachedChain, savePatchesToCache } from "./patch-cache";
|
|
42
|
-
|
|
43
|
-
/** Maximum stable patches to chain before falling back to full download */
|
|
44
|
-
const MAX_STABLE_CHAIN_DEPTH = 10;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Maximum nightly patches to chain before falling back to full download.
|
|
48
|
-
*/
|
|
49
|
-
const MAX_NIGHTLY_CHAIN_DEPTH = 30;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Maximum ratio of total patch chain size to full download size.
|
|
53
|
-
*/
|
|
54
|
-
const SIZE_THRESHOLD_RATIO = 0.6;
|
|
55
|
-
|
|
56
|
-
const SHA256_DIGEST_PATTERN = /^sha256:([0-9a-f]+)$/i;
|
|
57
|
-
|
|
58
|
-
/** A single link in the patch chain */
|
|
59
|
-
type PatchLink = {
|
|
60
|
-
data: Uint8Array;
|
|
61
|
-
size: number;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** A resolved chain of patches from current version to target version */
|
|
65
|
-
export type PatchChain = {
|
|
66
|
-
patches: PatchLink[];
|
|
67
|
-
totalSize: number;
|
|
68
|
-
expectedSha256: string;
|
|
69
|
-
steps?: { fromVersion: string; toVersion: string }[];
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/** Result of a successful delta upgrade */
|
|
73
|
-
export type DeltaResult = {
|
|
74
|
-
sha256: string;
|
|
75
|
-
patchBytes: number;
|
|
76
|
-
chainLength: number;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Pre-flight check
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check whether delta upgrade can be attempted.
|
|
85
|
-
*/
|
|
86
|
-
export function canAttemptDelta(targetVersion: string): boolean {
|
|
87
|
-
if (VERSION === "dev" || VERSION === "0.0.0-dev") {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Cross-channel upgrades are rare one-off operations; skip delta
|
|
92
|
-
if (isNightlyVersion(VERSION) !== isNightlyVersion(targetVersion)) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (isDowngrade(VERSION, targetVersion)) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Stable channel: GitHub Releases
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
export type GitHubAsset = {
|
|
108
|
-
name: string;
|
|
109
|
-
size: number;
|
|
110
|
-
digest?: string;
|
|
111
|
-
browser_download_url: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export type GitHubRelease = {
|
|
115
|
-
tag_name: string;
|
|
116
|
-
assets: GitHubAsset[];
|
|
117
|
-
body?: string;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Fetch recent releases from GitHub, ordered newest-first.
|
|
122
|
-
*/
|
|
123
|
-
export async function fetchRecentReleases(
|
|
124
|
-
signal?: AbortSignal,
|
|
125
|
-
): Promise<GitHubRelease[]> {
|
|
126
|
-
const perPage = MAX_STABLE_CHAIN_DEPTH + 2;
|
|
127
|
-
let response: Response;
|
|
128
|
-
try {
|
|
129
|
-
response = await fetch(`${GITHUB_RELEASES_URL}?per_page=${perPage}`, {
|
|
130
|
-
headers: {
|
|
131
|
-
Accept: "application/vnd.github.v3+json",
|
|
132
|
-
"User-Agent": getUserAgent(),
|
|
133
|
-
},
|
|
134
|
-
signal,
|
|
135
|
-
});
|
|
136
|
-
} catch {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
if (!response.ok) return [];
|
|
140
|
-
return (await response.json()) as GitHubRelease[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Extract SHA-256 hex digest from a GitHub asset's digest field.
|
|
145
|
-
*/
|
|
146
|
-
export function extractSha256(asset: GitHubAsset): string | null {
|
|
147
|
-
if (!asset.digest) return null;
|
|
148
|
-
const match = SHA256_DIGEST_PATTERN.exec(asset.digest);
|
|
149
|
-
return match ? (match[1]?.toLowerCase() ?? null) : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Download a patch file from a GitHub Release asset URL.
|
|
154
|
-
*/
|
|
155
|
-
export async function downloadStablePatch(
|
|
156
|
-
url: string,
|
|
157
|
-
signal?: AbortSignal,
|
|
158
|
-
): Promise<Uint8Array | null> {
|
|
159
|
-
let response: Response;
|
|
160
|
-
try {
|
|
161
|
-
response = await fetch(url, {
|
|
162
|
-
headers: { "User-Agent": getUserAgent() },
|
|
163
|
-
signal,
|
|
164
|
-
});
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
if (!response.ok) return null;
|
|
169
|
-
return new Uint8Array(await response.arrayBuffer());
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Extract the target binary SHA-256 from a GitHub Release.
|
|
174
|
-
*/
|
|
175
|
-
export function getStableTargetSha256(
|
|
176
|
-
release: GitHubRelease,
|
|
177
|
-
binaryName: string,
|
|
178
|
-
): string | null {
|
|
179
|
-
const binaryAsset = release.assets.find((a) => a.name === binaryName);
|
|
180
|
-
if (!binaryAsset) return null;
|
|
181
|
-
return extractSha256(binaryAsset);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export type ExtractStableChainOpts = {
|
|
185
|
-
releases: GitHubRelease[];
|
|
186
|
-
currentVersion: string;
|
|
187
|
-
targetVersion: string;
|
|
188
|
-
binaryName: string;
|
|
189
|
-
fullGzSize: number;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export type StableChainInfo = {
|
|
193
|
-
patchUrls: string[];
|
|
194
|
-
expectedSha256: string;
|
|
195
|
-
steps: { fromVersion: string; toVersion: string }[];
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Extract the chain of patch URLs from an already-fetched release list.
|
|
200
|
-
* Pure computation — no HTTP calls.
|
|
201
|
-
*/
|
|
202
|
-
export function extractStableChain(
|
|
203
|
-
opts: ExtractStableChainOpts,
|
|
204
|
-
): StableChainInfo | null {
|
|
205
|
-
const { releases, currentVersion, targetVersion, binaryName, fullGzSize } =
|
|
206
|
-
opts;
|
|
207
|
-
const patchAssetName = `${binaryName}.patch`;
|
|
208
|
-
|
|
209
|
-
const targetIdx = releases.findIndex((r) => r.tag_name === targetVersion);
|
|
210
|
-
const currentIdx = releases.findIndex((r) => r.tag_name === currentVersion);
|
|
211
|
-
if (targetIdx === -1 || currentIdx === -1 || targetIdx >= currentIdx) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const chainReleases = releases.slice(targetIdx, currentIdx);
|
|
216
|
-
if (chainReleases.length > MAX_STABLE_CHAIN_DEPTH) {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const targetRelease = chainReleases[0];
|
|
221
|
-
if (!targetRelease) return null;
|
|
222
|
-
const expectedSha256 =
|
|
223
|
-
getStableTargetSha256(targetRelease, binaryName) ?? "";
|
|
224
|
-
if (!expectedSha256) return null;
|
|
225
|
-
|
|
226
|
-
const patchUrls: string[] = [];
|
|
227
|
-
let totalSize = 0;
|
|
228
|
-
for (const release of chainReleases) {
|
|
229
|
-
const patchAsset = release.assets.find((a) => a.name === patchAssetName);
|
|
230
|
-
if (!patchAsset) return null;
|
|
231
|
-
patchUrls.push(patchAsset.browser_download_url);
|
|
232
|
-
totalSize += patchAsset.size;
|
|
233
|
-
if (totalSize > fullGzSize * SIZE_THRESHOLD_RATIO) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Reverse to get apply order: oldest patch first
|
|
239
|
-
patchUrls.reverse();
|
|
240
|
-
|
|
241
|
-
const reversedReleases = [...chainReleases].reverse();
|
|
242
|
-
const steps: { fromVersion: string; toVersion: string }[] = [];
|
|
243
|
-
let prevVersion = currentVersion;
|
|
244
|
-
for (const release of reversedReleases) {
|
|
245
|
-
steps.push({ fromVersion: prevVersion, toVersion: release.tag_name });
|
|
246
|
-
prevVersion = release.tag_name;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return { patchUrls, expectedSha256, steps };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Resolve a chain of stable patches from current to target version.
|
|
254
|
-
*/
|
|
255
|
-
export async function resolveStableChain(
|
|
256
|
-
currentVersion: string,
|
|
257
|
-
targetVersion: string,
|
|
258
|
-
signal?: AbortSignal,
|
|
259
|
-
): Promise<PatchChain | null> {
|
|
260
|
-
const binaryName = getPlatformBinaryName();
|
|
261
|
-
const releases = await fetchRecentReleases(signal);
|
|
262
|
-
|
|
263
|
-
const targetRelease = releases.find((r) => r.tag_name === targetVersion);
|
|
264
|
-
if (!targetRelease) return null;
|
|
265
|
-
const gzAsset = targetRelease.assets.find(
|
|
266
|
-
(a) => a.name === `${binaryName}.gz`,
|
|
267
|
-
);
|
|
268
|
-
if (!gzAsset) return null;
|
|
269
|
-
|
|
270
|
-
const chainInfo = extractStableChain({
|
|
271
|
-
releases,
|
|
272
|
-
currentVersion,
|
|
273
|
-
targetVersion,
|
|
274
|
-
binaryName,
|
|
275
|
-
fullGzSize: gzAsset.size,
|
|
276
|
-
});
|
|
277
|
-
if (!chainInfo) return null;
|
|
278
|
-
|
|
279
|
-
// Parallel patch download
|
|
280
|
-
const downloadResults = await Promise.all(
|
|
281
|
-
chainInfo.patchUrls.map((url) => downloadStablePatch(url, signal)),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const patches: PatchLink[] = [];
|
|
285
|
-
let totalSize = 0;
|
|
286
|
-
for (const data of downloadResults) {
|
|
287
|
-
if (!data) return null;
|
|
288
|
-
patches.push({ data, size: data.byteLength });
|
|
289
|
-
totalSize += data.byteLength;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
patches,
|
|
294
|
-
totalSize,
|
|
295
|
-
expectedSha256: chainInfo.expectedSha256,
|
|
296
|
-
steps: chainInfo.steps,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Nightly channel: GHCR
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
export function getPatchFromVersion(manifest: OciManifest): string | null {
|
|
305
|
-
return manifest.annotations?.["from-version"] ?? null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function getPatchTargetSha256(
|
|
309
|
-
manifest: OciManifest,
|
|
310
|
-
binaryName: string,
|
|
311
|
-
): string | null {
|
|
312
|
-
return manifest.annotations?.[`sha256-${binaryName}`] ?? null;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export const PATCH_TAG_PREFIX = "patch-";
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Filter patch tags to only those in the upgrade chain, sorted in apply order.
|
|
319
|
-
*/
|
|
320
|
-
export function filterAndSortChainTags(
|
|
321
|
-
allTags: string[],
|
|
322
|
-
currentVersion: string,
|
|
323
|
-
targetVersion: string,
|
|
324
|
-
): string[] {
|
|
325
|
-
const chainTags: { tag: string; version: string }[] = [];
|
|
326
|
-
|
|
327
|
-
for (const tag of allTags) {
|
|
328
|
-
const version = tag.slice(PATCH_TAG_PREFIX.length);
|
|
329
|
-
if (
|
|
330
|
-
Bun.semver.order(version, currentVersion) === 1 &&
|
|
331
|
-
Bun.semver.order(version, targetVersion) !== 1
|
|
332
|
-
) {
|
|
333
|
-
chainTags.push({ tag, version });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
chainTags.sort((a, b) => Bun.semver.order(a.version, b.version));
|
|
338
|
-
return chainTags.map((t) => t.tag);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
type NightlyChainValidation = {
|
|
342
|
-
digests: string[];
|
|
343
|
-
totalSize: number;
|
|
344
|
-
expectedSha256: string;
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
type ValidateChainOpts = {
|
|
348
|
-
manifests: OciManifest[];
|
|
349
|
-
chainTags: string[];
|
|
350
|
-
currentVersion: string;
|
|
351
|
-
targetVersion: string;
|
|
352
|
-
patchLayerName: string;
|
|
353
|
-
binaryName: string;
|
|
354
|
-
fullGzSize: number;
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
type ChainStepResult =
|
|
358
|
-
| { ok: true; digest: string; size: number }
|
|
359
|
-
| { ok: false };
|
|
360
|
-
|
|
361
|
-
export function validateChainStep(
|
|
362
|
-
manifest: OciManifest,
|
|
363
|
-
opts: { expectedFrom: string; patchLayerName: string; sizeLimit: number },
|
|
364
|
-
): ChainStepResult {
|
|
365
|
-
const fromVersion = getPatchFromVersion(manifest);
|
|
366
|
-
if (fromVersion !== opts.expectedFrom) {
|
|
367
|
-
return { ok: false };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const layer = manifest.layers.find((l) => {
|
|
371
|
-
const title = l.annotations?.["org.opencontainers.image.title"];
|
|
372
|
-
return title === opts.patchLayerName;
|
|
373
|
-
});
|
|
374
|
-
if (!layer) return { ok: false };
|
|
375
|
-
if (layer.size > opts.sizeLimit) return { ok: false };
|
|
376
|
-
|
|
377
|
-
return { ok: true, digest: layer.digest, size: layer.size };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function validateNightlyChain(
|
|
381
|
-
opts: ValidateChainOpts,
|
|
382
|
-
): NightlyChainValidation | null {
|
|
383
|
-
const {
|
|
384
|
-
manifests,
|
|
385
|
-
chainTags,
|
|
386
|
-
currentVersion,
|
|
387
|
-
targetVersion,
|
|
388
|
-
patchLayerName,
|
|
389
|
-
binaryName,
|
|
390
|
-
fullGzSize,
|
|
391
|
-
} = opts;
|
|
392
|
-
const digests: string[] = [];
|
|
393
|
-
let totalSize = 0;
|
|
394
|
-
let prevVersion = currentVersion;
|
|
395
|
-
|
|
396
|
-
for (let i = 0; i < manifests.length; i++) {
|
|
397
|
-
const manifest = manifests[i];
|
|
398
|
-
const tag = chainTags[i];
|
|
399
|
-
if (!(manifest && tag)) return null;
|
|
400
|
-
|
|
401
|
-
const remainingBudget = fullGzSize * SIZE_THRESHOLD_RATIO - totalSize;
|
|
402
|
-
const result = validateChainStep(manifest, {
|
|
403
|
-
expectedFrom: prevVersion,
|
|
404
|
-
patchLayerName,
|
|
405
|
-
sizeLimit: remainingBudget,
|
|
406
|
-
});
|
|
407
|
-
if (!result.ok) return null;
|
|
408
|
-
|
|
409
|
-
digests.push(result.digest);
|
|
410
|
-
totalSize += result.size;
|
|
411
|
-
prevVersion = tag.slice(PATCH_TAG_PREFIX.length);
|
|
412
|
-
|
|
413
|
-
if (i === manifests.length - 1) {
|
|
414
|
-
if (prevVersion !== targetVersion) return null;
|
|
415
|
-
const sha256 = getPatchTargetSha256(manifest, binaryName) ?? "";
|
|
416
|
-
if (!sha256) return null;
|
|
417
|
-
return { digests, totalSize, expectedSha256: sha256 };
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Resolve a chain of nightly patches from current to target version.
|
|
426
|
-
*/
|
|
427
|
-
export async function resolveNightlyChain(opts: {
|
|
428
|
-
token: string;
|
|
429
|
-
currentVersion: string;
|
|
430
|
-
targetVersion: string;
|
|
431
|
-
fullGzSize: number;
|
|
432
|
-
preloadedTags?: string[];
|
|
433
|
-
signal?: AbortSignal;
|
|
434
|
-
}): Promise<PatchChain | null> {
|
|
435
|
-
const {
|
|
436
|
-
token,
|
|
437
|
-
currentVersion,
|
|
438
|
-
targetVersion,
|
|
439
|
-
fullGzSize,
|
|
440
|
-
preloadedTags,
|
|
441
|
-
signal,
|
|
442
|
-
} = opts;
|
|
443
|
-
const binaryName = getPlatformBinaryName();
|
|
444
|
-
const patchLayerName = `${binaryName}.patch`;
|
|
445
|
-
|
|
446
|
-
const allTags =
|
|
447
|
-
preloadedTags ?? (await listTags(token, PATCH_TAG_PREFIX, signal));
|
|
448
|
-
|
|
449
|
-
const chainTags = filterAndSortChainTags(
|
|
450
|
-
allTags,
|
|
451
|
-
currentVersion,
|
|
452
|
-
targetVersion,
|
|
453
|
-
);
|
|
454
|
-
if (chainTags.length === 0 || chainTags.length > MAX_NIGHTLY_CHAIN_DEPTH) {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Fetch manifests for chain tags
|
|
459
|
-
const fetchedManifests = new Map<string, OciManifest>();
|
|
460
|
-
const results = await Promise.all(
|
|
461
|
-
chainTags.map(async (tag) => {
|
|
462
|
-
try {
|
|
463
|
-
const manifest = await fetchManifest(token, tag, signal);
|
|
464
|
-
return { tag, manifest };
|
|
465
|
-
} catch {
|
|
466
|
-
return { tag, manifest: null };
|
|
467
|
-
}
|
|
468
|
-
}),
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
for (const { tag, manifest } of results) {
|
|
472
|
-
if (manifest) fetchedManifests.set(tag, manifest);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const manifests: (OciManifest | undefined)[] = chainTags.map((tag) =>
|
|
476
|
-
fetchedManifests.get(tag),
|
|
477
|
-
);
|
|
478
|
-
if (manifests.some((m) => !m)) return null;
|
|
479
|
-
|
|
480
|
-
const validation = validateNightlyChain({
|
|
481
|
-
manifests: manifests as OciManifest[],
|
|
482
|
-
chainTags,
|
|
483
|
-
currentVersion,
|
|
484
|
-
targetVersion,
|
|
485
|
-
patchLayerName,
|
|
486
|
-
binaryName,
|
|
487
|
-
fullGzSize,
|
|
488
|
-
});
|
|
489
|
-
if (!validation) return null;
|
|
490
|
-
|
|
491
|
-
// Parallel blob download
|
|
492
|
-
const downloadResults = await Promise.all(
|
|
493
|
-
validation.digests.map((digest) =>
|
|
494
|
-
downloadLayerBlob(token, digest, signal).then(
|
|
495
|
-
(buf) => new Uint8Array(buf),
|
|
496
|
-
),
|
|
497
|
-
),
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
const patches: PatchLink[] = [];
|
|
501
|
-
let downloadedSize = 0;
|
|
502
|
-
for (const data of downloadResults) {
|
|
503
|
-
patches.push({ data, size: data.byteLength });
|
|
504
|
-
downloadedSize += data.byteLength;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const steps: { fromVersion: string; toVersion: string }[] = [];
|
|
508
|
-
let prevVersion = currentVersion;
|
|
509
|
-
for (const tag of chainTags) {
|
|
510
|
-
const toVersion = tag.slice(PATCH_TAG_PREFIX.length);
|
|
511
|
-
steps.push({ fromVersion: prevVersion, toVersion });
|
|
512
|
-
prevVersion = toVersion;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return {
|
|
516
|
-
patches,
|
|
517
|
-
totalSize: downloadedSize,
|
|
518
|
-
expectedSha256: validation.expectedSha256,
|
|
519
|
-
steps,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ---------------------------------------------------------------------------
|
|
524
|
-
// Main entry point: attempt delta upgrade
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Attempt to download and apply delta patches instead of a full binary.
|
|
529
|
-
*
|
|
530
|
-
* This is the main entry point called by `downloadBinaryToTemp()` in
|
|
531
|
-
* the upgrade module. Falls back gracefully to null on any failure.
|
|
532
|
-
*/
|
|
533
|
-
export async function attemptDeltaUpgrade(
|
|
534
|
-
targetVersion: string,
|
|
535
|
-
oldBinaryPath: string,
|
|
536
|
-
destPath: string,
|
|
537
|
-
offline?: boolean,
|
|
538
|
-
): Promise<DeltaResult | null> {
|
|
539
|
-
if (!canAttemptDelta(targetVersion)) {
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const channel = isNightlyVersion(targetVersion) ? "nightly" : "stable";
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const result =
|
|
547
|
-
channel === "nightly"
|
|
548
|
-
? await resolveAndApplyDelta({
|
|
549
|
-
targetVersion,
|
|
550
|
-
oldBinaryPath,
|
|
551
|
-
destPath,
|
|
552
|
-
resolveFromNetwork: () =>
|
|
553
|
-
resolveNightlyChainWithContext(targetVersion),
|
|
554
|
-
channel: "nightly",
|
|
555
|
-
offline,
|
|
556
|
-
})
|
|
557
|
-
: await resolveAndApplyDelta({
|
|
558
|
-
targetVersion,
|
|
559
|
-
oldBinaryPath,
|
|
560
|
-
destPath,
|
|
561
|
-
resolveFromNetwork: () =>
|
|
562
|
-
resolveStableChain(VERSION, targetVersion),
|
|
563
|
-
channel: "stable",
|
|
564
|
-
offline,
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
return result;
|
|
568
|
-
} catch (error) {
|
|
569
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
570
|
-
console.error(
|
|
571
|
-
`[lore] Delta upgrade failed (${msg}), falling back to full download`,
|
|
572
|
-
);
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
// Shared cache-first resolve + apply logic
|
|
579
|
-
// ---------------------------------------------------------------------------
|
|
580
|
-
|
|
581
|
-
type ResolveAndApplyOpts = {
|
|
582
|
-
targetVersion: string;
|
|
583
|
-
oldBinaryPath: string;
|
|
584
|
-
destPath: string;
|
|
585
|
-
resolveFromNetwork: () => Promise<PatchChain | null>;
|
|
586
|
-
channel: string;
|
|
587
|
-
offline?: boolean;
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
async function resolveAndApplyDelta(
|
|
591
|
-
opts: ResolveAndApplyOpts,
|
|
592
|
-
): Promise<DeltaResult | null> {
|
|
593
|
-
const {
|
|
594
|
-
targetVersion,
|
|
595
|
-
oldBinaryPath,
|
|
596
|
-
destPath,
|
|
597
|
-
resolveFromNetwork,
|
|
598
|
-
channel,
|
|
599
|
-
offline,
|
|
600
|
-
} = opts;
|
|
601
|
-
|
|
602
|
-
// Check patch cache first — enables fully offline upgrades
|
|
603
|
-
const cached = await tryLoadCachedChain(VERSION, targetVersion);
|
|
604
|
-
if (cached) {
|
|
605
|
-
return await applyChainAndReturn(cached, oldBinaryPath, destPath);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (offline) {
|
|
609
|
-
return null;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const chain = await resolveFromNetwork();
|
|
613
|
-
if (!chain) return null;
|
|
614
|
-
|
|
615
|
-
// Save to cache for future offline upgrades, then apply
|
|
616
|
-
if (chain.steps) {
|
|
617
|
-
savePatchesToCache(chain, chain.steps).catch(() => {});
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return await applyChainAndReturn(chain, oldBinaryPath, destPath);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async function tryLoadCachedChain(
|
|
624
|
-
currentVersion: string,
|
|
625
|
-
targetVersion: string,
|
|
626
|
-
): Promise<PatchChain | null> {
|
|
627
|
-
try {
|
|
628
|
-
return await loadCachedChain(currentVersion, targetVersion);
|
|
629
|
-
} catch {
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async function applyChainAndReturn(
|
|
635
|
-
chain: PatchChain,
|
|
636
|
-
oldBinaryPath: string,
|
|
637
|
-
destPath: string,
|
|
638
|
-
): Promise<DeltaResult> {
|
|
639
|
-
const sha256 = await applyPatchChain(chain, oldBinaryPath, destPath);
|
|
640
|
-
return {
|
|
641
|
-
sha256,
|
|
642
|
-
patchBytes: chain.totalSize,
|
|
643
|
-
chainLength: chain.patches.length,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Resolve a nightly chain with full context setup.
|
|
649
|
-
*/
|
|
650
|
-
async function resolveNightlyChainWithContext(
|
|
651
|
-
targetVersion: string,
|
|
652
|
-
signal?: AbortSignal,
|
|
653
|
-
): Promise<PatchChain | null> {
|
|
654
|
-
const token = await getAnonymousToken(signal);
|
|
655
|
-
|
|
656
|
-
const binaryName = getPlatformBinaryName();
|
|
657
|
-
const targetTag = `nightly-${targetVersion}`;
|
|
658
|
-
|
|
659
|
-
const [nightlyManifest, patchTags] = await Promise.all([
|
|
660
|
-
fetchManifest(token, targetTag, signal),
|
|
661
|
-
listTags(token, PATCH_TAG_PREFIX, signal),
|
|
662
|
-
]);
|
|
663
|
-
|
|
664
|
-
const gzLayer = nightlyManifest.layers.find((l) => {
|
|
665
|
-
const title = l.annotations?.["org.opencontainers.image.title"];
|
|
666
|
-
return title === `${binaryName}.gz`;
|
|
667
|
-
});
|
|
668
|
-
if (!gzLayer) return null;
|
|
669
|
-
|
|
670
|
-
return await resolveNightlyChain({
|
|
671
|
-
token,
|
|
672
|
-
currentVersion: VERSION,
|
|
673
|
-
targetVersion,
|
|
674
|
-
fullGzSize: gzLayer.size,
|
|
675
|
-
preloadedTags: patchTags,
|
|
676
|
-
signal,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// ---------------------------------------------------------------------------
|
|
681
|
-
// Patch application
|
|
682
|
-
// ---------------------------------------------------------------------------
|
|
683
|
-
|
|
684
|
-
function cleanupIntermediates(destPath: string): void {
|
|
685
|
-
for (const suffix of [".patching.a", ".patching.b"]) {
|
|
686
|
-
try {
|
|
687
|
-
unlinkSync(`${destPath}${suffix}`);
|
|
688
|
-
} catch {
|
|
689
|
-
// Ignore cleanup errors
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async function applyPatchesSequentially(
|
|
695
|
-
chain: PatchChain,
|
|
696
|
-
oldBinaryPath: string,
|
|
697
|
-
destPath: string,
|
|
698
|
-
): Promise<string> {
|
|
699
|
-
let currentOldPath = oldBinaryPath;
|
|
700
|
-
let sha256 = "";
|
|
701
|
-
|
|
702
|
-
const intermediateA = `${destPath}.patching.a`;
|
|
703
|
-
const intermediateB = `${destPath}.patching.b`;
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
for (let i = 0; i < chain.patches.length; i++) {
|
|
707
|
-
const patch = chain.patches[i];
|
|
708
|
-
if (!patch) throw new Error(`Missing patch at index ${i}`);
|
|
709
|
-
const isLast = i === chain.patches.length - 1;
|
|
710
|
-
const intermediate = i % 2 === 0 ? intermediateA : intermediateB;
|
|
711
|
-
const outputPath = isLast ? destPath : intermediate;
|
|
712
|
-
|
|
713
|
-
sha256 = await applyPatch(currentOldPath, patch.data, outputPath);
|
|
714
|
-
|
|
715
|
-
if (!isLast) {
|
|
716
|
-
currentOldPath = outputPath;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
} finally {
|
|
720
|
-
if (chain.patches.length > 1) {
|
|
721
|
-
cleanupIntermediates(destPath);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
return sha256;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Apply a resolved patch chain sequentially and verify the result.
|
|
730
|
-
*/
|
|
731
|
-
export async function applyPatchChain(
|
|
732
|
-
chain: PatchChain,
|
|
733
|
-
oldBinaryPath: string,
|
|
734
|
-
destPath: string,
|
|
735
|
-
): Promise<string> {
|
|
736
|
-
const sha256 = await applyPatchesSequentially(chain, oldBinaryPath, destPath);
|
|
737
|
-
|
|
738
|
-
if (sha256 !== chain.expectedSha256) {
|
|
739
|
-
throw new Error(
|
|
740
|
-
`SHA-256 mismatch after patching: got ${sha256}, expected ${chain.expectedSha256}`,
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return sha256;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
// Patch Pre-fetching (can be called during background version checks)
|
|
749
|
-
// ---------------------------------------------------------------------------
|
|
750
|
-
|
|
751
|
-
async function prefetchAndCache(
|
|
752
|
-
targetVersion: string,
|
|
753
|
-
signal: AbortSignal | undefined,
|
|
754
|
-
resolveChain: () => Promise<PatchChain | null>,
|
|
755
|
-
): Promise<void> {
|
|
756
|
-
if (!canAttemptDelta(targetVersion) || signal?.aborted) {
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const chain = await resolveChain();
|
|
761
|
-
if (!chain?.steps || signal?.aborted) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
await savePatchesToCache(chain, chain.steps);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Pre-fetch nightly delta patches for a future upgrade.
|
|
770
|
-
*/
|
|
771
|
-
export function prefetchNightlyPatches(
|
|
772
|
-
targetVersion: string,
|
|
773
|
-
signal?: AbortSignal,
|
|
774
|
-
): Promise<void> {
|
|
775
|
-
return prefetchAndCache(targetVersion, signal, () =>
|
|
776
|
-
resolveNightlyChainWithContext(targetVersion, signal),
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Pre-fetch stable delta patches for a future upgrade.
|
|
782
|
-
*/
|
|
783
|
-
export function prefetchStablePatches(
|
|
784
|
-
targetVersion: string,
|
|
785
|
-
signal?: AbortSignal,
|
|
786
|
-
): Promise<void> {
|
|
787
|
-
return prefetchAndCache(targetVersion, signal, () =>
|
|
788
|
-
resolveStableChain(VERSION, targetVersion, signal),
|
|
789
|
-
);
|
|
790
|
-
}
|