convex 1.41.0 → 1.42.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/browser.bundle.js +24 -6
  3. package/dist/browser.bundle.js.map +2 -2
  4. package/dist/cjs/browser/simple_client-node.js +85 -27
  5. package/dist/cjs/browser/simple_client-node.js.map +3 -3
  6. package/dist/cjs/browser/sync/authentication_manager.js +21 -4
  7. package/dist/cjs/browser/sync/authentication_manager.js.map +2 -2
  8. package/dist/cjs/browser/sync/client.js +2 -1
  9. package/dist/cjs/browser/sync/client.js.map +2 -2
  10. package/dist/cjs/cli/env.js +12 -5
  11. package/dist/cjs/cli/env.js.map +2 -2
  12. package/dist/cjs/cli/envDefault.js +12 -7
  13. package/dist/cjs/cli/envDefault.js.map +2 -2
  14. package/dist/cjs/cli/insights.js +11 -1
  15. package/dist/cjs/cli/insights.js.map +2 -2
  16. package/dist/cjs/cli/lib/env.js +5 -1
  17. package/dist/cjs/cli/lib/env.js.map +2 -2
  18. package/dist/cjs/cli/lib/login.js +16 -12
  19. package/dist/cjs/cli/lib/login.js.map +2 -2
  20. package/dist/cjs/cli/lib/logs.js +4 -1
  21. package/dist/cjs/cli/lib/logs.js.map +2 -2
  22. package/dist/cjs/cli/lib/run.js +27 -2
  23. package/dist/cjs/cli/lib/run.js.map +2 -2
  24. package/dist/cjs/cli/lib/typecheck.js +28 -10
  25. package/dist/cjs/cli/lib/typecheck.js.map +2 -2
  26. package/dist/cjs/cli/lib/utils/utils.js +30 -0
  27. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  28. package/dist/cjs/cli/program.js +2 -1
  29. package/dist/cjs/cli/program.js.map +2 -2
  30. package/dist/cjs/cli/project.js +27 -0
  31. package/dist/cjs/cli/project.js.map +7 -0
  32. package/dist/cjs/cli/projectCreate.js +90 -0
  33. package/dist/cjs/cli/projectCreate.js.map +7 -0
  34. package/dist/cjs/index.js +1 -1
  35. package/dist/cjs/index.js.map +1 -1
  36. package/dist/cjs/server/database.js.map +1 -1
  37. package/dist/cjs/server/impl/meta_impl.js +6 -6
  38. package/dist/cjs/server/impl/meta_impl.js.map +2 -2
  39. package/dist/cjs/server/impl/registration_impl.js +9 -4
  40. package/dist/cjs/server/impl/registration_impl.js.map +2 -2
  41. package/dist/cjs/server/index.js.map +2 -2
  42. package/dist/cjs/server/meta.js.map +1 -1
  43. package/dist/cjs/server/query.js.map +1 -1
  44. package/dist/cjs/server/registration.js.map +1 -1
  45. package/dist/cjs/server/storage.js.map +1 -1
  46. package/dist/cjs/server/system_fields.js.map +1 -1
  47. package/dist/cjs-types/browser/sync/authentication_manager.d.ts +2 -0
  48. package/dist/cjs-types/browser/sync/authentication_manager.d.ts.map +1 -1
  49. package/dist/cjs-types/browser/sync/client.d.ts +16 -0
  50. package/dist/cjs-types/browser/sync/client.d.ts.map +1 -1
  51. package/dist/cjs-types/cli/env.d.ts.map +1 -1
  52. package/dist/cjs-types/cli/envDefault.d.ts.map +1 -1
  53. package/dist/cjs-types/cli/insights.d.ts +1 -0
  54. package/dist/cjs-types/cli/insights.d.ts.map +1 -1
  55. package/dist/cjs-types/cli/lib/env.d.ts +3 -1
  56. package/dist/cjs-types/cli/lib/env.d.ts.map +1 -1
  57. package/dist/cjs-types/cli/lib/env.test.d.ts +2 -0
  58. package/dist/cjs-types/cli/lib/env.test.d.ts.map +1 -0
  59. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  60. package/dist/cjs-types/cli/lib/logs.d.ts +1 -1
  61. package/dist/cjs-types/cli/lib/logs.d.ts.map +1 -1
  62. package/dist/cjs-types/cli/lib/run.d.ts +1 -1
  63. package/dist/cjs-types/cli/lib/run.d.ts.map +1 -1
  64. package/dist/cjs-types/cli/lib/utils/utils.d.ts +10 -0
  65. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  66. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  67. package/dist/cjs-types/cli/project.d.ts +3 -0
  68. package/dist/cjs-types/cli/project.d.ts.map +1 -0
  69. package/dist/cjs-types/cli/projectCreate.d.ts +5 -0
  70. package/dist/cjs-types/cli/projectCreate.d.ts.map +1 -0
  71. package/dist/cjs-types/index.d.ts +1 -1
  72. package/dist/cjs-types/server/database.d.ts +21 -3
  73. package/dist/cjs-types/server/database.d.ts.map +1 -1
  74. package/dist/cjs-types/server/impl/meta_impl.d.ts.map +1 -1
  75. package/dist/cjs-types/server/impl/registration_impl.d.ts.map +1 -1
  76. package/dist/cjs-types/server/index.d.ts +1 -1
  77. package/dist/cjs-types/server/index.d.ts.map +1 -1
  78. package/dist/cjs-types/server/meta.d.ts +10 -0
  79. package/dist/cjs-types/server/meta.d.ts.map +1 -1
  80. package/dist/cjs-types/server/query.d.ts +1 -1
  81. package/dist/cjs-types/server/registration.d.ts +15 -3
  82. package/dist/cjs-types/server/registration.d.ts.map +1 -1
  83. package/dist/cjs-types/server/storage.d.ts +20 -3
  84. package/dist/cjs-types/server/storage.d.ts.map +1 -1
  85. package/dist/cli.bundle.cjs +1099 -634
  86. package/dist/cli.bundle.cjs.map +4 -4
  87. package/dist/esm/browser/simple_client-node.js +85 -27
  88. package/dist/esm/browser/simple_client-node.js.map +3 -3
  89. package/dist/esm/browser/sync/authentication_manager.js +21 -4
  90. package/dist/esm/browser/sync/authentication_manager.js.map +2 -2
  91. package/dist/esm/browser/sync/client.js +2 -1
  92. package/dist/esm/browser/sync/client.js.map +2 -2
  93. package/dist/esm/cli/env.js +12 -5
  94. package/dist/esm/cli/env.js.map +2 -2
  95. package/dist/esm/cli/envDefault.js +12 -7
  96. package/dist/esm/cli/envDefault.js.map +2 -2
  97. package/dist/esm/cli/insights.js +11 -1
  98. package/dist/esm/cli/insights.js.map +2 -2
  99. package/dist/esm/cli/lib/env.js +5 -1
  100. package/dist/esm/cli/lib/env.js.map +2 -2
  101. package/dist/esm/cli/lib/login.js +18 -13
  102. package/dist/esm/cli/lib/login.js.map +2 -2
  103. package/dist/esm/cli/lib/logs.js +5 -2
  104. package/dist/esm/cli/lib/logs.js.map +2 -2
  105. package/dist/esm/cli/lib/run.js +32 -3
  106. package/dist/esm/cli/lib/run.js.map +2 -2
  107. package/dist/esm/cli/lib/typecheck.js +28 -10
  108. package/dist/esm/cli/lib/typecheck.js.map +3 -3
  109. package/dist/esm/cli/lib/utils/utils.js +29 -0
  110. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  111. package/dist/esm/cli/program.js +2 -1
  112. package/dist/esm/cli/program.js.map +2 -2
  113. package/dist/esm/cli/project.js +5 -0
  114. package/dist/esm/cli/project.js.map +7 -0
  115. package/dist/esm/cli/projectCreate.js +68 -0
  116. package/dist/esm/cli/projectCreate.js.map +7 -0
  117. package/dist/esm/index.js +1 -1
  118. package/dist/esm/index.js.map +1 -1
  119. package/dist/esm/server/impl/meta_impl.js +6 -6
  120. package/dist/esm/server/impl/meta_impl.js.map +2 -2
  121. package/dist/esm/server/impl/registration_impl.js +9 -4
  122. package/dist/esm/server/impl/registration_impl.js.map +2 -2
  123. package/dist/esm/server/index.js.map +2 -2
  124. package/dist/esm-types/browser/sync/authentication_manager.d.ts +2 -0
  125. package/dist/esm-types/browser/sync/authentication_manager.d.ts.map +1 -1
  126. package/dist/esm-types/browser/sync/client.d.ts +16 -0
  127. package/dist/esm-types/browser/sync/client.d.ts.map +1 -1
  128. package/dist/esm-types/cli/env.d.ts.map +1 -1
  129. package/dist/esm-types/cli/envDefault.d.ts.map +1 -1
  130. package/dist/esm-types/cli/insights.d.ts +1 -0
  131. package/dist/esm-types/cli/insights.d.ts.map +1 -1
  132. package/dist/esm-types/cli/lib/env.d.ts +3 -1
  133. package/dist/esm-types/cli/lib/env.d.ts.map +1 -1
  134. package/dist/esm-types/cli/lib/env.test.d.ts +2 -0
  135. package/dist/esm-types/cli/lib/env.test.d.ts.map +1 -0
  136. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  137. package/dist/esm-types/cli/lib/logs.d.ts +1 -1
  138. package/dist/esm-types/cli/lib/logs.d.ts.map +1 -1
  139. package/dist/esm-types/cli/lib/run.d.ts +1 -1
  140. package/dist/esm-types/cli/lib/run.d.ts.map +1 -1
  141. package/dist/esm-types/cli/lib/utils/utils.d.ts +10 -0
  142. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  143. package/dist/esm-types/cli/program.d.ts.map +1 -1
  144. package/dist/esm-types/cli/project.d.ts +3 -0
  145. package/dist/esm-types/cli/project.d.ts.map +1 -0
  146. package/dist/esm-types/cli/projectCreate.d.ts +5 -0
  147. package/dist/esm-types/cli/projectCreate.d.ts.map +1 -0
  148. package/dist/esm-types/index.d.ts +1 -1
  149. package/dist/esm-types/server/database.d.ts +21 -3
  150. package/dist/esm-types/server/database.d.ts.map +1 -1
  151. package/dist/esm-types/server/impl/meta_impl.d.ts.map +1 -1
  152. package/dist/esm-types/server/impl/registration_impl.d.ts.map +1 -1
  153. package/dist/esm-types/server/index.d.ts +1 -1
  154. package/dist/esm-types/server/index.d.ts.map +1 -1
  155. package/dist/esm-types/server/meta.d.ts +10 -0
  156. package/dist/esm-types/server/meta.d.ts.map +1 -1
  157. package/dist/esm-types/server/query.d.ts +1 -1
  158. package/dist/esm-types/server/registration.d.ts +15 -3
  159. package/dist/esm-types/server/registration.d.ts.map +1 -1
  160. package/dist/esm-types/server/storage.d.ts +20 -3
  161. package/dist/esm-types/server/storage.d.ts.map +1 -1
  162. package/dist/react.bundle.js +24 -6
  163. package/dist/react.bundle.js.map +2 -2
  164. package/package.json +4 -4
  165. package/src/browser/sync/authentication_manager.ts +42 -14
  166. package/src/browser/sync/client.ts +17 -0
  167. package/src/cli/env.ts +16 -6
  168. package/src/cli/envDefault.ts +13 -7
  169. package/src/cli/insights.ts +12 -0
  170. package/src/cli/lib/env.test.ts +60 -0
  171. package/src/cli/lib/env.ts +9 -1
  172. package/src/cli/lib/login.ts +18 -18
  173. package/src/cli/lib/logs.ts +7 -2
  174. package/src/cli/lib/run.ts +37 -3
  175. package/src/cli/lib/typecheck.ts +26 -10
  176. package/src/cli/lib/utils/utils.ts +56 -0
  177. package/src/cli/program.ts +2 -0
  178. package/src/cli/project.ts +7 -0
  179. package/src/cli/projectCreate.ts +99 -0
  180. package/src/index.ts +1 -1
  181. package/src/react/ConvexAuthState.test.tsx +145 -0
  182. package/src/server/database.ts +21 -3
  183. package/src/server/impl/meta_impl.ts +7 -6
  184. package/src/server/impl/registration_impl.ts +11 -5
  185. package/src/server/index.ts +1 -0
  186. package/src/server/meta.ts +10 -0
  187. package/src/server/query.ts +1 -1
  188. package/src/server/registration.test.ts +25 -1
  189. package/src/server/registration.ts +16 -1
  190. package/src/server/storage.ts +22 -3
  191. package/src/server/system_fields.ts +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "convex",
3
3
  "description": "Client for the Convex Cloud",
4
- "version": "1.41.0",
4
+ "version": "1.42.1",
5
5
  "author": "Convex, Inc. <no-reply@convex.dev>",
6
6
  "homepage": "https://convex.dev",
7
7
  "repository": {
@@ -171,7 +171,7 @@
171
171
  "dependencies": {
172
172
  "esbuild": "0.27.0",
173
173
  "prettier": "^3.0.0",
174
- "ws": "8.20.1"
174
+ "ws": "8.21.0"
175
175
  },
176
176
  "peerDependencies": {
177
177
  "@auth0/auth0-react": "^2.0.1",
@@ -266,13 +266,13 @@
266
266
  "serve-handler": "~6.1.6",
267
267
  "shx": "~0.4.0",
268
268
  "skott": "~0.35.4",
269
- "tar": "^7.5.11",
269
+ "tar": "^7.5.16",
270
270
  "tsx": "^4.19.4",
271
271
  "typedoc": "^0.24.6",
272
272
  "typescript": "~5.0.3",
273
273
  "typescript-eslint": "~8.57.0",
274
274
  "vitest": "^4.1.0",
275
- "undici": "7.24.0",
275
+ "undici": "7.28.0",
276
276
  "wait-for-expect": "~4.0.0",
277
277
  "zod": "^3.24.0",
278
278
  "zod-to-json-schema": "^3.24.0"
@@ -36,16 +36,19 @@ type AuthConfig = {
36
36
  };
37
37
 
38
38
  /**
39
- * In general we take 3 steps:
40
- * 1. Fetch a possibly cached token
39
+ * By default we take 3 steps:
40
+ * 1. Fetch a possibly cached token and send it to the server
41
41
  * 2. Immediately fetch a fresh token without using a cache
42
42
  * 3. Repeat step 2 before the end of the fresh token's lifetime
43
43
  *
44
- * When we fetch without using a cache we know when the token
45
- * will expire, and can schedule refetching it.
44
+ * When `initialAuthTokenReuse` is enabled, step 2 is skipped: the
45
+ * cached token is reused and a refetch is scheduled based on its
46
+ * remaining lifetime (estimated via the server's clock skew). This
47
+ * avoids a second Authenticate message that would cause the server
48
+ * to re-execute all authenticated queries.
46
49
  *
47
- * If we get an error before a scheduled refetch, we go back
48
- * to step 2.
50
+ * If the server rejects a token before a scheduled refetch, we
51
+ * immediately fetch a fresh token and retry.
49
52
  */
50
53
  type AuthState =
51
54
  | { state: "noAuth" }
@@ -97,6 +100,7 @@ export class AuthenticationManager {
97
100
  private readonly clearAuth: () => void;
98
101
  private readonly logger: Logger;
99
102
  private readonly refreshTokenLeewaySeconds: number;
103
+ private readonly initialAuthTokenReuse: boolean;
100
104
  // Track last value to avoid redundant calls
101
105
  private lastRefreshChange: boolean;
102
106
  // Number of times we have attempted to confirm the latest token. We retry up
@@ -115,6 +119,7 @@ export class AuthenticationManager {
115
119
  config: {
116
120
  refreshTokenLeewaySeconds: number;
117
121
  logger: Logger;
122
+ initialAuthTokenReuse: boolean;
118
123
  },
119
124
  ) {
120
125
  this.syncState = syncState;
@@ -126,6 +131,7 @@ export class AuthenticationManager {
126
131
  this.clearAuth = callbacks.clearAuth;
127
132
  this.logger = config.logger;
128
133
  this.refreshTokenLeewaySeconds = config.refreshTokenLeewaySeconds;
134
+ this.initialAuthTokenReuse = config.initialAuthTokenReuse;
129
135
  this.lastRefreshChange = false;
130
136
  }
131
137
 
@@ -206,7 +212,12 @@ export class AuthenticationManager {
206
212
 
207
213
  if (this.authState.state === "waitingForServerConfirmationOfCachedToken") {
208
214
  this._logVerbose("server confirmed auth token is valid");
209
- void this.refetchToken();
215
+ const cachedToken = this.syncState.getAuth()?.value;
216
+ if (this.initialAuthTokenReuse && cachedToken) {
217
+ this.scheduleTokenRefetch(cachedToken, serverMessage.clientClockSkew);
218
+ } else {
219
+ void this.refetchToken();
220
+ }
210
221
  this.authState.config.onAuthChange(true);
211
222
  return;
212
223
  }
@@ -361,7 +372,7 @@ export class AuthenticationManager {
361
372
  this.tryRestartSocket();
362
373
  }
363
374
 
364
- private scheduleTokenRefetch(token: string) {
375
+ private scheduleTokenRefetch(token: string, clientClockSkewMs?: number) {
365
376
  if (this.authState.state === "noAuth") {
366
377
  return;
367
378
  }
@@ -384,17 +395,34 @@ export class AuthenticationManager {
384
395
  );
385
396
  return;
386
397
  }
387
- // Because the client and server clocks may be out of sync,
388
- // we only know that the token will expire after `exp - iat`,
389
- // and since we just fetched a fresh one we know when that
390
- // will happen.
391
- const tokenValiditySeconds = exp - iat;
392
- if (tokenValiditySeconds <= 2) {
398
+ const fullLifetimeSeconds = exp - iat;
399
+ if (fullLifetimeSeconds <= 2) {
393
400
  this.logger.error(
394
401
  "Auth token does not live long enough, cannot refetch the token",
395
402
  );
396
403
  return;
397
404
  }
405
+ let tokenValiditySeconds: number;
406
+ if (clientClockSkewMs !== undefined) {
407
+ // For cached tokens we use the server's clock skew estimate to
408
+ // compute remaining lifetime. The server computes
409
+ // clientClockSkew = clientTs - serverReceiveTs, so it's positive
410
+ // when the client is ahead and negative when behind. To recover
411
+ // server time: serverNow = Date.now() - clientClockSkew.
412
+ // Both Date.now() and setTimeout use the same client clock, so
413
+ // the scheduled delay is accurate regardless of absolute offset.
414
+ // Connect latency inflates the skew toward "client behind," which
415
+ // is conservative — we schedule the refetch slightly early.
416
+ const estimatedServerNowSeconds = (Date.now() - clientClockSkewMs) / 1000;
417
+ tokenValiditySeconds = exp - estimatedServerNowSeconds;
418
+ if (tokenValiditySeconds <= 0) {
419
+ tokenValiditySeconds = 0;
420
+ }
421
+ } else {
422
+ // For freshly-fetched tokens, `exp - iat` is the full lifetime
423
+ // starting from now.
424
+ tokenValiditySeconds = fullLifetimeSeconds;
425
+ }
398
426
  // Attempt to refresh the token `refreshTokenLeewaySeconds` before it expires,
399
427
  // or immediately if the token is already expiring soon.
400
428
  let delay = Math.min(
@@ -137,6 +137,22 @@ export interface BaseConvexClientOptions {
137
137
  * Defaults to false, not waiting for an auth token.
138
138
  */
139
139
  expectAuth?: boolean;
140
+ /**
141
+ * This API is experimental: it may change or disappear.
142
+ *
143
+ * When true, the client reuses the initial cached auth token instead
144
+ * of immediately fetching a fresh one. This avoids a second
145
+ * Authenticate message that causes the server to re-execute all
146
+ * authenticated queries.
147
+ *
148
+ * The cached token's remaining lifetime is estimated using the
149
+ * server's clock skew measurement, and a refresh is scheduled
150
+ * before it expires.
151
+ *
152
+ * Defaults to false, preserving the original behavior of immediately
153
+ * fetching a fresh token after the cached token is confirmed.
154
+ */
155
+ initialAuthTokenReuse?: boolean;
140
156
  }
141
157
 
142
158
  /**
@@ -358,6 +374,7 @@ export class BaseConvexClient {
358
374
  {
359
375
  logger: this.logger,
360
376
  refreshTokenLeewaySeconds: authRefreshTokenLeewaySeconds,
377
+ initialAuthTokenReuse: options.initialAuthTokenReuse ?? false,
361
378
  },
362
379
  );
363
380
  this.optimisticQueryResults = new OptimisticQueryResults();
package/src/cli/env.ts CHANGED
@@ -164,18 +164,25 @@ const envRemoveCmd = new Command("remove")
164
164
  });
165
165
 
166
166
  const envListCmd = new Command("list")
167
- .summary("List all variables and their values")
167
+ .summary("List all environment variables and their values")
168
168
  .description(
169
169
  [
170
- "• List all variables: `npx convex env list`",
170
+ "• List all variables and their values: `npx convex env list`",
171
+ "• List only variable names (no values): `npx convex env list --names-only`",
171
172
  "• Save all variables to a file: `npx convex env list > .env.convex`",
172
173
  "• Append to a file: `npx convex env list >> .env.convex`",
173
174
  ].join("\n"),
174
175
  )
176
+ .option(
177
+ "--names-only",
178
+ "List only the names of environment variables, without their values",
179
+ )
175
180
  .configureHelp({ showGlobalOptions: true })
176
181
  .allowExcessArguments(false)
177
- .action(async (_options, cmd) => {
178
- const options = cmd.optsWithGlobals();
182
+ .action(async (cmdOptions, cmd) => {
183
+ // Note: We use `as` here because optsWithGlobals() type inference doesn't
184
+ // include global options from the parent command (added via addDeploymentSelectionOptions)
185
+ const options = cmd.optsWithGlobals() as DeploymentSelectionOptions;
179
186
  const { ctx, deployment } = await selectEnvDeployment(options);
180
187
  await ensureHasConvexDependency(ctx, "env list");
181
188
  await withRunningBackend({
@@ -183,7 +190,9 @@ const envListCmd = new Command("list")
183
190
  deployment,
184
191
  action: async () => {
185
192
  const backend = deploymentEnvBackend(ctx, deployment);
186
- await envList(ctx, backend);
193
+ await envList(ctx, backend, {
194
+ namesOnly: cmdOptions.namesOnly ?? false,
195
+ });
187
196
  },
188
197
  });
189
198
  });
@@ -198,7 +207,8 @@ export const env = new Command("env")
198
207
  "• Set interactively: `npx convex env set NAME`",
199
208
  "• Set multiple from file: `npx convex env set --from-file .env`",
200
209
  "• Unset a variable: `npx convex env remove NAME`",
201
- "• List all variables: `npx convex env list`",
210
+ "• List all variables and their values: `npx convex env list`",
211
+ "• List only variable names (no values): `npx convex env list --names-only`",
202
212
  "• Print a variable's value: `npx convex env get NAME`",
203
213
  "",
204
214
  "By default, this sets and views variables on your dev deployment.",
@@ -141,24 +141,29 @@ const envDefaultRemove = addEnvDefaultOptions(
141
141
 
142
142
  const envDefaultList = addEnvDefaultOptions(
143
143
  new Command("list")
144
- .summary("List all default variables")
144
+ .summary("List all default environment variables and their values")
145
145
  .description(
146
146
  [
147
- "List all default variables.",
148
- "",
149
- "• `npx convex env default list`",
147
+ "List all default variables and their values: `npx convex env default list`",
148
+ "• List only default variable names (no values): `npx convex env default list --names-only`",
150
149
  "",
151
150
  "The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.",
152
151
  ].join("\n"),
153
152
  )
153
+ .option(
154
+ "--names-only",
155
+ "List only the names of environment variables, without their values",
156
+ )
154
157
  .configureHelp({ showGlobalOptions: true })
155
158
  .allowExcessArguments(false),
156
- ).action(async (_options, cmd) => {
159
+ ).action(async (cmdOptions, cmd) => {
157
160
  const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
158
161
  EnvDefaultExtraOptions;
159
162
  const { ctx, backend } = await resolveEnvDefaultBackend(options);
160
163
  await ensureHasConvexDependency(ctx, "env default list");
161
- await envList(ctx, backend);
164
+ await envList(ctx, backend, {
165
+ namesOnly: cmdOptions.namesOnly ?? false,
166
+ });
162
167
  });
163
168
 
164
169
  export const envDefault = new Command("default")
@@ -171,7 +176,8 @@ export const envDefault = new Command("default")
171
176
  "",
172
177
  "• Set a default variable: `npx convex env default set NAME 'value'`",
173
178
  "• Unset a default variable: `npx convex env default remove NAME`",
174
- "• List all default variables: `npx convex env default list`",
179
+ "• List all default variables and their values: `npx convex env default list`",
180
+ "• List only default variable names (no values): `npx convex env default list --names-only`",
175
181
  "• Print a default variable's value: `npx convex env default get NAME`",
176
182
  ].join("\n"),
177
183
  )
@@ -101,6 +101,7 @@ export const insights = new Command("insights")
101
101
  )
102
102
  .allowExcessArguments(false)
103
103
  .option("--details", "Show recent events for each insight", false)
104
+ .option("--json", "Output insights as JSON", false)
104
105
  .addDeploymentSelectionOptions(actionDescription("Show insights for"))
105
106
  .showHelpAfterError()
106
107
  .action(async (cmdOptions) => {
@@ -145,6 +146,17 @@ export const insights = new Command("insights")
145
146
  "?view=insights",
146
147
  );
147
148
 
149
+ if (cmdOptions.json) {
150
+ logOutput(
151
+ JSON.stringify(
152
+ { deploymentName, dashboardUrl, insights: insightsList },
153
+ null,
154
+ 2,
155
+ ),
156
+ );
157
+ return;
158
+ }
159
+
148
160
  if (insightsList.length === 0) {
149
161
  logOutput(
150
162
  chalk.green(
@@ -0,0 +1,60 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import type { Context } from "../../bundler/context.js";
3
+ import { envList, type EnvVar, type EnvVarBackend } from "./env.js";
4
+
5
+ const logMocks = vi.hoisted(() => ({
6
+ logFailure: vi.fn(),
7
+ logFinishedStep: vi.fn(),
8
+ logMessage: vi.fn(),
9
+ logOutput: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("../../bundler/log.js", () => ({
13
+ logFailure: logMocks.logFailure,
14
+ logFinishedStep: logMocks.logFinishedStep,
15
+ logMessage: logMocks.logMessage,
16
+ logOutput: logMocks.logOutput,
17
+ }));
18
+
19
+ function fakeBackend(envs: EnvVar[]): EnvVarBackend {
20
+ return {
21
+ get() {
22
+ return Promise.resolve(null);
23
+ },
24
+ list() {
25
+ return Promise.resolve(envs);
26
+ },
27
+ update() {
28
+ return Promise.reject(new Error("Unexpected update"));
29
+ },
30
+ notice: "",
31
+ };
32
+ }
33
+
34
+ describe("envList", () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ test("prints only names without values or warnings when namesOnly is true", async () => {
40
+ await envList(
41
+ {} as Context,
42
+ fakeBackend([
43
+ { name: "PLAIN", value: "plain-value" },
44
+ { name: "DANGEROUS", value: "secret'value#fragment" },
45
+ ]),
46
+ { namesOnly: true },
47
+ );
48
+
49
+ expect(logMocks.logOutput.mock.calls).toEqual([["PLAIN"], ["DANGEROUS"]]);
50
+ expect(logMocks.logMessage).not.toHaveBeenCalled();
51
+
52
+ const emitted = [
53
+ ...logMocks.logOutput.mock.calls.flat(),
54
+ ...logMocks.logMessage.mock.calls.flat(),
55
+ ].join("\n");
56
+ expect(emitted).not.toContain("plain-value");
57
+ expect(emitted).not.toContain("secret'value#fragment");
58
+ expect(emitted).not.toContain("Warning (");
59
+ });
60
+ });
@@ -171,13 +171,21 @@ export async function envRemove(
171
171
  );
172
172
  }
173
173
 
174
- export async function envList(ctx: Context, backend: EnvVarBackend) {
174
+ export async function envList(
175
+ ctx: Context,
176
+ backend: EnvVarBackend,
177
+ options?: { namesOnly?: boolean },
178
+ ) {
175
179
  const envs = await backend.list();
176
180
  if (envs.length === 0) {
177
181
  logMessage(`No environment variables set${backend.notice}`);
178
182
  return;
179
183
  }
180
184
  for (const { name, value } of envs) {
185
+ if (options?.namesOnly) {
186
+ logOutput(name);
187
+ continue;
188
+ }
181
189
  const { formatted, warning } = formatEnvValueForDotfile(value);
182
190
  if (warning) {
183
191
  logMessage(`Warning (${name}): ${warning}`);
@@ -5,6 +5,7 @@ import {
5
5
  logAndHandleFetchError,
6
6
  throwingFetch,
7
7
  isWebContainer,
8
+ typedPlatformClient,
8
9
  } from "./utils/utils.js";
9
10
  import open from "open";
10
11
  import { chalkStderr } from "chalk";
@@ -37,12 +38,6 @@ custom.setHttpOptionsDefaults({
37
38
  timeout: parseInt(process.env.OPENID_CLIENT_TIMEOUT || "10000"),
38
39
  });
39
40
 
40
- interface AuthorizeArgs {
41
- authnToken: string;
42
- deviceName: string;
43
- anonymousId?: string | undefined;
44
- }
45
-
46
41
  export async function checkAuthorization(
47
42
  ctx: Context,
48
43
  acceptOptIns: boolean,
@@ -390,18 +385,23 @@ export async function performLogin(
390
385
  });
391
386
  }
392
387
 
393
- const authorizeArgs: AuthorizeArgs = {
394
- authnToken: accessToken!,
395
- deviceName: deviceName,
396
- anonymousId: anonymousId,
397
- };
398
- const data = await bigBrainAPI({
399
- ctx,
400
- method: "POST",
401
- path: "authorize",
402
- data: authorizeArgs,
388
+ // Exchange the WorkOS access token for a Convex personal access token.
389
+ ctx._updateBigBrainAuth({
390
+ accessToken: accessToken!,
391
+ kind: "accessToken",
392
+ header: `Bearer ${accessToken!}`,
403
393
  });
404
- const globalConfig = { accessToken: data.accessToken };
394
+ const response = await typedPlatformClient(ctx).POST(
395
+ "/create_personal_access_token",
396
+ {
397
+ // `anonymousId` links a prior anonymous session to this account. It's
398
+ // intentionally absent from the generated platform API types,
399
+ // so cast to pass it through.
400
+ body: { name: deviceName, anonymousId } as { name: string },
401
+ },
402
+ );
403
+ const newAccessToken = response.data!.accessToken;
404
+ const globalConfig = { accessToken: newAccessToken };
405
405
  try {
406
406
  await modifyGlobalConfig(ctx, globalConfig);
407
407
  const path = globalConfigPath();
@@ -416,7 +416,7 @@ export async function performLogin(
416
416
  }
417
417
 
418
418
  logVerbose(`performLogin: updating big brain auth after login`);
419
- await updateBigBrainAuthAfterLogin(ctx, data.accessToken);
419
+ await updateBigBrainAuthAfterLogin(ctx, newAccessToken);
420
420
 
421
421
  logVerbose(`performLogin: checking opt ins, acceptOptIns: ${acceptOptIns}`);
422
422
  // Do opt in to TOS and Privacy Policy stuff
@@ -5,7 +5,7 @@ import { nextBackoff } from "./dev.js";
5
5
  import chalk, { chalkStderr } from "chalk";
6
6
  import { stripVTControlCharacters } from "node:util";
7
7
  import { format } from "node:util";
8
- import { deploymentFetch } from "./utils/utils.js";
8
+ import { deploymentFetch, ThrowingFetchError } from "./utils/utils.js";
9
9
  import { FunctionExecution } from "./apiTypes.js";
10
10
 
11
11
  export type LogMode = "always" | "pause-on-deploy" | "disable";
@@ -122,7 +122,12 @@ export async function watchLogs(
122
122
  options?.jsonl,
123
123
  );
124
124
  }
125
- } catch {
125
+ } catch (e) {
126
+ // A 403 means the admin key isn't authorized for this deployment.
127
+ // Retrying won't help, so surface the error and fail immediately.
128
+ if (e instanceof ThrowingFetchError && e.response.status === 403) {
129
+ return await e.handle(ctx);
130
+ }
126
131
  numFailures += 1;
127
132
  }
128
133
  // Handle backoff
@@ -11,7 +11,11 @@ import {
11
11
  import { Value, convexToJson, jsonToConvex } from "../../values/value.js";
12
12
  import { Context, OneoffCtx } from "../../bundler/context.js";
13
13
  import { logFinishedStep, logMessage, logOutput } from "../../bundler/log.js";
14
- import { waitForever, waitUntilCalled } from "./utils/utils.js";
14
+ import {
15
+ permissionDeniedErrorMessage,
16
+ waitForever,
17
+ waitUntilCalled,
18
+ } from "./utils/utils.js";
15
19
  import JSON5 from "json5";
16
20
  import path from "path";
17
21
  import { readProjectConfig } from "./config.js";
@@ -59,6 +63,15 @@ export async function runFunctionAndLog(
59
63
  } catch (err) {
60
64
  const errorMessage = (err as Error).toString().trim();
61
65
 
66
+ const permissionError = permissionDeniedErrorMessage(errorMessage);
67
+ if (permissionError !== null) {
68
+ return await ctx.crash({
69
+ exitCode: 1,
70
+ errorType: "fatal",
71
+ printedMessage: chalkStderr.red(permissionError),
72
+ });
73
+ }
74
+
62
75
  if (errorMessage.includes("Could not find function")) {
63
76
  const functions = (await runSystemQuery(ctx, {
64
77
  deploymentUrl: args.deploymentUrl,
@@ -377,7 +390,7 @@ export async function subscribeAndLog(
377
390
  }
378
391
 
379
392
  export async function subscribe(
380
- _ctx: Context,
393
+ ctx: Context,
381
394
  args: {
382
395
  deploymentUrl: string;
383
396
  adminKey: string;
@@ -399,7 +412,28 @@ export async function subscribe(
399
412
  args.deploymentUrl,
400
413
  (updatedQueries) => {
401
414
  for (const queryToken of updatedQueries) {
402
- args.callbacks?.onChange?.(client.localQueryResultByToken(queryToken)!);
415
+ let result: Value;
416
+ try {
417
+ result = client.localQueryResultByToken(queryToken)!;
418
+ } catch (err) {
419
+ // A failed query (e.g. a permission error) throws here. Surface
420
+ // permission errors cleanly instead of an uncaught stack trace, and
421
+ // let any other error propagate as before.
422
+ const permissionError = permissionDeniedErrorMessage(
423
+ (err as Error).toString(),
424
+ );
425
+ if (permissionError !== null) {
426
+ void ctx.crash({
427
+ exitCode: 1,
428
+ errorType: "fatal",
429
+ printedMessage: chalkStderr.red(permissionError),
430
+ });
431
+ return;
432
+ }
433
+ // eslint-disable-next-line no-restricted-syntax
434
+ throw err;
435
+ }
436
+ args.callbacks?.onChange?.(result);
403
437
  }
404
438
  },
405
439
  {
@@ -139,18 +139,34 @@ async function runTsc(
139
139
  tscArgs: string[],
140
140
  handleResult: TypecheckResultHandler,
141
141
  ): Promise<void> {
142
- // Check if tsc is even installed
143
- const tscPath =
144
- typescriptCompiler === "tsgo"
145
- ? path.join(
142
+ // Check if the TypeScript compiler is even installed
143
+ let compilerPath: string | undefined;
144
+ switch (typescriptCompiler) {
145
+ case "tsgo": {
146
+ // @typescript/native-preview@7.0.0-dev.20260626.1 switched to using `bin/tsgo` instead of `bin/tsgo.js`.
147
+ const tsgoPaths = ["tsgo", "tsgo.js"].map((filename) =>
148
+ path.join(
146
149
  "node_modules",
147
150
  "@typescript",
148
151
  "native-preview",
149
152
  "bin",
150
- "tsgo.js",
151
- )
152
- : path.join("node_modules", "typescript", "bin", "tsc");
153
- if (!ctx.fs.exists(tscPath)) {
153
+ filename,
154
+ ),
155
+ );
156
+ compilerPath = tsgoPaths.find((path) => ctx.fs.exists(path));
157
+ break;
158
+ }
159
+ case "tsc": {
160
+ const tscPath = path.join("node_modules", "typescript", "bin", "tsc");
161
+ if (ctx.fs.exists(tscPath)) {
162
+ compilerPath = tscPath;
163
+ }
164
+ break;
165
+ }
166
+ default:
167
+ typescriptCompiler satisfies never;
168
+ }
169
+ if (!compilerPath) {
154
170
  return handleResult("cantTypeCheck", () => {
155
171
  logError(
156
172
  chalkStderr.gray(
@@ -162,14 +178,14 @@ async function runTsc(
162
178
 
163
179
  // Check the TypeScript version matches the recommendation from Convex
164
180
  const versionResult = await spawnAsync(ctx, process.execPath, [
165
- tscPath,
181
+ compilerPath,
166
182
  "--version",
167
183
  ]);
168
184
 
169
185
  const version = versionResult.stdout.match(/Version (.*)/)?.[1] ?? null;
170
186
  const hasOlderTypeScriptVersion = version && semver.lt(version, "4.8.4");
171
187
 
172
- await runTscInner(ctx, tscPath, tscArgs, handleResult);
188
+ await runTscInner(ctx, compilerPath, tscArgs, handleResult);
173
189
 
174
190
  // Print this warning after any logs from running `tsc`
175
191
  if (hasOlderTypeScriptVersion) {
@@ -101,6 +101,52 @@ export function readDeployKeyFromEnv(
101
101
  return fromDeployKey || fromToken || undefined;
102
102
  }
103
103
 
104
+ // The prefix the backend's `require_operation` checks use when an identity
105
+ // lacks a permission. The full message names the missing permission, e.g.
106
+ // `You do not have permission to perform this operation (deployment:data:view).`
107
+ const PERMISSION_DENIED_MESSAGE_PREFIX =
108
+ "You do not have permission to perform this operation";
109
+
110
+ function deployKeyPermissionAdvice(): string | null {
111
+ // Name whichever env var is actually set. `CONVEX_DEPLOY_KEY` takes
112
+ // precedence over `CONVEX_DEPLOYMENT_TOKEN` when both are present.
113
+ const envVarName = process.env[CONVEX_DEPLOY_KEY_ENV_VAR_NAME]
114
+ ? CONVEX_DEPLOY_KEY_ENV_VAR_NAME
115
+ : process.env[CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME]
116
+ ? CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME
117
+ : null;
118
+ if (envVarName === null) {
119
+ return null;
120
+ }
121
+ return `This is determined by the permissions granted to ${envVarName}.`;
122
+ }
123
+
124
+ /**
125
+ * Detects a server-side authorization failure (a `require_operation` error,
126
+ * surfaced both as an HTTP 403 and as an error from a system UDF query) and
127
+ * produces a clean, single-line message naming the missing permission. When a
128
+ * deploy key is in use, appends advice for configuring its permissions.
129
+ *
130
+ * Returns `null` if `errorMessage` is not a permission error, so callers can
131
+ * fall back to their normal error handling.
132
+ */
133
+ export function permissionDeniedErrorMessage(
134
+ errorMessage: string,
135
+ ): string | null {
136
+ const index = errorMessage.indexOf(PERMISSION_DENIED_MESSAGE_PREFIX);
137
+ if (index === -1) {
138
+ return null;
139
+ }
140
+ // Drop any wrapping context (e.g. the `[CONVEX Q(...)]` prefix) and stack
141
+ // trace, keeping just the permission sentence.
142
+ let msg = errorMessage.slice(index).split("\n")[0].trim();
143
+ const advice = deployKeyPermissionAdvice();
144
+ if (advice) {
145
+ msg = `${msg}\n${advice}`;
146
+ }
147
+ return msg;
148
+ }
149
+
104
150
  export function parsePositiveInteger(value: string) {
105
151
  const parsedValue = parseInteger(value);
106
152
  if (parsedValue <= 0) {
@@ -177,6 +223,16 @@ export class ThrowingFetchError extends Error {
177
223
  } else if (this.response.status === 401) {
178
224
  error_type = "fatal";
179
225
  msg = `${msg}\nAuthenticate with \`npx convex dev\``;
226
+ } else if (this.response.status === 403) {
227
+ error_type = "fatal";
228
+ // Surface the missing permission cleanly (e.g. the server includes the
229
+ // required action like `deployment:data:view`) instead of the noisy
230
+ // "Error fetching ..." wrapper.
231
+ msg = this.serverErrorData?.message ?? msg;
232
+ const advice = deployKeyPermissionAdvice();
233
+ if (advice) {
234
+ msg = `${msg}\n${advice}`;
235
+ }
180
236
  } else if (this.response.status === 404) {
181
237
  error_type = "fatal";
182
238
  msg = `${msg}: ${this.response.url}`;