convex 1.43.0 → 1.44.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 +130 -113
- package/dist/browser.bundle.js +1 -1
- package/dist/browser.bundle.js.map +1 -1
- package/dist/cjs/cli/codegen_templates/component_server.js +4 -3
- package/dist/cjs/cli/codegen_templates/component_server.js.map +2 -2
- package/dist/cjs/cli/codegen_templates/server.js +24 -12
- package/dist/cjs/cli/codegen_templates/server.js.map +2 -2
- package/dist/cjs/cli/lib/login.js +12 -8
- package/dist/cjs/cli/lib/login.js.map +2 -2
- package/dist/cjs/cli/login.js +52 -7
- package/dist/cjs/cli/login.js.map +2 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/ConvexAuthState.js +30 -32
- package/dist/cjs/react/ConvexAuthState.js.map +2 -2
- package/dist/cjs/server/impl/actions_impl.js +12 -0
- package/dist/cjs/server/impl/actions_impl.js.map +2 -2
- package/dist/cjs/server/index.js +3 -0
- package/dist/cjs/server/index.js.map +2 -2
- package/dist/cjs/server/schema.js +69 -1
- package/dist/cjs/server/schema.js.map +2 -2
- package/dist/cjs-types/cli/codegen_templates/component_server.d.ts.map +1 -1
- package/dist/cjs-types/cli/codegen_templates/server.d.ts +1 -0
- package/dist/cjs-types/cli/codegen_templates/server.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/login.d.ts +5 -0
- package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
- package/dist/cjs-types/cli/login.d.ts.map +1 -1
- package/dist/cjs-types/cli/login.test.d.ts +2 -0
- package/dist/cjs-types/cli/login.test.d.ts.map +1 -0
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cjs-types/react/ConvexAuthState.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/actions_impl.d.ts.map +1 -1
- package/dist/cjs-types/server/impl/actions_impl.test.d.ts +2 -0
- package/dist/cjs-types/server/impl/actions_impl.test.d.ts.map +1 -0
- package/dist/cjs-types/server/index.d.ts +2 -2
- package/dist/cjs-types/server/index.d.ts.map +1 -1
- package/dist/cjs-types/server/schema.d.ts +83 -5
- package/dist/cjs-types/server/schema.d.ts.map +1 -1
- package/dist/cli.bundle.cjs +1277 -570
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/esm/cli/codegen_templates/component_server.js +8 -4
- package/dist/esm/cli/codegen_templates/component_server.js.map +2 -2
- package/dist/esm/cli/codegen_templates/server.js +22 -11
- package/dist/esm/cli/codegen_templates/server.js.map +2 -2
- package/dist/esm/cli/lib/login.js +11 -8
- package/dist/esm/cli/lib/login.js.map +2 -2
- package/dist/esm/cli/login.js +54 -10
- package/dist/esm/cli/login.js.map +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/ConvexAuthState.js +31 -32
- package/dist/esm/react/ConvexAuthState.js.map +2 -2
- package/dist/esm/server/impl/actions_impl.js +11 -0
- package/dist/esm/server/impl/actions_impl.js.map +2 -2
- package/dist/esm/server/index.js +2 -1
- package/dist/esm/server/index.js.map +2 -2
- package/dist/esm/server/schema.js +67 -0
- package/dist/esm/server/schema.js.map +2 -2
- package/dist/esm-types/cli/codegen_templates/component_server.d.ts.map +1 -1
- package/dist/esm-types/cli/codegen_templates/server.d.ts +1 -0
- package/dist/esm-types/cli/codegen_templates/server.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/login.d.ts +5 -0
- package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
- package/dist/esm-types/cli/login.d.ts.map +1 -1
- package/dist/esm-types/cli/login.test.d.ts +2 -0
- package/dist/esm-types/cli/login.test.d.ts.map +1 -0
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/esm-types/react/ConvexAuthState.d.ts.map +1 -1
- package/dist/esm-types/server/impl/actions_impl.d.ts.map +1 -1
- package/dist/esm-types/server/impl/actions_impl.test.d.ts +2 -0
- package/dist/esm-types/server/impl/actions_impl.test.d.ts.map +1 -0
- package/dist/esm-types/server/index.d.ts +2 -2
- package/dist/esm-types/server/index.d.ts.map +1 -1
- package/dist/esm-types/server/schema.d.ts +83 -5
- package/dist/esm-types/server/schema.d.ts.map +1 -1
- package/dist/react.bundle.js +31 -33
- package/dist/react.bundle.js.map +2 -2
- package/package.json +3 -4
- package/src/cli/codegen_templates/component_server.ts +12 -6
- package/src/cli/codegen_templates/server.ts +36 -19
- package/src/cli/lib/login.ts +20 -9
- package/src/cli/login.test.ts +261 -0
- package/src/cli/login.ts +88 -13
- package/src/index.ts +1 -1
- package/src/react/ConvexAuthState.tsx +15 -7
- package/src/server/impl/actions_impl.test.ts +59 -0
- package/src/server/impl/actions_impl.ts +23 -0
- package/src/server/index.ts +7 -1
- package/src/server/schema.test.ts +98 -0
- package/src/server/schema.ts +178 -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.44.0",
|
|
5
5
|
"author": "Convex, Inc. <no-reply@convex.dev>",
|
|
6
6
|
"homepage": "https://convex.dev",
|
|
7
7
|
"repository": {
|
|
@@ -258,19 +258,18 @@
|
|
|
258
258
|
"openapi-typescript-helpers": "~0.1.0",
|
|
259
259
|
"openid-client": "^5.3.1",
|
|
260
260
|
"ora": "^9.0.0",
|
|
261
|
-
"prettier": "3.8.1",
|
|
262
261
|
"react": "^18.0.0",
|
|
263
262
|
"react-dom": "^18.0.0",
|
|
264
263
|
"semver": "^7.6.0",
|
|
265
264
|
"serve-handler": "~6.1.6",
|
|
266
265
|
"shx": "~0.4.0",
|
|
267
266
|
"skott": "~0.35.4",
|
|
268
|
-
"tar": "^7.5.
|
|
267
|
+
"tar": "^7.5.21",
|
|
269
268
|
"tsx": "^4.19.4",
|
|
270
269
|
"typedoc": "^0.24.6",
|
|
271
270
|
"typescript": "~5.0.3",
|
|
272
271
|
"typescript-eslint": "~8.57.0",
|
|
273
|
-
"undici": "7.
|
|
272
|
+
"undici": "7.29.0",
|
|
274
273
|
"vitest": "^4.1.0",
|
|
275
274
|
"wait-for-expect": "~4.0.0",
|
|
276
275
|
"zod": "^3.24.0",
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { header } from "./common.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EnvVarMeta,
|
|
4
|
+
generateEnvInterface,
|
|
5
|
+
withPlatformEnvVars,
|
|
6
|
+
} from "./server.js";
|
|
3
7
|
|
|
4
8
|
export function componentServerTS(
|
|
5
9
|
isRoot: boolean,
|
|
6
10
|
// - undefined: not yet known (initial codegen). Emits an untyped `env` stub.
|
|
7
|
-
// -
|
|
8
|
-
//
|
|
11
|
+
// - array (empty or not): emit a typed `Env` interface and typed `env`
|
|
12
|
+
// export. The interface always includes the platform-provided env vars.
|
|
9
13
|
envVars: EnvVarMeta[] | undefined,
|
|
10
14
|
): string {
|
|
11
|
-
const
|
|
12
|
-
|
|
15
|
+
const emittedEnvVars =
|
|
16
|
+
envVars === undefined ? undefined : withPlatformEnvVars(envVars);
|
|
17
|
+
const hasEnv = emittedEnvVars !== undefined;
|
|
18
|
+
const envInterface = hasEnv ? generateEnvInterface(emittedEnvVars!) : "";
|
|
13
19
|
|
|
14
20
|
const result = `
|
|
15
21
|
${header(
|
|
@@ -113,7 +119,7 @@ export function componentServerTS(
|
|
|
113
119
|
* @returns The wrapped function. Import this function from \`convex/http.js\` and route it to hook it up.
|
|
114
120
|
*/
|
|
115
121
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
|
116
|
-
${hasEnv ? "export const env: Env =
|
|
122
|
+
${hasEnv ? "export const env: Env = (globalThis as unknown as { process: { env: Env } }).process.env;" : "export const env: Record<string, string | undefined> = (globalThis as unknown as { process: { env: Record<string, string | undefined> } }).process.env;"}
|
|
117
123
|
|
|
118
124
|
/**
|
|
119
125
|
* A set of services for use within Convex query functions.
|
|
@@ -6,8 +6,26 @@ export type EnvVarMeta = [
|
|
|
6
6
|
{ type: "value"; value: string; optional?: boolean | undefined },
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
+
// Environment variables the platform always injects into every deployment,
|
|
10
|
+
// independent of the user's `convex.config.ts` declarations. Both are always
|
|
11
|
+
// present, so they're typed as required `string`.
|
|
12
|
+
const PLATFORM_ENV_VARS: EnvVarMeta[] = [
|
|
13
|
+
["CONVEX_CLOUD_URL", { type: "value", value: '{"type":"string"}' }],
|
|
14
|
+
["CONVEX_SITE_URL", { type: "value", value: '{"type":"string"}' }],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Prepend the always-present platform env vars. Users can't declare env vars
|
|
18
|
+
// with these names — the backend rejects overriding built-in system env vars —
|
|
19
|
+
// so there's no collision to worry about.
|
|
20
|
+
export function withPlatformEnvVars(envVars: EnvVarMeta[]): EnvVarMeta[] {
|
|
21
|
+
return [...PLATFORM_ENV_VARS, ...envVars];
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
const ENV_DOCSTRING = `/**
|
|
10
|
-
* Typesafe environment variables
|
|
25
|
+
* Typesafe environment variables.
|
|
26
|
+
*
|
|
27
|
+
* This includes platform-provided env vars and any variables declared in
|
|
28
|
+
* \`convex.config.ts\`.
|
|
11
29
|
*/`;
|
|
12
30
|
|
|
13
31
|
export function serverCodegen({
|
|
@@ -17,42 +35,41 @@ export function serverCodegen({
|
|
|
17
35
|
useTypeScript: boolean;
|
|
18
36
|
// - undefined: env var declarations not yet known (initial codegen before
|
|
19
37
|
// server analysis). Emits an untyped `env` stub so imports don't break.
|
|
20
|
-
// -
|
|
21
|
-
//
|
|
38
|
+
// - array (empty or not): emit a typed `Env` interface and typed `env`
|
|
39
|
+
// export. The interface always includes the platform-provided env vars, so
|
|
40
|
+
// an empty array still yields a typed `env` with just those.
|
|
22
41
|
envVars: EnvVarMeta[] | undefined;
|
|
23
42
|
}) {
|
|
24
|
-
const
|
|
25
|
-
|
|
43
|
+
const emittedEnvVars =
|
|
44
|
+
envVars === undefined ? undefined : withPlatformEnvVars(envVars);
|
|
45
|
+
const hasEnv = emittedEnvVars !== undefined;
|
|
46
|
+
const envInterface = hasEnv ? generateEnvInterface(emittedEnvVars!) : "";
|
|
26
47
|
const envDeclDTS = hasEnv
|
|
27
48
|
? `
|
|
28
49
|
${ENV_DOCSTRING}
|
|
29
50
|
export declare const env: Env;
|
|
30
51
|
`
|
|
31
|
-
:
|
|
32
|
-
? `
|
|
52
|
+
: `
|
|
33
53
|
export declare const env: Record<string, string | undefined>;
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
`;
|
|
55
|
+
// Route through `globalThis` rather than the bare `process` global so the
|
|
56
|
+
// generated file typechecks even in projects without `@types/node`.
|
|
36
57
|
const envDeclTS = hasEnv
|
|
37
58
|
? `
|
|
38
59
|
${ENV_DOCSTRING}
|
|
39
|
-
export const env: Env =
|
|
60
|
+
export const env: Env = (globalThis as unknown as { process: { env: Env } }).process.env;
|
|
40
61
|
`
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
: "";
|
|
62
|
+
: `
|
|
63
|
+
export const env: Record<string, string | undefined> = (globalThis as unknown as { process: { env: Record<string, string | undefined> } }).process.env;
|
|
64
|
+
`;
|
|
46
65
|
const envDeclJS = hasEnv
|
|
47
66
|
? `
|
|
48
67
|
${ENV_DOCSTRING}
|
|
49
68
|
export const env = process.env;
|
|
50
69
|
`
|
|
51
|
-
:
|
|
52
|
-
? `
|
|
70
|
+
: `
|
|
53
71
|
export const env = process.env;
|
|
54
|
-
|
|
55
|
-
: "";
|
|
72
|
+
`;
|
|
56
73
|
|
|
57
74
|
if (!useTypeScript) {
|
|
58
75
|
// Generate separate .js and .d.ts files
|
package/src/cli/lib/login.ts
CHANGED
|
@@ -38,14 +38,14 @@ custom.setHttpOptionsDefaults({
|
|
|
38
38
|
timeout: parseInt(process.env.OPENID_CLIENT_TIMEOUT || "10000"),
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Whether Big Brain accepts this credential. Takes the header explicitly so
|
|
43
|
+
* callers can check a credential other than the one `ctx` resolved to.
|
|
44
|
+
*/
|
|
45
|
+
export async function isAuthorizedHeader(
|
|
42
46
|
ctx: Context,
|
|
43
|
-
|
|
47
|
+
header: string,
|
|
44
48
|
): Promise<boolean> {
|
|
45
|
-
const header = ctx.bigBrainAuth()?.header ?? null;
|
|
46
|
-
if (header === null) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
49
|
try {
|
|
50
50
|
const resp = await fetch(`${provisionHost}/api/authorize`, {
|
|
51
51
|
method: "HEAD",
|
|
@@ -57,9 +57,7 @@ export async function checkAuthorization(
|
|
|
57
57
|
// Don't throw an error if this request returns a non-200 status.
|
|
58
58
|
// Big Brain responds with a variety of error codes -- 401 if the token is correctly-formed but not valid, and either 400 or 500 if the token is ill-formed.
|
|
59
59
|
// We only care if this check returns a 200 code (so we can skip logging in again) -- any other errors should be silently skipped and we'll run the whole login flow again.
|
|
60
|
-
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
60
|
+
return resp.status === 200;
|
|
63
61
|
} catch (e: any) {
|
|
64
62
|
// This `catch` block should only be hit if a network error was encountered
|
|
65
63
|
logError(
|
|
@@ -67,6 +65,19 @@ export async function checkAuthorization(
|
|
|
67
65
|
);
|
|
68
66
|
return await logAndHandleFetchError(ctx, e);
|
|
69
67
|
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function checkAuthorization(
|
|
71
|
+
ctx: Context,
|
|
72
|
+
acceptOptIns: boolean,
|
|
73
|
+
): Promise<boolean> {
|
|
74
|
+
const header = ctx.bigBrainAuth()?.header ?? null;
|
|
75
|
+
if (header === null) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (!(await isAuthorizedHeader(ctx, header))) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
70
81
|
|
|
71
82
|
// Check that we have optin as well
|
|
72
83
|
const shouldContinue = await optins(ctx, acceptOptIns);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { nodeFs } from "../bundler/fs.js";
|
|
3
|
+
import { login } from "./login.js";
|
|
4
|
+
import { bigBrainAPI } from "./lib/utils/utils.js";
|
|
5
|
+
import { readGlobalConfig } from "./lib/utils/globalConfig.js";
|
|
6
|
+
|
|
7
|
+
vi.mock("@sentry/node", () => ({
|
|
8
|
+
captureException: vi.fn(),
|
|
9
|
+
close: vi.fn(),
|
|
10
|
+
}));
|
|
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
|
+
},
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock("./lib/utils/utils.js", async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal<typeof import("./lib/utils/utils.js")>();
|
|
26
|
+
return { ...actual, bigBrainAPI: vi.fn() };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock("./lib/utils/globalConfig.js", async (importOriginal) => {
|
|
30
|
+
const actual =
|
|
31
|
+
await importOriginal<typeof import("./lib/utils/globalConfig.js")>();
|
|
32
|
+
return { ...actual, readGlobalConfig: vi.fn() };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// `initializeBigBrainAuth` loads `.env.local` and `.env` into `process.env`.
|
|
36
|
+
// Tests set those variables directly, and `dotenv.parse` stays real for the
|
|
37
|
+
// file-attribution lookup.
|
|
38
|
+
vi.mock("dotenv", async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal<typeof import("dotenv")>();
|
|
40
|
+
return { ...actual, config: vi.fn() };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ACCOUNT_TOKEN = "account-token";
|
|
44
|
+
const TEAMS = [{ id: 1, name: "Team One", slug: "team-one" }];
|
|
45
|
+
const DEPLOYMENT_KEY = "dev:tall-goat-123|deployment-secret";
|
|
46
|
+
const PROJECT_KEY = "project:my-team:my-project|project-secret";
|
|
47
|
+
|
|
48
|
+
/** Authorization headers Big Brain accepts. Anything else gets a 401. */
|
|
49
|
+
let authorized: Set<string>;
|
|
50
|
+
let mockFetch: ReturnType<typeof vi.fn>;
|
|
51
|
+
|
|
52
|
+
async function runStatus() {
|
|
53
|
+
await login.parseAsync(["status"], { from: "user" });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function output(): string {
|
|
57
|
+
return vi
|
|
58
|
+
.mocked(process.stderr.write)
|
|
59
|
+
.mock.calls.map((call) => String(call[0]))
|
|
60
|
+
.join("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The credentials `/api/authorize` was called with, in order. */
|
|
64
|
+
function authorizeHeaders(): string[] {
|
|
65
|
+
return mockFetch.mock.calls.map(
|
|
66
|
+
(call) => (call[1] as any).headers.Authorization,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setEnvFile(path: string, contents: string) {
|
|
71
|
+
vi.mocked(nodeFs.exists).mockImplementation((p) => p === path);
|
|
72
|
+
vi.mocked(nodeFs.readUtf8File).mockImplementation((p) => {
|
|
73
|
+
if (p !== path) {
|
|
74
|
+
throw new Error(`Unexpected read of ${p}`);
|
|
75
|
+
}
|
|
76
|
+
return contents;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("npx convex login status", () => {
|
|
81
|
+
let savedEnv: NodeJS.ProcessEnv;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
savedEnv = { ...process.env };
|
|
85
|
+
process.env = {};
|
|
86
|
+
|
|
87
|
+
vi.resetAllMocks();
|
|
88
|
+
authorized = new Set([`Bearer ${ACCOUNT_TOKEN}`]);
|
|
89
|
+
mockFetch = vi.fn(async (url: string, options: any) => {
|
|
90
|
+
if (!String(url).endsWith("/api/authorize")) {
|
|
91
|
+
throw new Error(`Unexpected fetch of ${url}`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
status: authorized.has(options.headers.Authorization) ? 200 : 401,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
98
|
+
|
|
99
|
+
vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: ACCOUNT_TOKEN });
|
|
100
|
+
vi.mocked(bigBrainAPI).mockResolvedValue(TEAMS);
|
|
101
|
+
vi.mocked(nodeFs.exists).mockReturnValue(false);
|
|
102
|
+
|
|
103
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
104
|
+
vi.spyOn(process, "exit").mockImplementation((() => {
|
|
105
|
+
throw new Error("process.exit called");
|
|
106
|
+
}) as any);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
process.env = savedEnv;
|
|
111
|
+
vi.unstubAllGlobals();
|
|
112
|
+
vi.restoreAllMocks();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("account status", () => {
|
|
116
|
+
test("reports the account and its teams", async () => {
|
|
117
|
+
await runStatus();
|
|
118
|
+
|
|
119
|
+
expect(output()).toContain("Convex account token found in:");
|
|
120
|
+
expect(output()).toContain("config.json");
|
|
121
|
+
expect(output()).toContain("Status: Logged in");
|
|
122
|
+
expect(output()).toContain("Teams: 1 team accessible");
|
|
123
|
+
expect(output()).toContain("- Team One (team-one)");
|
|
124
|
+
expect(output()).not.toContain("Working Directory:");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("reports no token when the global config is missing", async () => {
|
|
128
|
+
vi.mocked(readGlobalConfig).mockReturnValue(null);
|
|
129
|
+
|
|
130
|
+
await runStatus();
|
|
131
|
+
|
|
132
|
+
expect(output()).toContain("No Convex account token found in:");
|
|
133
|
+
expect(output()).toContain("Status: Not logged in");
|
|
134
|
+
expect(bigBrainAPI).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("reports not logged in when Big Brain rejects the account token", async () => {
|
|
138
|
+
authorized.clear();
|
|
139
|
+
|
|
140
|
+
await runStatus();
|
|
141
|
+
|
|
142
|
+
expect(output()).toContain("Convex account token found in:");
|
|
143
|
+
expect(output()).toContain("Status: Not logged in");
|
|
144
|
+
expect(bigBrainAPI).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("deploy key in the working directory", () => {
|
|
149
|
+
test("reports a valid deployment key, checked without its prefix", async () => {
|
|
150
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
151
|
+
setEnvFile(".env.local", `CONVEX_DEPLOY_KEY=${DEPLOYMENT_KEY}\n`);
|
|
152
|
+
// Big Brain only accepts a deployment key with the `dev:` prefix removed.
|
|
153
|
+
authorized.add("Bearer deployment-secret");
|
|
154
|
+
|
|
155
|
+
await runStatus();
|
|
156
|
+
|
|
157
|
+
expect(output()).toContain(
|
|
158
|
+
"Working Directory: Valid CONVEX_DEPLOY_KEY in .env.local",
|
|
159
|
+
);
|
|
160
|
+
expect(authorizeHeaders()).toContain("Bearer deployment-secret");
|
|
161
|
+
expect(authorizeHeaders()).not.toContain(`Bearer ${DEPLOYMENT_KEY}`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("reports a deployment key Big Brain rejects", async () => {
|
|
165
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
166
|
+
setEnvFile(".env.local", `CONVEX_DEPLOY_KEY=${DEPLOYMENT_KEY}\n`);
|
|
167
|
+
|
|
168
|
+
await runStatus();
|
|
169
|
+
|
|
170
|
+
expect(output()).toContain(
|
|
171
|
+
"Working Directory: Invalid CONVEX_DEPLOY_KEY in .env.local",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("attributes a key that isn't in an env file to the shell", async () => {
|
|
176
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
177
|
+
|
|
178
|
+
await runStatus();
|
|
179
|
+
|
|
180
|
+
expect(output()).toContain(
|
|
181
|
+
"Working Directory: Invalid CONVEX_DEPLOY_KEY in shell environment",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("attributes a key found in .env", async () => {
|
|
186
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
187
|
+
setEnvFile(".env", `CONVEX_DEPLOY_KEY=${DEPLOYMENT_KEY}\n`);
|
|
188
|
+
|
|
189
|
+
await runStatus();
|
|
190
|
+
|
|
191
|
+
expect(output()).toContain(
|
|
192
|
+
"Working Directory: Invalid CONVEX_DEPLOY_KEY in .env",
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("names CONVEX_DEPLOYMENT_TOKEN when that's the variable set", async () => {
|
|
197
|
+
process.env.CONVEX_DEPLOYMENT_TOKEN = DEPLOYMENT_KEY;
|
|
198
|
+
setEnvFile(".env.local", `CONVEX_DEPLOYMENT_TOKEN=${DEPLOYMENT_KEY}\n`);
|
|
199
|
+
authorized.add("Bearer deployment-secret");
|
|
200
|
+
|
|
201
|
+
await runStatus();
|
|
202
|
+
|
|
203
|
+
expect(output()).toContain(
|
|
204
|
+
"Working Directory: Valid CONVEX_DEPLOYMENT_TOKEN in .env.local",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("reports a project key", async () => {
|
|
209
|
+
process.env.CONVEX_DEPLOY_KEY = PROJECT_KEY;
|
|
210
|
+
setEnvFile(".env.local", `CONVEX_DEPLOY_KEY=${PROJECT_KEY}\n`);
|
|
211
|
+
authorized.add("Bearer project-secret");
|
|
212
|
+
|
|
213
|
+
await runStatus();
|
|
214
|
+
|
|
215
|
+
expect(output()).toContain(
|
|
216
|
+
"Working Directory: Valid CONVEX_DEPLOY_KEY in .env.local",
|
|
217
|
+
);
|
|
218
|
+
expect(authorizeHeaders()).toContain("Bearer project-secret");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("stays quiet about a preview key, which never outranks the account", async () => {
|
|
222
|
+
vi.mocked(readGlobalConfig).mockReturnValue(null);
|
|
223
|
+
process.env.CONVEX_DEPLOY_KEY =
|
|
224
|
+
"preview:my-team:my-project|preview-secret";
|
|
225
|
+
|
|
226
|
+
await runStatus();
|
|
227
|
+
|
|
228
|
+
expect(output()).toContain("Status: Not logged in");
|
|
229
|
+
expect(output()).not.toContain("Working Directory:");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("account and deploy key together", () => {
|
|
234
|
+
test("reports both, listing teams for the account", async () => {
|
|
235
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
236
|
+
setEnvFile(".env.local", `CONVEX_DEPLOY_KEY=${DEPLOYMENT_KEY}\n`);
|
|
237
|
+
|
|
238
|
+
await runStatus();
|
|
239
|
+
|
|
240
|
+
expect(output()).toContain("Status: Logged in");
|
|
241
|
+
expect(output()).toContain("Teams: 1 team accessible");
|
|
242
|
+
expect(output()).toContain(
|
|
243
|
+
"Working Directory: Invalid CONVEX_DEPLOY_KEY in .env.local",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("reports the key while logged out", async () => {
|
|
248
|
+
vi.mocked(readGlobalConfig).mockReturnValue(null);
|
|
249
|
+
process.env.CONVEX_DEPLOY_KEY = DEPLOYMENT_KEY;
|
|
250
|
+
setEnvFile(".env.local", `CONVEX_DEPLOY_KEY=${DEPLOYMENT_KEY}\n`);
|
|
251
|
+
|
|
252
|
+
await runStatus();
|
|
253
|
+
|
|
254
|
+
expect(output()).toContain("No Convex account token found in:");
|
|
255
|
+
expect(output()).toContain("Status: Not logged in");
|
|
256
|
+
expect(output()).toContain(
|
|
257
|
+
"Working Directory: Invalid CONVEX_DEPLOY_KEY in .env.local",
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
package/src/cli/login.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Command, Option } from "@commander-js/extra-typings";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
import { BigBrainAuth, Context, oneoffContext } from "../bundler/context.js";
|
|
4
|
+
import { logFinishedStep, logMessage, logWarning } from "../bundler/log.js";
|
|
5
|
+
import {
|
|
6
|
+
checkAuthorization,
|
|
7
|
+
isAuthorizedHeader,
|
|
8
|
+
performLogin,
|
|
9
|
+
} from "./lib/login.js";
|
|
5
10
|
import {
|
|
6
11
|
loadProjectLocalConfig,
|
|
7
12
|
loadUuidForAnonymousUser,
|
|
@@ -16,7 +21,12 @@ import {
|
|
|
16
21
|
teamDashboardUrl,
|
|
17
22
|
} from "./lib/dashboard.js";
|
|
18
23
|
import { promptSearch, promptYesNo } from "./lib/utils/prompts.js";
|
|
19
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
|
|
26
|
+
CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME,
|
|
27
|
+
ENV_VAR_FILE_PATH,
|
|
28
|
+
validateOrSelectTeam,
|
|
29
|
+
} from "./lib/utils/utils.js";
|
|
20
30
|
import {
|
|
21
31
|
selectProject,
|
|
22
32
|
updateEnvAndConfigForDeploymentSelection,
|
|
@@ -35,6 +45,61 @@ import {
|
|
|
35
45
|
} from "./lib/utils/globalConfig.js";
|
|
36
46
|
import { getTeamsForUser } from "./lib/api.js";
|
|
37
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The env file a deploy key came from, or null if it came from the shell.
|
|
50
|
+
* `dotenv` doesn't overwrite variables that are already set, so a value from
|
|
51
|
+
* the shell outranks these files -- match on the value to tell them apart.
|
|
52
|
+
*/
|
|
53
|
+
function deployKeyEnvFile(
|
|
54
|
+
ctx: Context,
|
|
55
|
+
envVarName: string,
|
|
56
|
+
value: string,
|
|
57
|
+
): string | null {
|
|
58
|
+
for (const file of [ENV_VAR_FILE_PATH, ".env"]) {
|
|
59
|
+
if (!ctx.fs.exists(file)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (dotenv.parse(ctx.fs.readUtf8File(file))[envVarName] === value) {
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A project or deployment key in the environment outranks the account token for
|
|
71
|
+
* commands run from this directory, and an expired one makes the CLI look
|
|
72
|
+
* logged out, so report it separately from the account.
|
|
73
|
+
*/
|
|
74
|
+
async function reportDeployKeyInWorkingDirectory(
|
|
75
|
+
ctx: Context,
|
|
76
|
+
auth: BigBrainAuth | null,
|
|
77
|
+
) {
|
|
78
|
+
// A preview deploy key is only used when there's no account token at all.
|
|
79
|
+
if (
|
|
80
|
+
auth === null ||
|
|
81
|
+
auth.kind === "accessToken" ||
|
|
82
|
+
auth.kind === "previewDeployKey"
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const key = auth.kind === "projectKey" ? auth.projectKey : auth.deploymentKey;
|
|
87
|
+
const envVarName = process.env[CONVEX_DEPLOY_KEY_ENV_VAR_NAME]
|
|
88
|
+
? CONVEX_DEPLOY_KEY_ENV_VAR_NAME
|
|
89
|
+
: CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME;
|
|
90
|
+
const envFile = deployKeyEnvFile(ctx, envVarName, key);
|
|
91
|
+
const source = envFile ?? "shell environment";
|
|
92
|
+
// Big Brain only strips a `project:`/`team:` prefix itself, so a deployment
|
|
93
|
+
// key authorizes only without its `dev:`/`prod:` prefix. Split on the first
|
|
94
|
+
// `|` like the server does, so a secret containing one survives.
|
|
95
|
+
const secret = key.slice(key.indexOf("|") + 1);
|
|
96
|
+
if (await isAuthorizedHeader(ctx, `Bearer ${secret}`)) {
|
|
97
|
+
logMessage(`Working Directory: Valid ${envVarName} in ${source}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
logWarning(`Working Directory: Invalid ${envVarName} in ${source}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
38
103
|
const loginStatus = new Command("status")
|
|
39
104
|
.description("Check login status and list accessible teams")
|
|
40
105
|
.allowExcessArguments(false)
|
|
@@ -45,24 +110,33 @@ const loginStatus = new Command("status")
|
|
|
45
110
|
envFile: undefined,
|
|
46
111
|
});
|
|
47
112
|
|
|
48
|
-
|
|
49
|
-
|
|
113
|
+
// `_updateBigBrainAuth` below replaces any deploy key in `ctx` with the
|
|
114
|
+
// account token, so read the deploy key first.
|
|
115
|
+
const auth = ctx.bigBrainAuth();
|
|
50
116
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logMessage("
|
|
117
|
+
const globalConfig = readGlobalConfig(ctx);
|
|
118
|
+
if (globalConfig === null) {
|
|
119
|
+
logMessage(`No Convex account token found in: ${globalConfigPath()}`);
|
|
120
|
+
logMessage("Status: Not logged in");
|
|
121
|
+
await reportDeployKeyInWorkingDirectory(ctx, auth);
|
|
55
122
|
return;
|
|
56
123
|
}
|
|
124
|
+
logMessage(`Convex account token found in: ${globalConfigPath()}`);
|
|
57
125
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (!isLoggedIn) {
|
|
126
|
+
const accessToken = globalConfig.accessToken;
|
|
127
|
+
if (!(await isAuthorizedHeader(ctx, `Bearer ${accessToken}`))) {
|
|
61
128
|
logMessage("Status: Not logged in");
|
|
129
|
+
await reportDeployKeyInWorkingDirectory(ctx, auth);
|
|
62
130
|
return;
|
|
63
131
|
}
|
|
64
132
|
|
|
65
133
|
logMessage("Status: Logged in");
|
|
134
|
+
// Describe the account, even if a deploy key outranks it for other commands.
|
|
135
|
+
ctx._updateBigBrainAuth({
|
|
136
|
+
kind: "accessToken",
|
|
137
|
+
header: `Bearer ${accessToken}`,
|
|
138
|
+
accessToken,
|
|
139
|
+
});
|
|
66
140
|
const teams = await getTeamsForUser(ctx);
|
|
67
141
|
logMessage(
|
|
68
142
|
`Teams: ${teams.length} team${teams.length === 1 ? "" : "s"} accessible`,
|
|
@@ -70,6 +144,7 @@ const loginStatus = new Command("status")
|
|
|
70
144
|
for (const team of teams) {
|
|
71
145
|
logMessage(` - ${team.name} (${team.slug})`);
|
|
72
146
|
}
|
|
147
|
+
await reportDeployKeyInWorkingDirectory(ctx, auth);
|
|
73
148
|
});
|
|
74
149
|
|
|
75
150
|
export const login = new Command("login")
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "1.
|
|
1
|
+
export const version = "1.44.0";
|
|
@@ -3,6 +3,7 @@ import React, {
|
|
|
3
3
|
ReactNode,
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
|
+
useMemo,
|
|
6
7
|
useState,
|
|
7
8
|
} from "react";
|
|
8
9
|
import { AuthTokenFetcher } from "../browser/sync/client.js";
|
|
@@ -129,15 +130,22 @@ export function ConvexProviderWithAuth({
|
|
|
129
130
|
|
|
130
131
|
const isAuthenticated =
|
|
131
132
|
authProviderAuthenticated && (isConvexAuthenticated ?? false);
|
|
133
|
+
const isLoading = isConvexAuthenticated === null;
|
|
134
|
+
const isRefreshingWhileAuthenticated = isRefreshing && isAuthenticated;
|
|
135
|
+
|
|
136
|
+
// Stabilize the context value's identity so `useConvexAuth()` consumers
|
|
137
|
+
// only re-render when the derived auth state actually changes.
|
|
138
|
+
const authState = useMemo<ConvexAuthState>(
|
|
139
|
+
() => ({
|
|
140
|
+
isLoading,
|
|
141
|
+
isAuthenticated,
|
|
142
|
+
isRefreshing: isRefreshingWhileAuthenticated,
|
|
143
|
+
}),
|
|
144
|
+
[isLoading, isAuthenticated, isRefreshingWhileAuthenticated],
|
|
145
|
+
);
|
|
132
146
|
|
|
133
147
|
return (
|
|
134
|
-
<ConvexAuthContext.Provider
|
|
135
|
-
value={{
|
|
136
|
-
isLoading: isConvexAuthenticated === null,
|
|
137
|
-
isAuthenticated,
|
|
138
|
-
isRefreshing: isRefreshing && isAuthenticated,
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
148
|
+
<ConvexAuthContext.Provider value={authState}>
|
|
141
149
|
<ConvexAuthStateFirstEffect
|
|
142
150
|
authProviderAuthenticated={authProviderAuthenticated}
|
|
143
151
|
fetchAccessToken={fetchAccessToken}
|