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.
- package/CHANGELOG.md +38 -0
- package/dist/browser.bundle.js +24 -6
- package/dist/browser.bundle.js.map +2 -2
- package/dist/cjs/browser/simple_client-node.js +85 -27
- package/dist/cjs/browser/simple_client-node.js.map +3 -3
- package/dist/cjs/browser/sync/authentication_manager.js +21 -4
- package/dist/cjs/browser/sync/authentication_manager.js.map +2 -2
- package/dist/cjs/browser/sync/client.js +2 -1
- package/dist/cjs/browser/sync/client.js.map +2 -2
- package/dist/cjs/cli/env.js +12 -5
- package/dist/cjs/cli/env.js.map +2 -2
- package/dist/cjs/cli/envDefault.js +12 -7
- package/dist/cjs/cli/envDefault.js.map +2 -2
- package/dist/cjs/cli/insights.js +11 -1
- package/dist/cjs/cli/insights.js.map +2 -2
- package/dist/cjs/cli/lib/env.js +5 -1
- package/dist/cjs/cli/lib/env.js.map +2 -2
- package/dist/cjs/cli/lib/login.js +16 -12
- package/dist/cjs/cli/lib/login.js.map +2 -2
- package/dist/cjs/cli/lib/logs.js +4 -1
- package/dist/cjs/cli/lib/logs.js.map +2 -2
- package/dist/cjs/cli/lib/run.js +27 -2
- package/dist/cjs/cli/lib/run.js.map +2 -2
- package/dist/cjs/cli/lib/typecheck.js +28 -10
- package/dist/cjs/cli/lib/typecheck.js.map +2 -2
- package/dist/cjs/cli/lib/utils/utils.js +30 -0
- package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
- package/dist/cjs/cli/program.js +2 -1
- package/dist/cjs/cli/program.js.map +2 -2
- package/dist/cjs/cli/project.js +27 -0
- package/dist/cjs/cli/project.js.map +7 -0
- package/dist/cjs/cli/projectCreate.js +90 -0
- package/dist/cjs/cli/projectCreate.js.map +7 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/server/database.js.map +1 -1
- package/dist/cjs/server/impl/meta_impl.js +6 -6
- package/dist/cjs/server/impl/meta_impl.js.map +2 -2
- package/dist/cjs/server/impl/registration_impl.js +9 -4
- package/dist/cjs/server/impl/registration_impl.js.map +2 -2
- package/dist/cjs/server/index.js.map +2 -2
- package/dist/cjs/server/meta.js.map +1 -1
- package/dist/cjs/server/query.js.map +1 -1
- package/dist/cjs/server/registration.js.map +1 -1
- package/dist/cjs/server/storage.js.map +1 -1
- package/dist/cjs/server/system_fields.js.map +1 -1
- package/dist/cjs-types/browser/sync/authentication_manager.d.ts +2 -0
- package/dist/cjs-types/browser/sync/authentication_manager.d.ts.map +1 -1
- package/dist/cjs-types/browser/sync/client.d.ts +16 -0
- package/dist/cjs-types/browser/sync/client.d.ts.map +1 -1
- package/dist/cjs-types/cli/env.d.ts.map +1 -1
- package/dist/cjs-types/cli/envDefault.d.ts.map +1 -1
- package/dist/cjs-types/cli/insights.d.ts +1 -0
- package/dist/cjs-types/cli/insights.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/env.d.ts +3 -1
- package/dist/cjs-types/cli/lib/env.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/env.test.d.ts +2 -0
- package/dist/cjs-types/cli/lib/env.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/logs.d.ts +1 -1
- package/dist/cjs-types/cli/lib/logs.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/run.d.ts +1 -1
- package/dist/cjs-types/cli/lib/run.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/utils/utils.d.ts +10 -0
- package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/cjs-types/cli/program.d.ts.map +1 -1
- package/dist/cjs-types/cli/project.d.ts +3 -0
- package/dist/cjs-types/cli/project.d.ts.map +1 -0
- package/dist/cjs-types/cli/projectCreate.d.ts +5 -0
- package/dist/cjs-types/cli/projectCreate.d.ts.map +1 -0
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cjs-types/server/database.d.ts +21 -3
- package/dist/cjs-types/server/database.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/meta_impl.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/registration_impl.d.ts.map +1 -1
- package/dist/cjs-types/server/index.d.ts +1 -1
- package/dist/cjs-types/server/index.d.ts.map +1 -1
- package/dist/cjs-types/server/meta.d.ts +10 -0
- package/dist/cjs-types/server/meta.d.ts.map +1 -1
- package/dist/cjs-types/server/query.d.ts +1 -1
- package/dist/cjs-types/server/registration.d.ts +15 -3
- package/dist/cjs-types/server/registration.d.ts.map +1 -1
- package/dist/cjs-types/server/storage.d.ts +20 -3
- package/dist/cjs-types/server/storage.d.ts.map +1 -1
- package/dist/cli.bundle.cjs +1099 -634
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/esm/browser/simple_client-node.js +85 -27
- package/dist/esm/browser/simple_client-node.js.map +3 -3
- package/dist/esm/browser/sync/authentication_manager.js +21 -4
- package/dist/esm/browser/sync/authentication_manager.js.map +2 -2
- package/dist/esm/browser/sync/client.js +2 -1
- package/dist/esm/browser/sync/client.js.map +2 -2
- package/dist/esm/cli/env.js +12 -5
- package/dist/esm/cli/env.js.map +2 -2
- package/dist/esm/cli/envDefault.js +12 -7
- package/dist/esm/cli/envDefault.js.map +2 -2
- package/dist/esm/cli/insights.js +11 -1
- package/dist/esm/cli/insights.js.map +2 -2
- package/dist/esm/cli/lib/env.js +5 -1
- package/dist/esm/cli/lib/env.js.map +2 -2
- package/dist/esm/cli/lib/login.js +18 -13
- package/dist/esm/cli/lib/login.js.map +2 -2
- package/dist/esm/cli/lib/logs.js +5 -2
- package/dist/esm/cli/lib/logs.js.map +2 -2
- package/dist/esm/cli/lib/run.js +32 -3
- package/dist/esm/cli/lib/run.js.map +2 -2
- package/dist/esm/cli/lib/typecheck.js +28 -10
- package/dist/esm/cli/lib/typecheck.js.map +3 -3
- package/dist/esm/cli/lib/utils/utils.js +29 -0
- package/dist/esm/cli/lib/utils/utils.js.map +2 -2
- package/dist/esm/cli/program.js +2 -1
- package/dist/esm/cli/program.js.map +2 -2
- package/dist/esm/cli/project.js +5 -0
- package/dist/esm/cli/project.js.map +7 -0
- package/dist/esm/cli/projectCreate.js +68 -0
- package/dist/esm/cli/projectCreate.js.map +7 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/server/impl/meta_impl.js +6 -6
- package/dist/esm/server/impl/meta_impl.js.map +2 -2
- package/dist/esm/server/impl/registration_impl.js +9 -4
- package/dist/esm/server/impl/registration_impl.js.map +2 -2
- package/dist/esm/server/index.js.map +2 -2
- package/dist/esm-types/browser/sync/authentication_manager.d.ts +2 -0
- package/dist/esm-types/browser/sync/authentication_manager.d.ts.map +1 -1
- package/dist/esm-types/browser/sync/client.d.ts +16 -0
- package/dist/esm-types/browser/sync/client.d.ts.map +1 -1
- package/dist/esm-types/cli/env.d.ts.map +1 -1
- package/dist/esm-types/cli/envDefault.d.ts.map +1 -1
- package/dist/esm-types/cli/insights.d.ts +1 -0
- package/dist/esm-types/cli/insights.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/env.d.ts +3 -1
- package/dist/esm-types/cli/lib/env.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/env.test.d.ts +2 -0
- package/dist/esm-types/cli/lib/env.test.d.ts.map +1 -0
- package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/logs.d.ts +1 -1
- package/dist/esm-types/cli/lib/logs.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/run.d.ts +1 -1
- package/dist/esm-types/cli/lib/run.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/utils/utils.d.ts +10 -0
- package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/esm-types/cli/program.d.ts.map +1 -1
- package/dist/esm-types/cli/project.d.ts +3 -0
- package/dist/esm-types/cli/project.d.ts.map +1 -0
- package/dist/esm-types/cli/projectCreate.d.ts +5 -0
- package/dist/esm-types/cli/projectCreate.d.ts.map +1 -0
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/esm-types/server/database.d.ts +21 -3
- package/dist/esm-types/server/database.d.ts.map +1 -1
- package/dist/esm-types/server/impl/meta_impl.d.ts.map +1 -1
- package/dist/esm-types/server/impl/registration_impl.d.ts.map +1 -1
- package/dist/esm-types/server/index.d.ts +1 -1
- package/dist/esm-types/server/index.d.ts.map +1 -1
- package/dist/esm-types/server/meta.d.ts +10 -0
- package/dist/esm-types/server/meta.d.ts.map +1 -1
- package/dist/esm-types/server/query.d.ts +1 -1
- package/dist/esm-types/server/registration.d.ts +15 -3
- package/dist/esm-types/server/registration.d.ts.map +1 -1
- package/dist/esm-types/server/storage.d.ts +20 -3
- package/dist/esm-types/server/storage.d.ts.map +1 -1
- package/dist/react.bundle.js +24 -6
- package/dist/react.bundle.js.map +2 -2
- package/package.json +4 -4
- package/src/browser/sync/authentication_manager.ts +42 -14
- package/src/browser/sync/client.ts +17 -0
- package/src/cli/env.ts +16 -6
- package/src/cli/envDefault.ts +13 -7
- package/src/cli/insights.ts +12 -0
- package/src/cli/lib/env.test.ts +60 -0
- package/src/cli/lib/env.ts +9 -1
- package/src/cli/lib/login.ts +18 -18
- package/src/cli/lib/logs.ts +7 -2
- package/src/cli/lib/run.ts +37 -3
- package/src/cli/lib/typecheck.ts +26 -10
- package/src/cli/lib/utils/utils.ts +56 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/project.ts +7 -0
- package/src/cli/projectCreate.ts +99 -0
- package/src/index.ts +1 -1
- package/src/react/ConvexAuthState.test.tsx +145 -0
- package/src/server/database.ts +21 -3
- package/src/server/impl/meta_impl.ts +7 -6
- package/src/server/impl/registration_impl.ts +11 -5
- package/src/server/index.ts +1 -0
- package/src/server/meta.ts +10 -0
- package/src/server/query.ts +1 -1
- package/src/server/registration.test.ts +25 -1
- package/src/server/registration.ts +16 -1
- package/src/server/storage.ts +22 -3
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
45
|
-
*
|
|
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
|
|
48
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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 (
|
|
178
|
-
|
|
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.",
|
package/src/cli/envDefault.ts
CHANGED
|
@@ -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 (
|
|
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
|
)
|
package/src/cli/insights.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/cli/lib/env.ts
CHANGED
|
@@ -171,13 +171,21 @@ export async function envRemove(
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
export async function envList(
|
|
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}`);
|
package/src/cli/lib/login.ts
CHANGED
|
@@ -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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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,
|
|
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
|
package/src/cli/lib/logs.ts
CHANGED
|
@@ -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
|
package/src/cli/lib/run.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
{
|
package/src/cli/lib/typecheck.ts
CHANGED
|
@@ -139,18 +139,34 @@ async function runTsc(
|
|
|
139
139
|
tscArgs: string[],
|
|
140
140
|
handleResult: TypecheckResultHandler,
|
|
141
141
|
): Promise<void> {
|
|
142
|
-
// Check if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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,
|
|
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}`;
|