@iann29/synapse 1.8.5 → 1.8.6

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.
@@ -23,6 +23,24 @@ const {
23
23
  } = require("../config");
24
24
  const { readProjectConfig } = require("../project");
25
25
 
26
+ // v1.8.6 (A3): SessionExpiredError carries an operator-actionable
27
+ // message when the refresh token itself is rejected (>30d old or
28
+ // revoked at server side). bin/synapse.js prints err.message verbatim,
29
+ // so a clear "your session expired, run `synapse login <url>`" lands
30
+ // directly in front of the operator instead of a cryptic
31
+ // "Synapse API returned 401" message.
32
+ class SessionExpiredError extends Error {
33
+ constructor(baseUrl, cause) {
34
+ super(
35
+ `Your Synapse session expired. Run \`synapse login ${baseUrl}\` to sign in again.` +
36
+ (cause ? ` (refresh failed: ${cause})` : ""),
37
+ );
38
+ this.name = "SessionExpiredError";
39
+ this.baseUrl = baseUrl;
40
+ this.cause = cause;
41
+ }
42
+ }
43
+
26
44
  // Wraps an API client so any 401 transparently retries against
27
45
  // /v1/auth/refresh once. Mirrors what bin/synapse.js had before the
28
46
  // refactor; lives here so every command shares it.
@@ -43,10 +61,30 @@ function makeRefreshableApi(cfg) {
43
61
  ) {
44
62
  throw err;
45
63
  }
46
- const session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(
47
- cfg.refreshToken,
48
- );
49
- if (!session.accessToken) throw err;
64
+ // v1.8.6 (A3): catch refresh failures and surface a clear
65
+ // re-login instruction instead of the raw upstream 401.
66
+ // Refresh tokens expire (30d default) or get revoked
67
+ // server-side; either way the operator needs to log in
68
+ // again and the bare "Synapse API returned 401" gave them
69
+ // no path forward.
70
+ let session;
71
+ try {
72
+ session = await new SynapseAPI({ baseUrl: cfg.baseUrl }).refresh(
73
+ cfg.refreshToken,
74
+ );
75
+ } catch (refreshErr) {
76
+ const detail =
77
+ refreshErr instanceof SynapseAPIError
78
+ ? `${refreshErr.code} ${refreshErr.status}`
79
+ : String(refreshErr.message || refreshErr);
80
+ throw new SessionExpiredError(cfg.baseUrl, detail);
81
+ }
82
+ if (!session.accessToken) {
83
+ throw new SessionExpiredError(
84
+ cfg.baseUrl,
85
+ "no access token in refresh response",
86
+ );
87
+ }
50
88
  cfg.accessToken = session.accessToken;
51
89
  cfg.refreshToken = session.refreshToken || cfg.refreshToken;
52
90
  cfg.tokenType = session.tokenType || cfg.tokenType || "Bearer";
@@ -84,10 +122,19 @@ function createContext({ out, cwd = process.cwd(), env = process.env } = {}) {
84
122
 
85
123
  // Throws "Not logged in" with a helpful message when no session
86
124
  // exists. Commands that REQUIRE auth call this.
125
+ //
126
+ // v1.8.6 (A1): copy now points operators who don't know the URL
127
+ // at their admin instead of leaving them stuck. This message
128
+ // cascades through 7+ commands (whoami / status / select /
129
+ // credentials / dev / deploy / convex) because the error
130
+ // propagates verbatim through bin/synapse.js's top-level
131
+ // try/catch.
87
132
  get cfg() {
88
133
  const c = cfgLoad();
89
134
  if (!c || !c.baseUrl || !c.accessToken) {
90
- throw new Error("Not logged in. Run `synapse login <url>` first.");
135
+ throw new Error(
136
+ "Not logged in. Run `synapse login <your-synapse-url>` (e.g. `synapse login https://synapsepanel.com`). If you don't know the URL, ask the admin who set up your Synapse host.",
137
+ );
91
138
  }
92
139
  _cfg = c;
93
140
  return c;
@@ -130,4 +177,9 @@ function createContext({ out, cwd = process.cwd(), env = process.env } = {}) {
130
177
  };
131
178
  }
132
179
 
133
- module.exports = { createContext, makeRefreshableApi, normalizeBaseUrl };
180
+ module.exports = {
181
+ createContext,
182
+ makeRefreshableApi,
183
+ normalizeBaseUrl,
184
+ SessionExpiredError,
185
+ };
@@ -117,10 +117,27 @@ async function runConvexCommand(args, ctx) {
117
117
  ctx.out.info(
118
118
  `Using Synapse ${resolved.target} deployment ${resolved.deploymentName}.`,
119
119
  );
120
+ // v1.8.6 (A5): the upstream Convex CLI emits "Can't safely modify
121
+ // .env.local for NEXT_PUBLIC_CONVEX_SITE_URL, please edit manually."
122
+ // because our value is a self-hosted URL that doesn't match its
123
+ // `.convex.site` pattern. The warning is benign (the file IS
124
+ // correct — we wrote it), but it's confusing without context.
125
+ // Pre-announce so the operator knows it's expected.
126
+ ctx.out.info(
127
+ "(npx convex may warn it can't modify NEXT_PUBLIC_CONVEX_SITE_URL — benign; Synapse owns those values.)",
128
+ );
120
129
  } else {
121
130
  resolved = await resolveConvexInvocation(args, { projectDir: ctx.cwd });
122
131
  }
123
132
  const code = await runConvex(resolved.args, { credentials: resolved.credentials });
133
+ // v1.8.6 (A5): when npx convex exits non-zero, surface a hint about
134
+ // where the failure came from — operators see a `[X]` from convex
135
+ // and assume Synapse broke. Point them at the right `--help`.
136
+ if (code !== 0) {
137
+ ctx.out.info(
138
+ `\n(npx convex exited ${code}. If this looks like an unknown-command typo, run \`synapse convex --help\` for the upstream Convex help.)`,
139
+ );
140
+ }
124
141
  process.exitCode = code;
125
142
  }
126
143
 
@@ -63,7 +63,28 @@ Formats:
63
63
  const { format, rest } = parseFormat(args);
64
64
  const deployment = rest[0];
65
65
  if (!deployment) {
66
- throw new Error("Usage: synapse credentials <deployment> [--format env|shell|json]");
66
+ // v1.8.6 (A4): the operator already linked a project — we know
67
+ // which deployment names exist. Surfacing them turns a dead-end
68
+ // Usage line into an actionable hint without an extra API call.
69
+ // The plain Usage line stays as the fallback for unlinked
70
+ // directories.
71
+ const linked = ctx.projectConfig;
72
+ let hint = "";
73
+ if (linked && linked.deployments) {
74
+ const names = [];
75
+ if (linked.deployments.dev?.name) {
76
+ names.push(`dev=${linked.deployments.dev.name}`);
77
+ }
78
+ if (linked.deployments.prod?.name) {
79
+ names.push(`prod=${linked.deployments.prod.name}`);
80
+ }
81
+ if (names.length > 0) {
82
+ hint = `\n\nThis project has: ${names.join(", ")}. Try \`synapse credentials ${linked.deployments.dev?.name || linked.deployments.prod?.name}\`. Run \`synapse status\` to see them all.`;
83
+ }
84
+ }
85
+ throw new Error(
86
+ `Usage: synapse credentials <deployment> [--format env|shell|json]${hint}`,
87
+ );
67
88
  }
68
89
  if (!FORMATS.has(format)) {
69
90
  throw new Error("format must be one of: env, shell, json");
@@ -37,7 +37,20 @@ The saved session carries both the access token (1h TTL) and a refresh token (30
37
37
  });
38
38
  ctx.out.result(
39
39
  { baseUrl, user: session.user || null, configPath: file },
40
- () => ctx.out.info(`Saved Synapse session to ${file}`),
40
+ () => {
41
+ ctx.out.info(`Saved Synapse session to ${file}`);
42
+ // v1.8.6 (A2): post-login next-step hint. Without this, the
43
+ // operator just sees "saved" and stares at the prompt — login
44
+ // is step 1 of a 3-step onboarding (login → select → dev),
45
+ // and the absence of step 2 was the most common first-run
46
+ // dead-end reported.
47
+ ctx.out.info(
48
+ `\nNext step: \`cd\` into your app directory and run \`synapse select\` to link it to a project + deployment.`,
49
+ );
50
+ ctx.out.info(
51
+ `Then run \`synapse doctor\` to confirm everything is healthy, or \`synapse open\` to browse the dashboard.`,
52
+ );
53
+ },
41
54
  );
42
55
  },
43
56
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iann29/synapse",
3
- "version": "1.8.5",
3
+ "version": "1.8.6",
4
4
  "description": "Thin CLI wrapper for using the official Convex CLI with Synapse-managed deployments.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {