@prisma/cli 3.0.0-beta.2 → 3.0.0-beta.20
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/README.md +5 -16
- package/dist/adapters/git.js +8 -3
- package/dist/adapters/local-state.js +26 -7
- package/dist/adapters/mock-api.js +81 -2
- package/dist/adapters/token-storage.js +344 -25
- package/dist/cli.js +18 -3
- package/dist/cli2.js +12 -4
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +90 -61
- package/dist/commands/auth/index.js +55 -2
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +159 -0
- package/dist/commands/env.js +8 -4
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/project/index.js +1 -1
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +54 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +286 -131
- package/dist/controllers/app.js +849 -309
- package/dist/controllers/auth.js +255 -11
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +318 -0
- package/dist/controllers/project.js +156 -87
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/lib/agent/cli-command.js +17 -0
- package/dist/lib/agent/constants.js +12 -0
- package/dist/lib/agent/package-manager.js +99 -0
- package/dist/lib/agent/setup-status.js +83 -0
- package/dist/lib/app/app-interaction.js +5 -0
- package/dist/lib/app/{preview-provider.js → app-provider.js} +220 -104
- package/dist/lib/app/branch-database-api.js +102 -0
- package/dist/lib/app/branch-database-deploy.js +326 -0
- package/dist/lib/app/branch-database.js +216 -0
- package/dist/lib/app/build-settings.js +93 -0
- package/dist/lib/app/build.js +82 -0
- package/dist/lib/app/bun-project.js +15 -9
- package/dist/lib/app/compute-config.js +145 -0
- package/dist/lib/app/deploy-plan.js +59 -0
- package/dist/lib/app/{preview-progress.js → deploy-progress.js} +12 -12
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +82 -0
- package/dist/lib/app/env-vars.js +28 -2
- package/dist/lib/app/local-dev.js +8 -49
- package/dist/lib/app/production-deploy-gate.js +162 -0
- package/dist/lib/app/read-branch.js +30 -0
- package/dist/lib/auth/auth-ops.js +38 -23
- package/dist/lib/auth/guard.js +6 -2
- package/dist/lib/auth/login.js +117 -15
- package/dist/lib/database/provider.js +167 -0
- package/dist/lib/diagnostics.js +15 -0
- package/dist/lib/fs/home-path.js +24 -0
- package/dist/lib/git/local-branch.js +53 -0
- package/dist/lib/git/local-status.js +57 -0
- package/dist/lib/project/interactive-setup.js +2 -1
- package/dist/lib/project/local-pin.js +183 -34
- package/dist/lib/project/resolution.js +206 -50
- package/dist/lib/project/setup.js +64 -18
- package/dist/output/patterns.js +1 -1
- package/dist/presenters/agent.js +74 -0
- package/dist/presenters/app-env.js +149 -14
- package/dist/presenters/app.js +187 -26
- package/dist/presenters/auth.js +99 -2
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/database.js +274 -0
- package/dist/presenters/project.js +44 -26
- package/dist/presenters/verbose-context.js +64 -0
- package/dist/shell/cli-command.js +12 -0
- package/dist/shell/command-arguments.js +7 -1
- package/dist/shell/command-meta.js +243 -15
- package/dist/shell/command-runner.js +60 -21
- package/dist/shell/diagnostics-output.js +57 -0
- package/dist/shell/errors.js +61 -1
- package/dist/shell/help.js +31 -20
- package/dist/shell/output.js +10 -1
- package/dist/shell/prompt.js +7 -3
- package/dist/shell/runtime.js +10 -6
- package/dist/shell/ui.js +42 -3
- package/dist/shell/update-check.js +247 -0
- package/dist/use-cases/auth.js +68 -1
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/package.json +27 -10
- package/dist/lib/app/preview-build.js +0 -284
- package/dist/lib/app/preview-interaction.js +0 -5
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
1
|
+
import { resolveBunEntrypoint } from "./bun-project.js";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
//#region src/lib/app/local-dev.ts
|
|
6
|
-
const NEXT_CONFIG_FILENAMES = [
|
|
7
|
-
"next.config.js",
|
|
8
|
-
"next.config.mjs",
|
|
9
|
-
"next.config.ts",
|
|
10
|
-
"next.config.mts"
|
|
11
|
-
];
|
|
12
6
|
const DEFAULT_LOCAL_DEV_PORT = 3e3;
|
|
13
|
-
async function resolveLocalBuildType(appPath, buildType) {
|
|
14
|
-
if (buildType === "bun" || buildType === "nextjs") return buildType;
|
|
15
|
-
if (buildType !== "auto") return null;
|
|
16
|
-
return detectLocalBuildType(appPath);
|
|
17
|
-
}
|
|
18
|
-
async function detectLocalBuildType(appPath) {
|
|
19
|
-
if (await isNextProject(appPath)) return "nextjs";
|
|
20
|
-
if (await isBunProject(appPath)) return "bun";
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
7
|
async function runLocalApp(options) {
|
|
24
8
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
25
9
|
if (options.buildType === "nextjs") {
|
|
@@ -58,7 +42,8 @@ async function runLocalApp(options) {
|
|
|
58
42
|
env: {
|
|
59
43
|
...options.env,
|
|
60
44
|
PORT: String(options.port)
|
|
61
|
-
}
|
|
45
|
+
},
|
|
46
|
+
signal: options.signal
|
|
62
47
|
}, spawnImpl, "Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available.");
|
|
63
48
|
return {
|
|
64
49
|
framework: "nextjs",
|
|
@@ -69,7 +54,7 @@ async function runLocalApp(options) {
|
|
|
69
54
|
signal: command.signal
|
|
70
55
|
};
|
|
71
56
|
}
|
|
72
|
-
const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
|
|
57
|
+
const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal);
|
|
73
58
|
const command = await runWithFallback([{
|
|
74
59
|
command: "bun",
|
|
75
60
|
args: ["--watch", entrypoint],
|
|
@@ -79,7 +64,8 @@ async function runLocalApp(options) {
|
|
|
79
64
|
env: {
|
|
80
65
|
...options.env,
|
|
81
66
|
PORT: String(options.port)
|
|
82
|
-
}
|
|
67
|
+
},
|
|
68
|
+
signal: options.signal
|
|
83
69
|
}, spawnImpl, "Bun is required to run this app locally. Install it from https://bun.sh.");
|
|
84
70
|
return {
|
|
85
71
|
framework: "bun",
|
|
@@ -90,33 +76,6 @@ async function runLocalApp(options) {
|
|
|
90
76
|
signal: command.signal
|
|
91
77
|
};
|
|
92
78
|
}
|
|
93
|
-
async function isNextProject(appPath) {
|
|
94
|
-
for (const fileName of NEXT_CONFIG_FILENAMES) try {
|
|
95
|
-
await access(path.join(appPath, fileName));
|
|
96
|
-
return true;
|
|
97
|
-
} catch {}
|
|
98
|
-
return hasDependency(await readBunPackageJson(appPath), "next");
|
|
99
|
-
}
|
|
100
|
-
async function isBunProject(appPath) {
|
|
101
|
-
try {
|
|
102
|
-
await access(path.join(appPath, "bun.lock"));
|
|
103
|
-
return true;
|
|
104
|
-
} catch {}
|
|
105
|
-
try {
|
|
106
|
-
await access(path.join(appPath, "bun.lockb"));
|
|
107
|
-
return true;
|
|
108
|
-
} catch {}
|
|
109
|
-
const packageJson = await readBunPackageJson(appPath);
|
|
110
|
-
if (!packageJson) return false;
|
|
111
|
-
const hasEntrypoint = typeof readBunPackageEntrypoint(packageJson) === "string";
|
|
112
|
-
const hasBunDependency = hasDependency(packageJson, "@types/bun") || hasDependency(packageJson, "bun");
|
|
113
|
-
const usesBunScripts = (typeof packageJson.scripts === "object" && packageJson.scripts !== null ? Object.values(packageJson.scripts) : []).some((value) => typeof value === "string" && /\bbun\b/.test(value));
|
|
114
|
-
return hasEntrypoint && (hasBunDependency || usesBunScripts);
|
|
115
|
-
}
|
|
116
|
-
function hasDependency(packageJson, dependencyName) {
|
|
117
|
-
if (!packageJson) return false;
|
|
118
|
-
return [packageJson.dependencies, packageJson.devDependencies].some((group) => typeof group === "object" && group !== null && dependencyName in group);
|
|
119
|
-
}
|
|
120
79
|
async function runWithFallback(candidates, options, spawnImpl, missingCommandMessage) {
|
|
121
80
|
for (const candidate of candidates) try {
|
|
122
81
|
const result = await spawnCommand(candidate, options, spawnImpl);
|
|
@@ -147,4 +106,4 @@ function spawnCommand(candidate, options, spawnImpl) {
|
|
|
147
106
|
});
|
|
148
107
|
}
|
|
149
108
|
//#endregion
|
|
150
|
-
export { DEFAULT_LOCAL_DEV_PORT,
|
|
109
|
+
export { DEFAULT_LOCAL_DEV_PORT, runLocalApp };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
import { canPrompt } from "../../shell/runtime.js";
|
|
3
|
+
import { confirmPrompt } from "../../shell/prompt.js";
|
|
4
|
+
//#region src/lib/app/production-deploy-gate.ts
|
|
5
|
+
async function enforceProductionDeployGate(context, provider, options) {
|
|
6
|
+
if (options.branchKind !== "production") return { firstProductionDeploy: false };
|
|
7
|
+
if (!options.appId) {
|
|
8
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
9
|
+
return { firstProductionDeploy: true };
|
|
10
|
+
}
|
|
11
|
+
const currentLiveDeployment = resolveCurrentProductionDeployment(await provider.listDeployments(options.appId).catch((error) => {
|
|
12
|
+
throw productionDeployInspectionFailedError(error);
|
|
13
|
+
}));
|
|
14
|
+
if (!currentLiveDeployment) {
|
|
15
|
+
renderFirstProductionDeployLine(context, options.appName);
|
|
16
|
+
return { firstProductionDeploy: true };
|
|
17
|
+
}
|
|
18
|
+
if (!options.prod) throw productionDeployRequiresFlagError();
|
|
19
|
+
if (context.flags.yes) {
|
|
20
|
+
renderProductionDeployYesLine(context);
|
|
21
|
+
return { firstProductionDeploy: false };
|
|
22
|
+
}
|
|
23
|
+
if (!canPrompt(context)) throw productionDeployConfirmationRequiredError(options.appName);
|
|
24
|
+
renderProductionDeployConfirmation(context, currentLiveDeployment);
|
|
25
|
+
if (!await confirmPrompt({
|
|
26
|
+
input: context.runtime.stdin,
|
|
27
|
+
output: context.output.stderr,
|
|
28
|
+
signal: context.runtime.signal,
|
|
29
|
+
message: "Deploy to production?",
|
|
30
|
+
initialValue: false
|
|
31
|
+
})) throw productionDeployCancelledError();
|
|
32
|
+
return { firstProductionDeploy: false };
|
|
33
|
+
}
|
|
34
|
+
function resolveCurrentProductionDeployment(result) {
|
|
35
|
+
if (result.deployments.length === 0) return null;
|
|
36
|
+
if (result.app.liveDeploymentId) {
|
|
37
|
+
const live = result.deployments.find((deployment) => deployment.id === result.app.liveDeploymentId);
|
|
38
|
+
if (live) return live;
|
|
39
|
+
}
|
|
40
|
+
return result.deployments.find((deployment) => deployment.live === true) ?? result.deployments[0] ?? null;
|
|
41
|
+
}
|
|
42
|
+
function renderFirstProductionDeployLine(context, appName) {
|
|
43
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
44
|
+
context.output.stderr.write(`First deploy of "${appName}" -- promoting to production.\n\n`);
|
|
45
|
+
}
|
|
46
|
+
function renderProductionDeployYesLine(context) {
|
|
47
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
48
|
+
context.output.stderr.write("Deploying to production (--prod --yes).\n\n");
|
|
49
|
+
}
|
|
50
|
+
function renderProductionDeployConfirmation(context, currentLiveDeployment) {
|
|
51
|
+
if (context.flags.json || context.flags.quiet) return;
|
|
52
|
+
const lines = [
|
|
53
|
+
"This will deploy to production and replace the live deployment.",
|
|
54
|
+
"",
|
|
55
|
+
` Current live: ${currentLiveDeployment.id} deployed ${formatDeploymentAge(currentLiveDeployment.createdAt)}`,
|
|
56
|
+
" New deploy: will be built from your local code",
|
|
57
|
+
""
|
|
58
|
+
];
|
|
59
|
+
context.output.stderr.write(`${lines.join("\n")}\n`);
|
|
60
|
+
}
|
|
61
|
+
function formatDeploymentAge(createdAt) {
|
|
62
|
+
const createdAtMs = Date.parse(createdAt);
|
|
63
|
+
if (!Number.isFinite(createdAtMs)) return createdAt;
|
|
64
|
+
const elapsedMs = Math.max(0, Date.now() - createdAtMs);
|
|
65
|
+
for (const unit of [
|
|
66
|
+
{
|
|
67
|
+
label: "day",
|
|
68
|
+
ms: 1440 * 60 * 1e3
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "hour",
|
|
72
|
+
ms: 3600 * 1e3
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "minute",
|
|
76
|
+
ms: 60 * 1e3
|
|
77
|
+
}
|
|
78
|
+
]) if (elapsedMs >= unit.ms) {
|
|
79
|
+
const value = Math.floor(elapsedMs / unit.ms);
|
|
80
|
+
return `${value} ${unit.label}${value === 1 ? "" : "s"} ago`;
|
|
81
|
+
}
|
|
82
|
+
return "less than a minute ago";
|
|
83
|
+
}
|
|
84
|
+
function productionDeployRequiresFlagError() {
|
|
85
|
+
return new CliError({
|
|
86
|
+
code: "PROD_DEPLOY_REQUIRES_FLAG",
|
|
87
|
+
domain: "app",
|
|
88
|
+
summary: "Production deploy requires --prod",
|
|
89
|
+
why: "The resolved Branch is production and this App already has a production deployment.",
|
|
90
|
+
fix: "Re-run with --prod, or deploy from a preview Branch.",
|
|
91
|
+
exitCode: 2,
|
|
92
|
+
nextActions: [{
|
|
93
|
+
kind: "run-command",
|
|
94
|
+
journey: "deploy-app",
|
|
95
|
+
label: "Deploy to production",
|
|
96
|
+
command: "prisma-cli app deploy --prod"
|
|
97
|
+
}, {
|
|
98
|
+
kind: "run-command",
|
|
99
|
+
journey: "deploy-app",
|
|
100
|
+
label: "Create a preview branch",
|
|
101
|
+
commands: ["git checkout -b <branch-name>", "prisma-cli app deploy"]
|
|
102
|
+
}],
|
|
103
|
+
humanLines: [
|
|
104
|
+
"This would deploy to production.",
|
|
105
|
+
"",
|
|
106
|
+
"Production deploys require explicit intent. Re-run with:",
|
|
107
|
+
"",
|
|
108
|
+
" prisma-cli app deploy --prod",
|
|
109
|
+
"",
|
|
110
|
+
"Or deploy a preview from a feature branch:",
|
|
111
|
+
"",
|
|
112
|
+
" git checkout -b <branch-name>",
|
|
113
|
+
" prisma-cli app deploy"
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function productionDeployConfirmationRequiredError(appName) {
|
|
118
|
+
return new CliError({
|
|
119
|
+
code: "CONFIRMATION_REQUIRED",
|
|
120
|
+
domain: "app",
|
|
121
|
+
summary: "Production deploy requires confirmation in the current mode",
|
|
122
|
+
why: "This command cannot prompt for production deploy confirmation in the current mode.",
|
|
123
|
+
fix: `Pass --prod --yes to confirm deployment of "${appName}" to production.`,
|
|
124
|
+
exitCode: 1,
|
|
125
|
+
nextSteps: ["prisma-cli app deploy --prod --yes"],
|
|
126
|
+
nextActions: [{
|
|
127
|
+
kind: "run-command",
|
|
128
|
+
journey: "deploy-app",
|
|
129
|
+
label: "Deploy to production non-interactively",
|
|
130
|
+
command: "prisma-cli app deploy --prod --yes"
|
|
131
|
+
}]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function productionDeployCancelledError() {
|
|
135
|
+
return new CliError({
|
|
136
|
+
code: "CONFIRMATION_REQUIRED",
|
|
137
|
+
domain: "app",
|
|
138
|
+
summary: "Production deploy cancelled",
|
|
139
|
+
why: null,
|
|
140
|
+
fix: null,
|
|
141
|
+
exitCode: 0,
|
|
142
|
+
humanLines: ["Cancelled."]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function productionDeployInspectionFailedError(error) {
|
|
146
|
+
return new CliError({
|
|
147
|
+
code: "DEPLOY_FAILED",
|
|
148
|
+
domain: "app",
|
|
149
|
+
summary: "Failed to inspect production deployments",
|
|
150
|
+
why: error instanceof Error ? error.message : String(error),
|
|
151
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
152
|
+
debug: formatDebugDetails(error),
|
|
153
|
+
exitCode: 1,
|
|
154
|
+
nextSteps: ["prisma-cli app list-deploys"]
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function formatDebugDetails(error) {
|
|
158
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
159
|
+
return typeof error === "string" ? error : null;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { enforceProductionDeployGate };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/lib/app/read-branch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the branch an app management command should read from, without ever
|
|
4
|
+
* creating one. Returns the branch whose `gitName` matches `branchName`, else
|
|
5
|
+
* the project's default branch, else null when the project has no branches.
|
|
6
|
+
*/
|
|
7
|
+
async function resolveReadBranch(client, options) {
|
|
8
|
+
const branches = [];
|
|
9
|
+
let cursor;
|
|
10
|
+
do {
|
|
11
|
+
const result = await client.GET("/v1/projects/{projectId}/branches", {
|
|
12
|
+
params: {
|
|
13
|
+
path: { projectId: options.projectId },
|
|
14
|
+
query: { cursor }
|
|
15
|
+
},
|
|
16
|
+
signal: options.signal
|
|
17
|
+
});
|
|
18
|
+
if (result.error || !result.data) throw new Error(`Failed to list branches for project ${options.projectId}: ${JSON.stringify(result.error)}`);
|
|
19
|
+
branches.push(...result.data.data);
|
|
20
|
+
cursor = result.data.pagination.hasMore ? result.data.pagination.nextCursor ?? void 0 : void 0;
|
|
21
|
+
} while (cursor);
|
|
22
|
+
const chosen = branches.find((branch) => branch.gitName === options.branchName) ?? branches.find((branch) => branch.isDefault) ?? null;
|
|
23
|
+
return chosen ? {
|
|
24
|
+
id: chosen.id,
|
|
25
|
+
name: chosen.gitName,
|
|
26
|
+
kind: chosen.role
|
|
27
|
+
} : null;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { resolveReadBranch };
|
|
@@ -24,20 +24,22 @@ function workspaceIdFromClaims(claims) {
|
|
|
24
24
|
const id = sub.slice(10).trim();
|
|
25
25
|
return id.length > 0 ? id : null;
|
|
26
26
|
}
|
|
27
|
-
async function performLogin(env) {
|
|
27
|
+
async function performLogin(env, signal) {
|
|
28
28
|
await login({
|
|
29
|
-
tokenStorage: new FileTokenStorage(env),
|
|
30
|
-
env
|
|
29
|
+
tokenStorage: new FileTokenStorage(env, signal, { activateOnSetTokens: true }),
|
|
30
|
+
env,
|
|
31
|
+
signal
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
|
-
async function readAuthState(env) {
|
|
34
|
+
async function readAuthState(env, signal) {
|
|
34
35
|
const rawServiceToken = env[SERVICE_TOKEN_ENV_VAR];
|
|
35
36
|
if (rawServiceToken !== void 0) {
|
|
36
37
|
const serviceToken = rawServiceToken.trim();
|
|
37
38
|
if (serviceToken.length === 0) throw new Error(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
|
|
38
|
-
return readServiceTokenAuthState(serviceToken, env);
|
|
39
|
+
return readServiceTokenAuthState(serviceToken, env, signal);
|
|
39
40
|
}
|
|
40
|
-
const
|
|
41
|
+
const tokenStorage = new FileTokenStorage(env, signal);
|
|
42
|
+
const tokens = await tokenStorage.getTokens();
|
|
41
43
|
if (!tokens) return {
|
|
42
44
|
authenticated: false,
|
|
43
45
|
provider: null,
|
|
@@ -45,20 +47,26 @@ async function readAuthState(env) {
|
|
|
45
47
|
workspace: null,
|
|
46
48
|
credential: null
|
|
47
49
|
};
|
|
48
|
-
const client = await requireComputeAuth(env);
|
|
49
|
-
const currentPrincipal = await readCurrentPrincipalAuthState(client);
|
|
50
|
-
if (currentPrincipal)
|
|
50
|
+
const client = await requireComputeAuth(env, signal);
|
|
51
|
+
const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
|
|
52
|
+
if (currentPrincipal) {
|
|
53
|
+
if (currentPrincipal.authenticated && currentPrincipal.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, currentPrincipal.workspace);
|
|
54
|
+
return currentPrincipal;
|
|
55
|
+
}
|
|
51
56
|
const claims = decodeJwtPayload(tokens.accessToken);
|
|
52
|
-
|
|
57
|
+
const authState = await buildAuthState({
|
|
53
58
|
workspaceIdFromCredential: tokens.workspaceId,
|
|
54
59
|
claims,
|
|
55
60
|
env,
|
|
56
|
-
client
|
|
61
|
+
client,
|
|
62
|
+
signal
|
|
57
63
|
});
|
|
64
|
+
if (authState.authenticated && authState.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, authState.workspace);
|
|
65
|
+
return authState;
|
|
58
66
|
}
|
|
59
|
-
async function readServiceTokenAuthState(token, env) {
|
|
60
|
-
const client = await requireComputeAuth(env);
|
|
61
|
-
const currentPrincipal = await readCurrentPrincipalAuthState(client);
|
|
67
|
+
async function readServiceTokenAuthState(token, env, signal) {
|
|
68
|
+
const client = await requireComputeAuth(env, signal);
|
|
69
|
+
const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
|
|
62
70
|
if (currentPrincipal) return currentPrincipal;
|
|
63
71
|
const claims = decodeJwtPayload(token);
|
|
64
72
|
const workspaceId = workspaceIdFromClaims(claims);
|
|
@@ -73,15 +81,19 @@ async function readServiceTokenAuthState(token, env) {
|
|
|
73
81
|
workspaceIdFromCredential: workspaceId,
|
|
74
82
|
claims,
|
|
75
83
|
env,
|
|
76
|
-
client
|
|
84
|
+
client,
|
|
85
|
+
signal
|
|
77
86
|
});
|
|
78
87
|
}
|
|
79
|
-
async function buildAuthState({ workspaceIdFromCredential, claims, env, client }) {
|
|
88
|
+
async function buildAuthState({ workspaceIdFromCredential, claims, env, client, signal }) {
|
|
80
89
|
let workspaceId = workspaceIdFromCredential;
|
|
81
90
|
let workspaceName = workspaceIdFromCredential;
|
|
82
|
-
client ??= await requireComputeAuth(env);
|
|
91
|
+
client ??= await requireComputeAuth(env, signal);
|
|
83
92
|
if (client) try {
|
|
84
|
-
const { data, response } = await client.GET("/v1/workspaces/{id}", {
|
|
93
|
+
const { data, response } = await client.GET("/v1/workspaces/{id}", {
|
|
94
|
+
params: { path: { id: workspaceIdFromCredential } },
|
|
95
|
+
signal
|
|
96
|
+
});
|
|
85
97
|
if (response?.status === 401) return {
|
|
86
98
|
authenticated: false,
|
|
87
99
|
provider: null,
|
|
@@ -94,7 +106,9 @@ async function buildAuthState({ workspaceIdFromCredential, claims, env, client }
|
|
|
94
106
|
workspaceName = data.data.id;
|
|
95
107
|
}
|
|
96
108
|
if (data?.data?.name) workspaceName = data.data.name;
|
|
97
|
-
} catch {
|
|
109
|
+
} catch {
|
|
110
|
+
signal?.throwIfAborted();
|
|
111
|
+
}
|
|
98
112
|
const email = emailFromClaims(claims);
|
|
99
113
|
return {
|
|
100
114
|
authenticated: true,
|
|
@@ -107,10 +121,10 @@ async function buildAuthState({ workspaceIdFromCredential, claims, env, client }
|
|
|
107
121
|
credential: null
|
|
108
122
|
};
|
|
109
123
|
}
|
|
110
|
-
async function readCurrentPrincipalAuthState(client) {
|
|
124
|
+
async function readCurrentPrincipalAuthState(client, signal) {
|
|
111
125
|
if (!client) return null;
|
|
112
126
|
try {
|
|
113
|
-
const { data, response } = await client.GET("/v1/me");
|
|
127
|
+
const { data, response } = await client.GET("/v1/me", { signal });
|
|
114
128
|
if (response?.status === 401) return {
|
|
115
129
|
authenticated: false,
|
|
116
130
|
provider: null,
|
|
@@ -133,11 +147,12 @@ async function readCurrentPrincipalAuthState(client) {
|
|
|
133
147
|
credential: principal.credential
|
|
134
148
|
};
|
|
135
149
|
} catch {
|
|
150
|
+
signal?.throwIfAborted();
|
|
136
151
|
return null;
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
|
-
async function performLogout(env) {
|
|
140
|
-
await new FileTokenStorage(env).clearTokens();
|
|
154
|
+
async function performLogout(env, signal) {
|
|
155
|
+
await new FileTokenStorage(env, signal).clearTokens();
|
|
141
156
|
}
|
|
142
157
|
//#endregion
|
|
143
158
|
export { performLogin, performLogout, readAuthState };
|
package/dist/lib/auth/guard.js
CHANGED
|
@@ -11,7 +11,8 @@ import { createManagementApiClient, createManagementApiSdk } from "@prisma/manag
|
|
|
11
11
|
*
|
|
12
12
|
* Returns null if not authenticated.
|
|
13
13
|
*/
|
|
14
|
-
async function requireComputeAuth(env = process.env) {
|
|
14
|
+
async function requireComputeAuth(env = process.env, signal) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
15
16
|
const rawToken = env[SERVICE_TOKEN_ENV_VAR];
|
|
16
17
|
if (rawToken !== void 0) {
|
|
17
18
|
const token = rawToken.trim();
|
|
@@ -21,7 +22,10 @@ async function requireComputeAuth(env = process.env) {
|
|
|
21
22
|
token
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
|
-
const tokenStorage = new FileTokenStorage(env
|
|
25
|
+
const tokenStorage = new FileTokenStorage(env, signal, {
|
|
26
|
+
activateOnSetTokens: false,
|
|
27
|
+
lockSetTokens: false
|
|
28
|
+
});
|
|
25
29
|
if (!await tokenStorage.getTokens()) return null;
|
|
26
30
|
return createManagementApiSdk({
|
|
27
31
|
clientId: CLIENT_ID,
|
package/dist/lib/auth/login.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getApiBaseUrl } from "./client.js";
|
|
2
2
|
import { FileTokenStorage } from "../../adapters/token-storage.js";
|
|
3
|
-
import open from "open";
|
|
4
3
|
import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
|
|
4
|
+
import open from "open";
|
|
5
5
|
import events from "node:events";
|
|
6
6
|
import http from "node:http";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
7
8
|
//#region src/lib/auth/login.ts
|
|
8
9
|
var AuthError$1 = class extends Error {
|
|
9
10
|
constructor(message) {
|
|
@@ -14,11 +15,15 @@ var AuthError$1 = class extends Error {
|
|
|
14
15
|
async function login(options = {}) {
|
|
15
16
|
const hostname = options.hostname ?? "localhost";
|
|
16
17
|
const port = options.port ?? 0;
|
|
18
|
+
const input = options.input ?? process.stdin;
|
|
19
|
+
const output = options.output ?? process.stderr;
|
|
20
|
+
const interactive = input.isTTY === true;
|
|
17
21
|
const server = http.createServer();
|
|
18
22
|
server.listen({
|
|
19
23
|
host: hostname,
|
|
20
24
|
port
|
|
21
25
|
});
|
|
26
|
+
const pasteAbort = new AbortController();
|
|
22
27
|
try {
|
|
23
28
|
const state = new LoginState({
|
|
24
29
|
hostname,
|
|
@@ -28,9 +33,30 @@ async function login(options = {}) {
|
|
|
28
33
|
apiBaseUrl: options.apiBaseUrl,
|
|
29
34
|
authBaseUrl: options.authBaseUrl,
|
|
30
35
|
openUrl: options.openUrl,
|
|
31
|
-
env: options.env
|
|
36
|
+
env: options.env,
|
|
37
|
+
signal: options.signal,
|
|
38
|
+
output
|
|
32
39
|
});
|
|
33
|
-
|
|
40
|
+
let completed = false;
|
|
41
|
+
let completion;
|
|
42
|
+
const completeOnce = (url) => {
|
|
43
|
+
if (!completion) completion = state.handleCallback(url).then(() => {
|
|
44
|
+
completed = true;
|
|
45
|
+
}, (error) => {
|
|
46
|
+
completion = void 0;
|
|
47
|
+
throw error;
|
|
48
|
+
});
|
|
49
|
+
return completion;
|
|
50
|
+
};
|
|
51
|
+
const httpResult = new Promise((resolve, reject) => {
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
reject(options.signal?.reason);
|
|
54
|
+
};
|
|
55
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
56
|
+
const settle = (callback) => {
|
|
57
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
58
|
+
callback();
|
|
59
|
+
};
|
|
34
60
|
server.on("request", async (req, res) => {
|
|
35
61
|
const url = new URL(`http://${state.host}${req.url}`);
|
|
36
62
|
if (url.pathname !== "/auth/callback") {
|
|
@@ -38,36 +64,94 @@ async function login(options = {}) {
|
|
|
38
64
|
res.end("Not found");
|
|
39
65
|
return;
|
|
40
66
|
}
|
|
67
|
+
if (completed) {
|
|
68
|
+
const workspaceName = await state.resolveWorkspaceName();
|
|
69
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
70
|
+
res.end(renderSuccessPage(workspaceName));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
41
73
|
try {
|
|
42
|
-
await
|
|
74
|
+
await completeOnce(url);
|
|
75
|
+
const workspaceName = await state.resolveWorkspaceName();
|
|
76
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
77
|
+
res.end(renderSuccessPage(workspaceName));
|
|
78
|
+
settle(resolve);
|
|
43
79
|
} catch (error) {
|
|
44
80
|
res.statusCode = 400;
|
|
45
81
|
const message = error instanceof Error ? error.message : String(error);
|
|
46
82
|
res.end(message);
|
|
47
|
-
reject(error);
|
|
83
|
+
settle(() => reject(error));
|
|
48
84
|
return;
|
|
49
85
|
}
|
|
50
|
-
const workspaceName = await state.resolveWorkspaceName();
|
|
51
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
52
|
-
res.end(renderSuccessPage(workspaceName));
|
|
53
|
-
resolve();
|
|
54
86
|
});
|
|
55
87
|
});
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
options.signal?.throwIfAborted();
|
|
89
|
+
const callbackResult = interactive ? Promise.race([httpResult, consumePastedCallback({
|
|
90
|
+
input,
|
|
91
|
+
output,
|
|
92
|
+
signal: pasteAbort.signal,
|
|
93
|
+
complete: completeOnce
|
|
94
|
+
})]) : httpResult;
|
|
95
|
+
await Promise.all([state.openLoginPage(interactive), callbackResult]);
|
|
58
96
|
} finally {
|
|
97
|
+
pasteAbort.abort();
|
|
59
98
|
if (server.listening) await new Promise((resolve) => server.close(() => resolve()));
|
|
60
99
|
}
|
|
61
100
|
}
|
|
101
|
+
async function consumePastedCallback(options) {
|
|
102
|
+
if (!options.input.isTTY) return;
|
|
103
|
+
const rl = readline.createInterface({
|
|
104
|
+
input: options.input,
|
|
105
|
+
output: options.output
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const url = await readPastedCallbackUrl(rl, options);
|
|
110
|
+
if (url === null) return;
|
|
111
|
+
if (url === void 0) continue;
|
|
112
|
+
if (await tryCompletePastedCallback(url, options)) return;
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
rl.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function readPastedCallbackUrl(rl, options) {
|
|
119
|
+
let answer;
|
|
120
|
+
try {
|
|
121
|
+
answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error?.name === "AbortError") return null;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
const trimmed = answer.trim().replace(/^["']|["']$/g, "");
|
|
127
|
+
try {
|
|
128
|
+
if (!trimmed) throw new Error("empty input");
|
|
129
|
+
return new URL(trimmed);
|
|
130
|
+
} catch {
|
|
131
|
+
options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function tryCompletePastedCallback(url, options) {
|
|
136
|
+
try {
|
|
137
|
+
await options.complete(url);
|
|
138
|
+
return true;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
62
145
|
var LoginState = class {
|
|
63
146
|
latestVerifier;
|
|
64
147
|
latestState;
|
|
65
148
|
sdk;
|
|
66
149
|
openUrl;
|
|
67
150
|
tokenStorage;
|
|
151
|
+
output;
|
|
68
152
|
constructor(options) {
|
|
69
153
|
this.options = options;
|
|
70
|
-
this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
|
|
154
|
+
this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env, options.signal, { activateOnSetTokens: true });
|
|
71
155
|
this.sdk = createManagementApiSdk({
|
|
72
156
|
clientId: options.clientId ?? "cmm3lndn701oo0uefvxzo0ivw",
|
|
73
157
|
redirectUri: `http://${options.hostname}:${options.port}/auth/callback`,
|
|
@@ -76,8 +160,10 @@ var LoginState = class {
|
|
|
76
160
|
authBaseUrl: options.authBaseUrl
|
|
77
161
|
});
|
|
78
162
|
this.openUrl = options.openUrl ?? open;
|
|
163
|
+
this.output = options.output;
|
|
79
164
|
}
|
|
80
|
-
async openLoginPage() {
|
|
165
|
+
async openLoginPage(interactive) {
|
|
166
|
+
this.options.signal?.throwIfAborted();
|
|
81
167
|
const { url, state, verifier } = await this.sdk.getLoginUrl({
|
|
82
168
|
scope: "workspace:admin offline_access",
|
|
83
169
|
additionalParams: {
|
|
@@ -88,7 +174,19 @@ var LoginState = class {
|
|
|
88
174
|
});
|
|
89
175
|
this.latestState = state;
|
|
90
176
|
this.latestVerifier = verifier;
|
|
91
|
-
|
|
177
|
+
this.options.signal?.throwIfAborted();
|
|
178
|
+
if (interactive) this.printLoginInstructions(url);
|
|
179
|
+
try {
|
|
180
|
+
await this.openUrl(url);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!interactive) throw error;
|
|
183
|
+
}
|
|
184
|
+
this.options.signal?.throwIfAborted();
|
|
185
|
+
}
|
|
186
|
+
printLoginInstructions(url) {
|
|
187
|
+
const output = this.output;
|
|
188
|
+
if (!output) return;
|
|
189
|
+
output.write(`\nOpen this URL to sign in: ${url}\n\nIf the browser opens on another machine, finish sign-in there. When it\nredirects to localhost, copy the full localhost URL from the address bar\nand paste it here.\n\n`);
|
|
92
190
|
}
|
|
93
191
|
async handleCallback(url) {
|
|
94
192
|
if (url.pathname !== "/auth/callback") throw new AuthError$1("Not a callback URL");
|
|
@@ -115,10 +213,14 @@ var LoginState = class {
|
|
|
115
213
|
try {
|
|
116
214
|
const tokens = await this.tokenStorage.getTokens();
|
|
117
215
|
if (!tokens?.workspaceId) return null;
|
|
118
|
-
const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", {
|
|
216
|
+
const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", {
|
|
217
|
+
params: { path: { id: tokens.workspaceId } },
|
|
218
|
+
signal: this.options.signal
|
|
219
|
+
});
|
|
119
220
|
const name = data?.data?.name;
|
|
120
221
|
return typeof name === "string" && name.trim().length > 0 ? name.trim() : null;
|
|
121
222
|
} catch {
|
|
223
|
+
this.options.signal?.throwIfAborted();
|
|
122
224
|
return null;
|
|
123
225
|
}
|
|
124
226
|
}
|