relay-companion 0.1.573 → 0.1.574
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.
|
@@ -88,6 +88,13 @@ async function handoffApplication({ activationEnabled = false, envelope, version
|
|
|
88
88
|
const target = { version, sourceSha }, transactionId = `application_${sourceSha}_${platformKey}`;
|
|
89
89
|
let lock, releaseDrain, snapshot, location;
|
|
90
90
|
const current = () => read(pointerPath);
|
|
91
|
+
// The runtime this handoff leaves running: the application's own, or a newer
|
|
92
|
+
// one already running under an installed Relay app, which only the app replaces.
|
|
93
|
+
const expectedRuntime = () => {
|
|
94
|
+
snapshot ||= read(snapshotFile);
|
|
95
|
+
const kept = snapshot?.previous?.version;
|
|
96
|
+
return /^\d+\.\d+\.\d+$/.test(kept || "") && lifecycle.versionCompare(kept, payload.runtime.version) > 0 ? kept : payload.runtime.version;
|
|
97
|
+
};
|
|
91
98
|
const adapters = {
|
|
92
99
|
lock() {
|
|
93
100
|
lock = bootstrap.acquireCanonicalLock(path.join(base, "runtime", "transaction.lock"));
|
|
@@ -103,8 +110,11 @@ async function handoffApplication({ activationEnabled = false, envelope, version
|
|
|
103
110
|
snapshot = read(snapshotFile);
|
|
104
111
|
// Another updater may have advanced after an interrupted handoff. Never
|
|
105
112
|
// turn an old journal into authorization to roll that newer runtime back.
|
|
106
|
-
if (lifecycle.versionCompare(active.version, payload.runtime.version) > 0) return { ok: false, preserveCurrent: true, reason: "newer-runtime-preserved" };
|
|
107
113
|
const owner = applicationOwner({ homeDir, platform });
|
|
114
|
+
// The runtime updates itself far more often than the app. Where an app
|
|
115
|
+
// already owns Relay, a newer runtime stays and only the app is replaced;
|
|
116
|
+
// a legacy installation is never moved to an app older than its runtime.
|
|
117
|
+
if (!owner && lifecycle.versionCompare(active.version, payload.runtime.version) > 0) return { ok: false, preserveCurrent: true, reason: "newer-runtime-preserved" };
|
|
108
118
|
if (owner && lifecycle.versionCompare(owner.installedPackageVersion, version) > 0) return { ok: false, preserveCurrent: true, reason: "newer-application-preserved" };
|
|
109
119
|
if (snapshot && active.packageRoot !== snapshot.previous?.packageRoot
|
|
110
120
|
&& !(owner?.packagingSourceSha === sourceSha && active.version === payload.runtime.version)) return { ok: false, preserveCurrent: true, reason: "another-runtime-transaction-preserved" };
|
|
@@ -149,16 +159,16 @@ async function handoffApplication({ activationEnabled = false, envelope, version
|
|
|
149
159
|
const active = current();
|
|
150
160
|
const owner = applicationOwner({ homeDir, platform });
|
|
151
161
|
return { ok: owner?.packagingSourceSha === sourceSha && owner.installedPackageVersion === version
|
|
152
|
-
&& active?.version ===
|
|
162
|
+
&& active?.version === expectedRuntime() && (await health(active, { platform })).ok };
|
|
153
163
|
},
|
|
154
|
-
verifyRecovery: () => verifyRecovery({ homeDir, platform, version:
|
|
155
|
-
observeStability: () => ready({ homeDir, platform, target: { version:
|
|
164
|
+
verifyRecovery: () => verifyRecovery({ homeDir, platform, version: expectedRuntime() }),
|
|
165
|
+
observeStability: () => ready({ homeDir, platform, target: { version: expectedRuntime(), packageRoot: current()?.packageRoot }, requireProgress: true }),
|
|
156
166
|
async restorePrevious() {
|
|
157
167
|
snapshot ||= read(snapshotFile);
|
|
158
168
|
const previous = snapshot?.previous;
|
|
159
169
|
if (!previous || !nativeLocationInside(path.join(base, "runtime", "releases"), previous.packageRoot)
|
|
160
170
|
|| !nativeLocationInside(previous.packageRoot, previous.bin)) return { ok: false };
|
|
161
|
-
if (current()?.active && lifecycle.versionCompare(current().version,
|
|
171
|
+
if (current()?.active && lifecycle.versionCompare(current().version, expectedRuntime()) > 0) return { ok: false };
|
|
162
172
|
let restored = current()?.packageRoot === previous.packageRoot && (await health(previous, { platform })).ok;
|
|
163
173
|
if (!restored) {
|
|
164
174
|
const result = run(previous.node, [previous.bin, "repair-runtime", "--no-trampoline", "--claim"], { encoding: "utf8", windowsHide: true, timeout: 3 * 60_000 });
|
|
@@ -174,11 +184,11 @@ async function handoffApplication({ activationEnabled = false, envelope, version
|
|
|
174
184
|
},
|
|
175
185
|
async retirePrevious() {
|
|
176
186
|
const active = current();
|
|
177
|
-
if (active?.version !==
|
|
187
|
+
if (active?.version !== expectedRuntime() || !(await health(active, { platform })).ok) return { ok: false };
|
|
178
188
|
// Known-good runtime trees and account data remain recovery-owned. Retire
|
|
179
189
|
// migration eligibility, not the rollback copy or the frozen npm bridge.
|
|
180
190
|
write(path.join(base, "application-handoff-complete.json"), { schema: 1, ...target, platform: platformKey,
|
|
181
|
-
runtime: payload.runtime, at: Date.now(), previousRuntimeRetained: true });
|
|
191
|
+
runtime: payload.runtime, keptRuntime: expectedRuntime() !== payload.runtime.version ? expectedRuntime() : undefined, at: Date.now(), previousRuntimeRetained: true });
|
|
182
192
|
return { ok: true };
|
|
183
193
|
},
|
|
184
194
|
};
|
|
@@ -129,10 +129,13 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
129
129
|
|| fs.existsSync(path.join(homeDir, ".relay-companion")))) {
|
|
130
130
|
needsRecovery = true;
|
|
131
131
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
// The runtime updates itself far more often than the app. Under an installed
|
|
133
|
+
// Relay app a newer runtime stays and only the app is replaced (below); a
|
|
134
|
+
// legacy installation is never handed to an app older than its runtime.
|
|
135
|
+
const newerRuntime = Boolean(current && /^\d+\.\d+\.\d+$/.test(current.version || "")
|
|
136
|
+
&& versionCompare(bundle.receipt.version, current.version) < 0);
|
|
137
|
+
if (newerRuntime && recovery.marker(ownerPath)?.appId !== APPLICATION_ID) throw new Error("An older installer cannot downgrade Relay");
|
|
138
|
+
if (current && /^\d+\.\d+\.\d+$/.test(current.version || "") && versionCompare(current.version, "0.1.500") < 0) needsRecovery = true;
|
|
136
139
|
// Never execute an old CLI as a prerequisite for rescuing it.
|
|
137
140
|
if (current && !needsRecovery && !(await health(current, { platform: process.platform })).ok) needsRecovery = true;
|
|
138
141
|
if (priorJournal && !["complete", "rolled-back"].includes(priorJournal.state)) {
|
|
@@ -152,6 +155,22 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
|
|
|
152
155
|
onProgress({ phase: "ready", canCancel: false });
|
|
153
156
|
return { ok: true, alreadyInstalled: true, owner: installedOwner, runtime: current, updateOwner: "canonical-runtime" };
|
|
154
157
|
}
|
|
158
|
+
if (newerRuntime) {
|
|
159
|
+
// Repairing would install this older runtime over the newer one; the
|
|
160
|
+
// runtime's own recovery owns that. A healthy newer runtime keeps running.
|
|
161
|
+
if (needsRecovery) throw new Error("An older installer cannot downgrade Relay");
|
|
162
|
+
const owner = { schema: 1, appId: APPLICATION_ID, platform: process.platform,
|
|
163
|
+
root, executable, receipt: receiptPath, version: bundle.receipt.version,
|
|
164
|
+
applicationVersion: bundle.receipt.applicationVersion || bundle.receipt.version,
|
|
165
|
+
packagingSourceSha: bundle.receipt.packagingSourceSha, installationId: previousOwner?.installationId || crypto.randomUUID(),
|
|
166
|
+
updateOwner: "canonical-runtime" };
|
|
167
|
+
const at = Date.now();
|
|
168
|
+
atomic(journalPath, { schema: 1, state: "complete", owner, previousOwner, previous: current, releaseId: current.releaseId,
|
|
169
|
+
runtimeKept: true, at, completedAt: at });
|
|
170
|
+
atomic(ownerPath, owner);
|
|
171
|
+
onProgress({ phase: "ready", canCancel: false });
|
|
172
|
+
return { ok: true, applicationOnly: true, owner, runtime: current, updateOwner: "canonical-runtime" };
|
|
173
|
+
}
|
|
155
174
|
if (bundle.receipt.runtimeDelivery === "download") {
|
|
156
175
|
signal?.throwIfAborted();
|
|
157
176
|
// Stage beside releases: tar requires archive and destination on one volume.
|
|
@@ -61,7 +61,10 @@ async function checkApplicationUpdate({ activationEnabled = rollout.enabled, hom
|
|
|
61
61
|
let result;
|
|
62
62
|
if (owner && (versionCompare(owner.installedPackageVersion, payload.version) > 0
|
|
63
63
|
|| ((owner.applicationVersion || owner.version) === payload.version && owner.packagingSourceSha === payload.sourceSha))) result = { state: "current", changed: false };
|
|
64
|
-
|
|
64
|
+
// An installed app is updated even when its runtime has moved past the
|
|
65
|
+
// offer's: the runtime stays and only the app changes. Before an app owns
|
|
66
|
+
// Relay, an offer older than the running runtime is still not taken.
|
|
67
|
+
else if (!owner && versionCompare(current.version, payload.runtime.version) > 0) result = { state: "older-runtime-refused", changed: false };
|
|
65
68
|
else {
|
|
66
69
|
const platformKey = `${platform}-${arch}`;
|
|
67
70
|
const kind = platform === "darwin" ? "zip" : platform === "win32" ? "exe"
|
|
@@ -71,7 +74,7 @@ async function checkApplicationUpdate({ activationEnabled = rollout.enabled, hom
|
|
|
71
74
|
const directory = path.join(root, "application-packages", `${payload.version}-${payload.sourceSha}-${platformKey}`);
|
|
72
75
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
73
76
|
const file = path.join(directory, `installer.${kind}`);
|
|
74
|
-
if (!fs.existsSync(file)) await download(artifact.url, file, artifact);
|
|
77
|
+
if (!fs.existsSync(file)) await download(artifact.url, file, artifact, { origin: release.CHANNEL_ORIGINS[consent.channel] });
|
|
75
78
|
await release.verifyApplicationArtifact(file, artifact);
|
|
76
79
|
// Recheck the live signed policy after a potentially slow download. A
|
|
77
80
|
// withdrawn offer must not start a new transaction from a cached plan.
|
|
@@ -19,6 +19,9 @@ const {
|
|
|
19
19
|
} = require("./runtime-executables.cjs");
|
|
20
20
|
|
|
21
21
|
const RELEASE_ORIGIN = "https://api.sendrelays.com";
|
|
22
|
+
// Dev app updates are served by the Dev API. A caller names the origin its
|
|
23
|
+
// signed offer pinned; nothing else is ever trusted.
|
|
24
|
+
const ARTIFACT_ORIGINS = new Set([RELEASE_ORIGIN, "https://dev-api.sendrelays.com"]);
|
|
22
25
|
const RELEASE_BASE_PATH = "/v1/companion-releases";
|
|
23
26
|
const PACKAGE_NAME = "relay-companion";
|
|
24
27
|
// Native macOS bundles exceed Node's default 1 MiB tar-output buffer. Keep a
|
|
@@ -297,7 +300,8 @@ function sha512File(file) {
|
|
|
297
300
|
async function downloadVerifiedArtifact(url, file, artifact, options = {}) {
|
|
298
301
|
options.signal?.throwIfAborted();
|
|
299
302
|
const parsed = new URL(url);
|
|
300
|
-
|
|
303
|
+
const origin = options.origin || RELEASE_ORIGIN;
|
|
304
|
+
if (parsed.protocol !== "https:" || !ARTIFACT_ORIGINS.has(origin) || parsed.origin !== origin) {
|
|
301
305
|
fail(`Relay refused an untrusted artifact origin: ${parsed.origin}`);
|
|
302
306
|
}
|
|
303
307
|
if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || artifact.bytes > MAX_ARTIFACT_BYTES) {
|
package/package.json
CHANGED