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 === payload.runtime.version && (await health(active, { platform })).ok };
162
+ && active?.version === expectedRuntime() && (await health(active, { platform })).ok };
153
163
  },
154
- verifyRecovery: () => verifyRecovery({ homeDir, platform, version: payload.runtime.version }),
155
- observeStability: () => ready({ homeDir, platform, target: { version: payload.runtime.version, packageRoot: current()?.packageRoot }, requireProgress: true }),
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, payload.runtime.version) > 0) return { ok: false };
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 !== payload.runtime.version || !(await health(active, { platform })).ok) return { ok: false };
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
- if (current && /^\d+\.\d+\.\d+$/.test(current.version || "")) {
133
- if (versionCompare(bundle.receipt.version, current.version) < 0) throw new Error("An older installer cannot downgrade Relay");
134
- if (versionCompare(current.version, "0.1.500") < 0) needsRecovery = true;
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
- else if (versionCompare(current.version, payload.runtime.version) > 0) result = { state: "older-runtime-refused", changed: false };
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
- if (parsed.protocol !== "https:" || parsed.origin !== RELEASE_ORIGIN) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.573",
3
+ "version": "0.1.574",
4
4
  "description": "Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.",
5
5
  "homepage": "https://sendrelays.com/get-started",
6
6
  "repository": {