convex 1.42.0 → 1.42.2-alpha.0
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 +7 -0
- package/dist/browser.bundle.js +24 -6
- package/dist/browser.bundle.js.map +2 -2
- package/dist/cjs/browser/simple_client.js +2 -2
- package/dist/cjs/browser/simple_client.js.map +2 -2
- 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/dev.js +1 -0
- package/dist/cjs/cli/dev.js.map +2 -2
- package/dist/cjs/cli/generatedApi.js.map +1 -1
- package/dist/cjs/cli/lib/api.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 +4 -12
- package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/client.js +1 -1
- package/dist/cjs/react/client.js.map +2 -2
- package/dist/cjs/react-clerk/ConvexProviderWithClerk.js +2 -1
- package/dist/cjs/react-clerk/ConvexProviderWithClerk.js.map +2 -2
- package/dist/cjs/server/database.js.map +1 -1
- package/dist/cjs/server/impl/meta_impl.js +2 -2
- package/dist/cjs/server/impl/meta_impl.js.map +2 -2
- package/dist/cjs/server/impl/query_impl.js.map +2 -2
- package/dist/cjs/server/impl/registration_impl.js +0 -2
- package/dist/cjs/server/impl/registration_impl.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/schema.js.map +1 -1
- package/dist/cjs-types/browser/simple_client.d.ts +1 -2
- package/dist/cjs-types/browser/simple_client.d.ts.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/dev.d.ts.map +1 -1
- package/dist/cjs-types/cli/dev.test.d.ts +2 -0
- package/dist/cjs-types/cli/dev.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/generatedApi.d.ts +0 -1
- package/dist/cjs-types/cli/generatedApi.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/api.d.ts +0 -1
- package/dist/cjs-types/cli/lib/api.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cjs-types/index.d.ts.map +1 -1
- package/dist/cjs-types/react/client.d.ts.map +1 -1
- package/dist/cjs-types/react/client_provided_base_client.test.d.ts +2 -0
- package/dist/cjs-types/react/client_provided_base_client.test.d.ts.map +1 -0
- package/dist/cjs-types/react-clerk/ConvexProviderWithClerk.d.ts +1 -0
- package/dist/cjs-types/react-clerk/ConvexProviderWithClerk.d.ts.map +1 -1
- package/dist/cjs-types/server/database.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/query_impl.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/registration_impl.d.ts.map +1 -1
- package/dist/cjs-types/server/meta.d.ts.map +1 -1
- package/dist/cjs-types/server/query.d.ts.map +1 -1
- package/dist/cjs-types/server/registration.d.ts.map +1 -1
- package/dist/cjs-types/server/schema.d.ts.map +1 -1
- package/dist/cli.bundle.cjs +58 -28
- package/dist/cli.bundle.cjs.map +3 -3
- package/dist/esm/browser/simple_client.js +5 -3
- package/dist/esm/browser/simple_client.js.map +2 -2
- 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/dev.js +3 -1
- package/dist/esm/cli/dev.js.map +2 -2
- package/dist/esm/cli/lib/api.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 +4 -12
- package/dist/esm/cli/lib/utils/utils.js.map +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/client.js +1 -1
- package/dist/esm/react/client.js.map +2 -2
- package/dist/esm/react-clerk/ConvexProviderWithClerk.js +2 -1
- package/dist/esm/react-clerk/ConvexProviderWithClerk.js.map +2 -2
- package/dist/esm/server/impl/meta_impl.js +2 -2
- package/dist/esm/server/impl/meta_impl.js.map +2 -2
- package/dist/esm/server/impl/query_impl.js.map +2 -2
- package/dist/esm/server/impl/registration_impl.js +0 -2
- package/dist/esm/server/impl/registration_impl.js.map +2 -2
- package/dist/esm/server/schema.js.map +1 -1
- package/dist/esm-types/browser/simple_client.d.ts +1 -2
- package/dist/esm-types/browser/simple_client.d.ts.map +1 -1
- 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/dev.d.ts.map +1 -1
- package/dist/esm-types/cli/dev.test.d.ts +2 -0
- package/dist/esm-types/cli/dev.test.d.ts.map +1 -0
- package/dist/esm-types/cli/generatedApi.d.ts +0 -1
- package/dist/esm-types/cli/generatedApi.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/api.d.ts +0 -1
- package/dist/esm-types/cli/lib/api.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/esm-types/index.d.ts.map +1 -1
- package/dist/esm-types/react/client.d.ts.map +1 -1
- package/dist/esm-types/react/client_provided_base_client.test.d.ts +2 -0
- package/dist/esm-types/react/client_provided_base_client.test.d.ts.map +1 -0
- package/dist/esm-types/react-clerk/ConvexProviderWithClerk.d.ts +1 -0
- package/dist/esm-types/react-clerk/ConvexProviderWithClerk.d.ts.map +1 -1
- package/dist/esm-types/server/database.d.ts.map +1 -1
- package/dist/esm-types/server/impl/query_impl.d.ts.map +1 -1
- package/dist/esm-types/server/impl/registration_impl.d.ts.map +1 -1
- package/dist/esm-types/server/meta.d.ts.map +1 -1
- package/dist/esm-types/server/query.d.ts.map +1 -1
- package/dist/esm-types/server/registration.d.ts.map +1 -1
- package/dist/esm-types/server/schema.d.ts.map +1 -1
- package/dist/react.bundle.js +25 -7
- package/dist/react.bundle.js.map +2 -2
- package/package.json +2 -2
- package/src/browser/simple_client.ts +12 -10
- package/src/browser/sync/authentication_manager.ts +42 -14
- package/src/browser/sync/client.ts +17 -0
- package/src/cli/dev.test.ts +157 -0
- package/src/cli/dev.ts +7 -0
- package/src/cli/generatedApi.ts +0 -1
- package/src/cli/lib/api.ts +0 -1
- package/src/cli/lib/config.test.ts +1 -4
- package/src/cli/lib/typecheck.ts +26 -10
- package/src/cli/lib/utils/utils.ts +4 -12
- package/src/index.ts +1 -1
- package/src/react/ConvexAuthState.test.tsx +145 -0
- package/src/react/client.ts +23 -6
- package/src/react/client_provided_base_client.test.tsx +56 -0
- package/src/react-clerk/ConvexProviderWithClerk.tsx +3 -1
- package/src/server/database.ts +6 -4
- package/src/server/impl/meta_impl.ts +2 -2
- package/src/server/impl/query_impl.ts +1 -3
- package/src/server/impl/registration_impl.ts +0 -2
- package/src/server/meta.ts +10 -0
- package/src/server/query.ts +9 -6
- package/src/server/registration.ts +10 -10
- package/src/server/schema.ts +3 -2
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.42.0",
|
|
4
|
+
"version": "1.42.2-alpha.0",
|
|
5
5
|
"author": "Convex, Inc. <no-reply@convex.dev>",
|
|
6
6
|
"homepage": "https://convex.dev",
|
|
7
7
|
"repository": {
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
"fetch-retry": "~6.0.0",
|
|
249
249
|
"find-up": "^8.0.0",
|
|
250
250
|
"globals": "~17.4.0",
|
|
251
|
-
"happy-dom": "~20.
|
|
251
|
+
"happy-dom": "~20.10.0",
|
|
252
252
|
"jsdom": "~28.1.0",
|
|
253
253
|
"json5": "~2.2.3",
|
|
254
254
|
"knip": "~5.86.0",
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { validateDeploymentUrl } from "../common/index.js";
|
|
2
|
-
import {
|
|
3
|
-
BaseConvexClient,
|
|
4
|
-
BaseConvexClientOptions,
|
|
5
|
-
MutationOptions,
|
|
6
|
-
PaginatedQueryToken,
|
|
7
|
-
QueryToken,
|
|
8
|
-
UserIdentityAttributes,
|
|
9
|
-
} from "./index.js";
|
|
10
2
|
import {
|
|
11
3
|
FunctionArgs,
|
|
12
4
|
FunctionReference,
|
|
@@ -15,13 +7,23 @@ import {
|
|
|
15
7
|
} from "../server/index.js";
|
|
16
8
|
import { getFunctionName } from "../server/api.js";
|
|
17
9
|
import { AuthTokenFetcher } from "./sync/authentication_manager.js";
|
|
18
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BaseConvexClient,
|
|
12
|
+
BaseConvexClientOptions,
|
|
13
|
+
ConnectionState,
|
|
14
|
+
MutationOptions,
|
|
15
|
+
} from "./sync/client.js";
|
|
19
16
|
import {
|
|
20
17
|
ExtendedTransition,
|
|
21
18
|
PaginatedQueryClient,
|
|
22
19
|
} from "./sync/paginated_query_client.js";
|
|
23
20
|
import { PaginatedQueryResult } from "./sync/pagination.js";
|
|
24
|
-
import {
|
|
21
|
+
import { UserIdentityAttributes } from "./sync/protocol.js";
|
|
22
|
+
import {
|
|
23
|
+
PaginatedQueryToken,
|
|
24
|
+
QueryToken,
|
|
25
|
+
serializedQueryTokenIsPaginated,
|
|
26
|
+
} from "./sync/udf_path_utils.js";
|
|
25
27
|
|
|
26
28
|
// In Node.js builds this points to a bundled WebSocket implementation. If no
|
|
27
29
|
// WebSocket implementation is manually specified or globally available,
|
|
@@ -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();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { nodeFs } from "../bundler/fs.js";
|
|
4
|
+
import { dev } from "./dev.js";
|
|
5
|
+
import { deploymentCredentialsOrConfigure } from "./configure.js";
|
|
6
|
+
|
|
7
|
+
// In-memory filesystem — populated in beforeEach.
|
|
8
|
+
let testFiles: Map<string, string>;
|
|
9
|
+
// Everything written to stderr during a test, so we can assert on error output.
|
|
10
|
+
let stderr: string;
|
|
11
|
+
|
|
12
|
+
vi.mock("../bundler/fs.js", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("../bundler/fs.js")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
nodeFs: {
|
|
17
|
+
...actual.nodeFs,
|
|
18
|
+
exists: vi.fn(),
|
|
19
|
+
readUtf8File: vi.fn(),
|
|
20
|
+
writeUtf8File: vi.fn(),
|
|
21
|
+
mkdir: vi.fn(),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// `deploymentCredentialsOrConfigure` is where the CLI prompts the user to
|
|
27
|
+
// create or select a project. We stub it out so we can assert *whether* it gets
|
|
28
|
+
// reached — the whole point of the fast-fail check is that it should not.
|
|
29
|
+
vi.mock("./configure.js", async (importOriginal) => {
|
|
30
|
+
const actual = await importOriginal<typeof import("./configure.js")>();
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
deploymentCredentialsOrConfigure: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Avoid actually pushing if the action ever gets that far.
|
|
38
|
+
vi.mock("./lib/dev.js", async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal<typeof import("./lib/dev.js")>();
|
|
40
|
+
return {
|
|
41
|
+
...actual,
|
|
42
|
+
devAgainstDeployment: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
vi.mock("dotenv", async (importOriginal) => {
|
|
47
|
+
const actual = await importOriginal<typeof import("dotenv")>();
|
|
48
|
+
return {
|
|
49
|
+
...actual,
|
|
50
|
+
config: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
vi.mock("@sentry/node", () => ({
|
|
55
|
+
captureException: vi.fn(),
|
|
56
|
+
captureMessage: vi.fn(),
|
|
57
|
+
close: vi.fn(),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
describe("npx convex dev", () => {
|
|
61
|
+
let savedEnv: NodeJS.ProcessEnv;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
savedEnv = { ...process.env };
|
|
65
|
+
process.env = {};
|
|
66
|
+
|
|
67
|
+
testFiles = new Map();
|
|
68
|
+
stderr = "";
|
|
69
|
+
|
|
70
|
+
vi.resetAllMocks();
|
|
71
|
+
|
|
72
|
+
vi.mocked(nodeFs.exists).mockImplementation((p: string) =>
|
|
73
|
+
testFiles.has(path.resolve(p)),
|
|
74
|
+
);
|
|
75
|
+
vi.mocked(nodeFs.readUtf8File).mockImplementation((p: string) => {
|
|
76
|
+
const content = testFiles.get(path.resolve(p));
|
|
77
|
+
if (content === undefined) {
|
|
78
|
+
const err: any = new Error(
|
|
79
|
+
`ENOENT: no such file or directory, open '${p}'`,
|
|
80
|
+
);
|
|
81
|
+
err.code = "ENOENT";
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return content;
|
|
85
|
+
});
|
|
86
|
+
vi.mocked(nodeFs.writeUtf8File).mockImplementation(
|
|
87
|
+
(p: string, content: string) => {
|
|
88
|
+
testFiles.set(path.resolve(p), content);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// `crash` calls `process.exit`; make it throw so `parseAsync` rejects.
|
|
93
|
+
vi.spyOn(process, "exit").mockImplementation((() => {
|
|
94
|
+
throw new Error("the CLI tried to exit the process");
|
|
95
|
+
}) as any);
|
|
96
|
+
vi.spyOn(process.stderr, "write").mockImplementation((chunk: any) => {
|
|
97
|
+
stderr += chunk.toString();
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
process.env = savedEnv;
|
|
104
|
+
vi.restoreAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("fails fast without prompting to create/select a project when run outside a Convex project", async () => {
|
|
108
|
+
// A package.json with no `convex` dependency: this directory is not a
|
|
109
|
+
// Convex project.
|
|
110
|
+
testFiles.set(
|
|
111
|
+
path.resolve("package.json"),
|
|
112
|
+
JSON.stringify({ name: "not-a-convex-app", dependencies: {} }),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await expect(dev.parseAsync([], { from: "user" })).rejects.toThrow(
|
|
116
|
+
"the CLI tried to exit the process",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// The error must mention how to fix it...
|
|
120
|
+
expect(stderr).toContain("add `convex` to your package.json");
|
|
121
|
+
// ...and crucially, it must happen *before* we ever ask the user to create
|
|
122
|
+
// or select a project.
|
|
123
|
+
expect(deploymentCredentialsOrConfigure).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("fails fast when there is no package.json at all", async () => {
|
|
127
|
+
// Empty directory: no package.json.
|
|
128
|
+
await expect(dev.parseAsync([], { from: "user" })).rejects.toThrow(
|
|
129
|
+
"the CLI tried to exit the process",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(deploymentCredentialsOrConfigure).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("proceeds to project configuration once the Convex dependency check passes", async () => {
|
|
136
|
+
testFiles.set(
|
|
137
|
+
path.resolve("package.json"),
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
name: "convex-app",
|
|
140
|
+
dependencies: { convex: "^1.0.0" },
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Short-circuit right at the project-selection step so we don't run the
|
|
145
|
+
// rest of the dev flow; reaching this proves the dependency check passed.
|
|
146
|
+
const sentinel = new Error("reached project configuration");
|
|
147
|
+
vi.mocked(deploymentCredentialsOrConfigure).mockRejectedValue(sentinel);
|
|
148
|
+
|
|
149
|
+
// `--configure` skips deployment selection and goes straight to
|
|
150
|
+
// `deploymentCredentialsOrConfigure`.
|
|
151
|
+
await expect(
|
|
152
|
+
dev.parseAsync(["--configure"], { from: "user" }),
|
|
153
|
+
).rejects.toThrow("reached project configuration");
|
|
154
|
+
|
|
155
|
+
expect(deploymentCredentialsOrConfigure).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
package/src/cli/dev.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { devAgainstDeployment } from "./lib/dev.js";
|
|
|
9
9
|
import {
|
|
10
10
|
CONVEX_DEPLOYMENT_ENV_VAR_NAME,
|
|
11
11
|
CONVEX_SELF_HOSTED_URL_VAR_NAME,
|
|
12
|
+
ensureHasConvexDependency,
|
|
12
13
|
} from "./lib/utils/utils.js";
|
|
13
14
|
import {
|
|
14
15
|
getDeploymentSelection,
|
|
@@ -210,6 +211,12 @@ Same format as .env.local or .env files, and overrides them.`,
|
|
|
210
211
|
|
|
211
212
|
const devOptions = await normalizeDevOptions(ctx, cmdOptions);
|
|
212
213
|
|
|
214
|
+
// Check that we're in a Convex project (a package.json with a `convex`
|
|
215
|
+
// dependency) before doing anything else, so we fail fast with a clear
|
|
216
|
+
// error instead of prompting to create/select a project and only then
|
|
217
|
+
// crashing because there's nothing to push.
|
|
218
|
+
await ensureHasConvexDependency(ctx, "run `npx convex dev`");
|
|
219
|
+
|
|
213
220
|
if (cmdOptions.configure === undefined) {
|
|
214
221
|
if (cmdOptions.team || cmdOptions.project || cmdOptions.devDeployment)
|
|
215
222
|
return await ctx.crash({
|
package/src/cli/generatedApi.ts
CHANGED
|
@@ -405,7 +405,6 @@ export interface components {
|
|
|
405
405
|
createTime: number;
|
|
406
406
|
devDeploymentName?: string | null;
|
|
407
407
|
id: components["schemas"]["ProjectId"];
|
|
408
|
-
isDemo: boolean;
|
|
409
408
|
name: components["schemas"]["ProjectName"];
|
|
410
409
|
prodDeploymentName?: string | null;
|
|
411
410
|
slug: components["schemas"]["ProjectSlug"];
|
package/src/cli/lib/api.ts
CHANGED
|
@@ -257,10 +257,7 @@ test("parseProjectConfig - top-level validation", async () => {
|
|
|
257
257
|
null,
|
|
258
258
|
"✖ Expected `convex.json` to contain an object\n",
|
|
259
259
|
);
|
|
260
|
-
await assertParseError(
|
|
261
|
-
[],
|
|
262
|
-
"✖ Expected `convex.json` to contain an object\n",
|
|
263
|
-
);
|
|
260
|
+
await assertParseError([], "✖ Expected `convex.json` to contain an object\n");
|
|
264
261
|
});
|
|
265
262
|
|
|
266
263
|
test("ensureConvexFunctionsDir - creates functions directory", async () => {
|
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) {
|
|
@@ -602,18 +602,10 @@ export async function validateOrSelectProject(
|
|
|
602
602
|
});
|
|
603
603
|
}
|
|
604
604
|
if (!projectSlug) {
|
|
605
|
-
const nonDemoProjects = projects.filter((project) => !project.isDemo);
|
|
606
|
-
if (nonDemoProjects.length === 0) {
|
|
607
|
-
return await ctx.crash({
|
|
608
|
-
exitCode: 1,
|
|
609
|
-
errorType: "fatal",
|
|
610
|
-
printedMessage: `No existing non-demo projects! Run this command again and choose "create a new project."`,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
605
|
// Prompt the user to select project.
|
|
614
|
-
switch (
|
|
606
|
+
switch (projects.length) {
|
|
615
607
|
case 1: {
|
|
616
|
-
const project =
|
|
608
|
+
const project = projects[0];
|
|
617
609
|
const confirmed = await promptYesNo(ctx, {
|
|
618
610
|
message: `${singleProjectPrompt} ${project.name} (${project.slug})?`,
|
|
619
611
|
});
|
|
@@ -621,12 +613,12 @@ export async function validateOrSelectProject(
|
|
|
621
613
|
if (!confirmed) {
|
|
622
614
|
return null;
|
|
623
615
|
}
|
|
624
|
-
return
|
|
616
|
+
return projects[0].slug;
|
|
625
617
|
}
|
|
626
618
|
default:
|
|
627
619
|
return await promptSearch(ctx, {
|
|
628
620
|
message: multiProjectPrompt,
|
|
629
|
-
choices:
|
|
621
|
+
choices: projects.map((project: Project) => ({
|
|
630
622
|
name: `${project.name} (${project.slug})`,
|
|
631
623
|
value: project.slug,
|
|
632
624
|
})),
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "1.42.0";
|
|
1
|
+
export const version = "1.42.2-alpha.0";
|
|
@@ -198,6 +198,7 @@ test("Tokens are used to schedule refetch", async () => {
|
|
|
198
198
|
function mockServerConfirmsAuth(
|
|
199
199
|
client: ConvexReactClient,
|
|
200
200
|
oldIdentityVersion: number,
|
|
201
|
+
clientClockSkew?: number,
|
|
201
202
|
) {
|
|
202
203
|
act(() => {
|
|
203
204
|
const querySetVersion = client.sync["remoteQuerySet"]["version"];
|
|
@@ -212,6 +213,7 @@ function mockServerConfirmsAuth(
|
|
|
212
213
|
identity: oldIdentityVersion + 1,
|
|
213
214
|
},
|
|
214
215
|
modifications: [],
|
|
216
|
+
...(clientClockSkew !== undefined ? { clientClockSkew } : {}),
|
|
215
217
|
});
|
|
216
218
|
});
|
|
217
219
|
}
|
|
@@ -539,3 +541,146 @@ test("stop() during reauth notifies isRefreshing false", async () => {
|
|
|
539
541
|
|
|
540
542
|
expect(onRefreshChange).toHaveBeenCalledWith(false);
|
|
541
543
|
});
|
|
544
|
+
|
|
545
|
+
// --- initialAuthTokenReuse tests ---
|
|
546
|
+
// When enabled, the cached token is reused after server confirmation
|
|
547
|
+
// (no immediate fresh-token fetch), and the refetch is scheduled using
|
|
548
|
+
// the server's clientClockSkew to estimate remaining lifetime.
|
|
549
|
+
// The server computes clientClockSkew = clientTs - serverReceiveTs:
|
|
550
|
+
// negative means client behind, positive means ahead. Connect latency
|
|
551
|
+
// makes the skew more negative (conservative).
|
|
552
|
+
|
|
553
|
+
test("initialAuthTokenReuse: sends only one Authenticate", async () => {
|
|
554
|
+
const client = new ConvexReactClient("https://127.0.0.1:3001", {
|
|
555
|
+
initialAuthTokenReuse: true,
|
|
556
|
+
});
|
|
557
|
+
const tokenLifetimeSeconds = 60;
|
|
558
|
+
let tokenId = 0;
|
|
559
|
+
const tokenFetcher = vi.fn(async () =>
|
|
560
|
+
jwtEncode(
|
|
561
|
+
{ iat: 1234500, exp: 1234500 + tokenLifetimeSeconds },
|
|
562
|
+
"secret" + tokenId++,
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const sendMessageSpy = vi.spyOn(
|
|
567
|
+
client.sync["webSocketManager"],
|
|
568
|
+
"sendMessage",
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const onAuthChange = vi.fn();
|
|
572
|
+
void client.setAuth(tokenFetcher, onAuthChange);
|
|
573
|
+
|
|
574
|
+
await flushPromises();
|
|
575
|
+
mockServerConfirmsAuth(client, 0);
|
|
576
|
+
await flushPromises();
|
|
577
|
+
|
|
578
|
+
expect(onAuthChange).toHaveBeenCalledWith(true);
|
|
579
|
+
|
|
580
|
+
const authenticateMessages = sendMessageSpy.mock.calls.filter(
|
|
581
|
+
([msg]) => msg.type === "Authenticate",
|
|
582
|
+
);
|
|
583
|
+
expect(authenticateMessages).toHaveLength(1);
|
|
584
|
+
|
|
585
|
+
await client.close();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("initialAuthTokenReuse: clock skew adjusts schedule for partially-expired token", async () => {
|
|
589
|
+
const client = new ConvexReactClient("https://127.0.0.1:3001", {
|
|
590
|
+
initialAuthTokenReuse: true,
|
|
591
|
+
});
|
|
592
|
+
const defaultLeewaySeconds = 10;
|
|
593
|
+
const tokenLifetimeSeconds = 60;
|
|
594
|
+
const tokenAgeOnServer = 30;
|
|
595
|
+
const clientClockSkewMs = 0;
|
|
596
|
+
const clientNow = Date.now() / 1000;
|
|
597
|
+
const serverNow = clientNow;
|
|
598
|
+
const tokenFetcher = vi.fn(async () =>
|
|
599
|
+
jwtEncode(
|
|
600
|
+
{
|
|
601
|
+
iat: serverNow - tokenAgeOnServer,
|
|
602
|
+
exp: serverNow - tokenAgeOnServer + tokenLifetimeSeconds,
|
|
603
|
+
},
|
|
604
|
+
"secret",
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
void client.setAuth(tokenFetcher, () => {});
|
|
608
|
+
|
|
609
|
+
await flushPromises();
|
|
610
|
+
mockServerConfirmsAuth(client, 0, clientClockSkewMs);
|
|
611
|
+
|
|
612
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(1);
|
|
613
|
+
|
|
614
|
+
const expectedDelay =
|
|
615
|
+
(tokenLifetimeSeconds - tokenAgeOnServer - defaultLeewaySeconds) * 1000;
|
|
616
|
+
|
|
617
|
+
vi.advanceTimersByTime(expectedDelay - 1);
|
|
618
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(1);
|
|
619
|
+
|
|
620
|
+
vi.advanceTimersByTime(1);
|
|
621
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(2);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("initialAuthTokenReuse: clock skew triggers immediate refetch for nearly-expired token", async () => {
|
|
625
|
+
const client = new ConvexReactClient("https://127.0.0.1:3001", {
|
|
626
|
+
initialAuthTokenReuse: true,
|
|
627
|
+
});
|
|
628
|
+
const tokenLifetimeSeconds = 60;
|
|
629
|
+
const clientClockSkewMs = -50_000;
|
|
630
|
+
const clientNow = Date.now() / 1000;
|
|
631
|
+
const serverNow = clientNow - clientClockSkewMs / 1000;
|
|
632
|
+
const tokenFetcher = vi.fn(async () =>
|
|
633
|
+
jwtEncode(
|
|
634
|
+
{
|
|
635
|
+
iat: serverNow - 55,
|
|
636
|
+
exp: serverNow - 55 + tokenLifetimeSeconds,
|
|
637
|
+
},
|
|
638
|
+
"secret",
|
|
639
|
+
),
|
|
640
|
+
);
|
|
641
|
+
void client.setAuth(tokenFetcher, () => {});
|
|
642
|
+
|
|
643
|
+
await flushPromises();
|
|
644
|
+
mockServerConfirmsAuth(client, 0, clientClockSkewMs);
|
|
645
|
+
|
|
646
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(1);
|
|
647
|
+
|
|
648
|
+
vi.advanceTimersByTime(1);
|
|
649
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(2);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("initialAuthTokenReuse: clock skew corrects for client clock behind server", async () => {
|
|
653
|
+
const client = new ConvexReactClient("https://127.0.0.1:3001", {
|
|
654
|
+
initialAuthTokenReuse: true,
|
|
655
|
+
});
|
|
656
|
+
const defaultLeewaySeconds = 10;
|
|
657
|
+
const tokenLifetimeSeconds = 60;
|
|
658
|
+
const tokenAgeOnServer = 30;
|
|
659
|
+
const clientClockSkewMs = -20_000;
|
|
660
|
+
const clientNow = Date.now() / 1000;
|
|
661
|
+
const serverNow = clientNow - clientClockSkewMs / 1000;
|
|
662
|
+
const tokenFetcher = vi.fn(async () =>
|
|
663
|
+
jwtEncode(
|
|
664
|
+
{
|
|
665
|
+
iat: serverNow - tokenAgeOnServer,
|
|
666
|
+
exp: serverNow - tokenAgeOnServer + tokenLifetimeSeconds,
|
|
667
|
+
},
|
|
668
|
+
"secret",
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
void client.setAuth(tokenFetcher, () => {});
|
|
672
|
+
|
|
673
|
+
await flushPromises();
|
|
674
|
+
mockServerConfirmsAuth(client, 0, clientClockSkewMs);
|
|
675
|
+
|
|
676
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(1);
|
|
677
|
+
|
|
678
|
+
const expectedDelay =
|
|
679
|
+
(tokenLifetimeSeconds - tokenAgeOnServer - defaultLeewaySeconds) * 1000;
|
|
680
|
+
|
|
681
|
+
vi.advanceTimersByTime(expectedDelay - 1);
|
|
682
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(1);
|
|
683
|
+
|
|
684
|
+
vi.advanceTimersByTime(1);
|
|
685
|
+
expect(tokenFetcher).toHaveBeenCalledTimes(2);
|
|
686
|
+
});
|