patchrelay 0.10.2 → 0.10.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.10.2",
4
- "commit": "9590d03b1a4d",
5
- "builtAt": "2026-03-22T19:12:49.577Z"
3
+ "version": "0.10.3",
4
+ "commit": "4390db85f84b",
5
+ "builtAt": "2026-03-22T19:25:51.766Z"
6
6
  }
package/dist/cli/index.js CHANGED
@@ -212,7 +212,17 @@ export async function runCli(argv, options) {
212
212
  if (command === "doctor") {
213
213
  const { runPreflight } = await import("../preflight.js");
214
214
  const report = await runPreflight(config);
215
- writeOutput(stdout, json ? formatJson(report) : formatDoctor(report));
215
+ const cliVersion = getBuildInfo().version;
216
+ let serviceVersion;
217
+ try {
218
+ const healthUrl = `http://${config.server.bind}:${config.server.port}${config.server.healthPath}`;
219
+ const res = await fetch(healthUrl, { signal: AbortSignal.timeout(2000) });
220
+ const body = await res.json();
221
+ serviceVersion = body.version ?? undefined;
222
+ }
223
+ catch { /* service not reachable */ }
224
+ const doctorReport = { ...report, cliVersion, serviceVersion };
225
+ writeOutput(stdout, json ? formatJson(doctorReport) : formatDoctor(doctorReport, cliVersion, serviceVersion));
216
226
  return report.ok ? 0 : 1;
217
227
  }
218
228
  if (command === "inspect") {
@@ -5,8 +5,15 @@ export function writeOutput(stream, text) {
5
5
  export function writeUsageError(stream, error) {
6
6
  writeOutput(stream, `${helpTextFor(error.helpTopic)}\n\nError: ${error.message}\n`);
7
7
  }
8
- export function formatDoctor(report) {
8
+ export function formatDoctor(report, cliVersion, serviceVersion) {
9
9
  const lines = ["PatchRelay doctor", ""];
10
+ if (cliVersion) {
11
+ const versionLine = serviceVersion
12
+ ? (cliVersion === serviceVersion ? `cli=${cliVersion} service=${serviceVersion}` : `cli=${cliVersion} service=${serviceVersion} (mismatch!)`)
13
+ : `cli=${cliVersion} service=not reachable`;
14
+ lines.push(versionLine);
15
+ lines.push("");
16
+ }
10
17
  for (const check of report.checks) {
11
18
  const marker = check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
12
19
  lines.push(`${marker} [${check.scope}] ${check.message}`);
@@ -58,6 +58,15 @@ export class LinearInstallationStore {
58
58
  .run(params.accessTokenCiphertext, params.refreshTokenCiphertext ?? null, params.scopesJson ?? null, params.tokenType ?? null, params.expiresAt ?? null, isoNow(), id);
59
59
  return this.getLinearInstallation(id);
60
60
  }
61
+ updateLinearInstallationIdentity(id, params) {
62
+ this.connection
63
+ .prepare(`UPDATE linear_installations
64
+ SET workspace_name = COALESCE(?, workspace_name),
65
+ workspace_key = COALESCE(?, workspace_key),
66
+ updated_at = ?
67
+ WHERE id = ?`)
68
+ .run(params.workspaceName ?? null, params.workspaceKey ?? null, isoNow(), id);
69
+ }
61
70
  getLinearInstallation(id) {
62
71
  const row = this.connection
63
72
  .prepare("SELECT * FROM linear_installations WHERE id = ?")
package/dist/http.js CHANGED
@@ -332,7 +332,7 @@ export async function buildHttpServer(config, service, logger) {
332
332
  });
333
333
  app.get("/api/oauth/linear/start", async (request, reply) => {
334
334
  const projectId = getQueryParam(request, "projectId");
335
- const result = service.createLinearOAuthStart(projectId ? { projectId } : undefined);
335
+ const result = await service.createLinearOAuthStart(projectId ? { projectId } : undefined);
336
336
  return reply.send({ ok: true, ...result });
337
337
  });
338
338
  app.get("/api/oauth/linear/state/:state", async (request, reply) => {
@@ -1,4 +1,5 @@
1
- import { createLinearOAuthUrl, createOAuthStateToken, installLinearOAuthCode } from "./linear-oauth.js";
1
+ import { createLinearOAuthUrl, createOAuthStateToken, fetchLinearViewerIdentity, installLinearOAuthCode } from "./linear-oauth.js";
2
+ import { decryptSecret } from "./token-crypto.js";
2
3
  const LINEAR_OAUTH_STATE_TTL_MS = 15 * 60 * 1000;
3
4
  function oauthStateExpired(createdAt) {
4
5
  const createdAtMs = Date.parse(createdAt);
@@ -13,7 +14,7 @@ export class LinearOAuthService {
13
14
  this.stores = stores;
14
15
  this.logger = logger;
15
16
  }
16
- createStart(params) {
17
+ async createStart(params) {
17
18
  if (params?.projectId && !this.config.projects.some((project) => project.id === params.projectId)) {
18
19
  throw new Error(`Unknown project: ${params.projectId}`);
19
20
  }
@@ -22,11 +23,13 @@ export class LinearOAuthService {
22
23
  if (existingLink) {
23
24
  const installation = this.stores.linearInstallations.getLinearInstallation(existingLink.installationId);
24
25
  if (installation) {
26
+ await this.refreshInstallationIdentity(installation);
27
+ const updated = this.stores.linearInstallations.getLinearInstallation(installation.id) ?? installation;
25
28
  return {
26
29
  completed: true,
27
30
  reusedExisting: true,
28
31
  projectId: params.projectId,
29
- installation: this.getInstallationSummary(installation),
32
+ installation: this.getInstallationSummary(updated),
30
33
  };
31
34
  }
32
35
  }
@@ -35,11 +38,13 @@ export class LinearOAuthService {
35
38
  const installation = installations[0];
36
39
  if (installation) {
37
40
  this.stores.linearInstallations.linkProjectInstallation(params.projectId, installation.id);
41
+ await this.refreshInstallationIdentity(installation);
42
+ const updated = this.stores.linearInstallations.getLinearInstallation(installation.id) ?? installation;
38
43
  return {
39
44
  completed: true,
40
45
  reusedExisting: true,
41
46
  projectId: params.projectId,
42
- installation: this.getInstallationSummary(installation),
47
+ installation: this.getInstallationSummary(updated),
43
48
  };
44
49
  }
45
50
  }
@@ -118,6 +123,21 @@ export class LinearOAuthService {
118
123
  linkedProjects: links.filter((link) => link.installationId === installation.id).map((link) => link.projectId),
119
124
  }));
120
125
  }
126
+ async refreshInstallationIdentity(installation) {
127
+ try {
128
+ const accessToken = decryptSecret(installation.accessTokenCiphertext, this.config.linear.tokenEncryptionKey);
129
+ const identity = await fetchLinearViewerIdentity(this.config.linear.graphqlUrl, accessToken, this.logger);
130
+ if (identity.workspaceName || identity.workspaceKey) {
131
+ this.stores.linearInstallations.updateLinearInstallationIdentity(installation.id, {
132
+ ...(identity.workspaceName ? { workspaceName: identity.workspaceName } : {}),
133
+ ...(identity.workspaceKey ? { workspaceKey: identity.workspaceKey } : {}),
134
+ });
135
+ }
136
+ }
137
+ catch (error) {
138
+ this.logger.debug({ installationId: installation.id, error: error instanceof Error ? error.message : String(error) }, "Failed to refresh installation identity (non-blocking)");
139
+ }
140
+ }
121
141
  getInstallationSummary(installation) {
122
142
  return {
123
143
  id: installation.id,
package/dist/service.js CHANGED
@@ -48,8 +48,8 @@ export class PatchRelayService {
48
48
  stop() {
49
49
  this.runtime.stop();
50
50
  }
51
- createLinearOAuthStart(params) {
52
- return this.oauthService.createStart(params);
51
+ async createLinearOAuthStart(params) {
52
+ return await this.oauthService.createStart(params);
53
53
  }
54
54
  async completeLinearOAuth(params) {
55
55
  return await this.oauthService.complete(params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {