@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.30
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 +6 -15
- package/dist/adapters/local-state.js +15 -4
- package/dist/adapters/mock-api.js +244 -0
- package/dist/adapters/token-storage.js +335 -34
- package/dist/cli.js +7 -7
- package/dist/cli2.js +24 -5
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +91 -61
- package/dist/commands/auth/index.js +55 -2
- package/dist/commands/branch/index.js +2 -27
- package/dist/commands/bucket/index.js +123 -0
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +249 -0
- package/dist/commands/env.js +8 -4
- package/dist/commands/feedback/index.js +20 -0
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/init/index.js +33 -0
- package/dist/commands/project/index.js +54 -5
- package/dist/controllers/agent-setup.js +52 -0
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +55 -0
- package/dist/controllers/app-env-file.js +181 -0
- package/dist/controllers/app-env.js +227 -104
- package/dist/controllers/app.js +746 -306
- package/dist/controllers/auth.js +247 -3
- package/dist/controllers/branch.js +78 -48
- package/dist/controllers/bucket.js +278 -0
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +567 -0
- package/dist/controllers/feedback.js +86 -0
- package/dist/controllers/init.js +753 -0
- package/dist/controllers/project.js +377 -22
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/lib/agent/cli-command.js +20 -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/{preview-provider.js → app-provider.js} +137 -88
- 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 +83 -0
- package/dist/lib/app/bun-project.js +3 -4
- 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 +3 -60
- 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 +10 -4
- package/dist/lib/auth/guard.js +4 -1
- package/dist/lib/auth/login.js +33 -26
- package/dist/lib/auth/recipient.js +42 -0
- package/dist/lib/bucket/provider.js +139 -0
- package/dist/lib/database/provider.js +378 -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 +5 -4
- package/dist/lib/project/local-pin.js +171 -41
- package/dist/lib/project/provider.js +92 -0
- package/dist/lib/project/resolution.js +199 -48
- package/dist/lib/project/setup.js +67 -20
- 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 +208 -27
- package/dist/presenters/auth.js +99 -2
- package/dist/presenters/branch.js +37 -102
- package/dist/presenters/bucket.js +174 -0
- package/dist/presenters/database.js +448 -0
- package/dist/presenters/feedback.js +26 -0
- package/dist/presenters/init.js +30 -0
- package/dist/presenters/project.js +139 -27
- 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 +458 -17
- package/dist/shell/command-runner.js +58 -18
- package/dist/shell/diagnostics-output.js +57 -0
- package/dist/shell/errors.js +56 -1
- package/dist/shell/help.js +31 -20
- package/dist/shell/output.js +72 -1
- package/dist/shell/prompt.js +12 -5
- package/dist/shell/runtime.js +8 -4
- package/dist/shell/ui.js +42 -3
- package/dist/shell/update-check.js +2 -2
- 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/dist/use-cases/project.js +2 -1
- package/package.json +21 -4
- package/dist/lib/app/preview-build.js +0 -312
- package/dist/lib/app/preview-interaction.js +0 -5
|
@@ -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 };
|
|
@@ -26,7 +26,7 @@ function workspaceIdFromClaims(claims) {
|
|
|
26
26
|
}
|
|
27
27
|
async function performLogin(env, signal) {
|
|
28
28
|
await login({
|
|
29
|
-
tokenStorage: new FileTokenStorage(env, signal),
|
|
29
|
+
tokenStorage: new FileTokenStorage(env, signal, { activateOnSetTokens: true }),
|
|
30
30
|
env,
|
|
31
31
|
signal
|
|
32
32
|
});
|
|
@@ -38,7 +38,8 @@ async function readAuthState(env, signal) {
|
|
|
38
38
|
if (serviceToken.length === 0) throw new Error(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
|
|
39
39
|
return readServiceTokenAuthState(serviceToken, env, signal);
|
|
40
40
|
}
|
|
41
|
-
const
|
|
41
|
+
const tokenStorage = new FileTokenStorage(env, signal);
|
|
42
|
+
const tokens = await tokenStorage.getTokens();
|
|
42
43
|
if (!tokens) return {
|
|
43
44
|
authenticated: false,
|
|
44
45
|
provider: null,
|
|
@@ -48,15 +49,20 @@ async function readAuthState(env, signal) {
|
|
|
48
49
|
};
|
|
49
50
|
const client = await requireComputeAuth(env, signal);
|
|
50
51
|
const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
|
|
51
|
-
if (currentPrincipal)
|
|
52
|
+
if (currentPrincipal) {
|
|
53
|
+
if (currentPrincipal.authenticated && currentPrincipal.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, currentPrincipal.workspace);
|
|
54
|
+
return currentPrincipal;
|
|
55
|
+
}
|
|
52
56
|
const claims = decodeJwtPayload(tokens.accessToken);
|
|
53
|
-
|
|
57
|
+
const authState = await buildAuthState({
|
|
54
58
|
workspaceIdFromCredential: tokens.workspaceId,
|
|
55
59
|
claims,
|
|
56
60
|
env,
|
|
57
61
|
client,
|
|
58
62
|
signal
|
|
59
63
|
});
|
|
64
|
+
if (authState.authenticated && authState.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, authState.workspace);
|
|
65
|
+
return authState;
|
|
60
66
|
}
|
|
61
67
|
async function readServiceTokenAuthState(token, env, signal) {
|
|
62
68
|
const client = await requireComputeAuth(env, signal);
|
package/dist/lib/auth/guard.js
CHANGED
|
@@ -22,7 +22,10 @@ async function requireComputeAuth(env = process.env, signal) {
|
|
|
22
22
|
token
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
const tokenStorage = new FileTokenStorage(env, signal
|
|
25
|
+
const tokenStorage = new FileTokenStorage(env, signal, {
|
|
26
|
+
activateOnSetTokens: false,
|
|
27
|
+
lockSetTokens: false
|
|
28
|
+
});
|
|
26
29
|
if (!await tokenStorage.getTokens()) return null;
|
|
27
30
|
return createManagementApiSdk({
|
|
28
31
|
clientId: CLIENT_ID,
|
package/dist/lib/auth/login.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
7
|
import readline from "node:readline/promises";
|
|
@@ -106,35 +106,42 @@ async function consumePastedCallback(options) {
|
|
|
106
106
|
});
|
|
107
107
|
try {
|
|
108
108
|
for (;;) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (error?.name === "AbortError") return;
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
const trimmed = answer.trim().replace(/^["']|["']$/g, "");
|
|
117
|
-
let url;
|
|
118
|
-
try {
|
|
119
|
-
if (!trimmed) throw new Error("empty input");
|
|
120
|
-
url = new URL(trimmed);
|
|
121
|
-
} catch {
|
|
122
|
-
options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await options.complete(url);
|
|
127
|
-
return;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
-
options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
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;
|
|
133
113
|
}
|
|
134
114
|
} finally {
|
|
135
115
|
rl.close();
|
|
136
116
|
}
|
|
137
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
|
+
}
|
|
138
145
|
var LoginState = class {
|
|
139
146
|
latestVerifier;
|
|
140
147
|
latestState;
|
|
@@ -144,7 +151,7 @@ var LoginState = class {
|
|
|
144
151
|
output;
|
|
145
152
|
constructor(options) {
|
|
146
153
|
this.options = options;
|
|
147
|
-
this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env, options.signal);
|
|
154
|
+
this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env, options.signal, { activateOnSetTokens: true });
|
|
148
155
|
this.sdk = createManagementApiSdk({
|
|
149
156
|
clientId: options.clientId ?? "cmm3lndn701oo0uefvxzo0ivw",
|
|
150
157
|
redirectUri: `http://${options.hostname}:${options.port}/auth/callback`,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getApiBaseUrl } from "./client.js";
|
|
2
|
+
import { FileTokenStorage } from "../../adapters/token-storage.js";
|
|
3
|
+
import { createManagementApiSdk } from "@prisma/management-api-sdk";
|
|
4
|
+
//#region src/lib/auth/recipient.ts
|
|
5
|
+
var RecipientSessionInvalidError = class extends Error {
|
|
6
|
+
constructor(workspaceRef) {
|
|
7
|
+
super(`The stored session for workspace "${workspaceRef}" could not be validated.`);
|
|
8
|
+
this.workspaceRef = workspaceRef;
|
|
9
|
+
this.name = "RecipientSessionInvalidError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a locally stored OAuth workspace session and return a validated
|
|
14
|
+
* access token for it, refreshing through the SDK when the stored token has
|
|
15
|
+
* expired. The active workspace pointer is never touched.
|
|
16
|
+
*
|
|
17
|
+
* Throws WorkspaceSelectionError when the ref does not match exactly one
|
|
18
|
+
* stored session, and RecipientSessionInvalidError when the session cannot
|
|
19
|
+
* be validated or refreshed.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveRecipientWorkspaceSession(workspaceRef, env = process.env, signal) {
|
|
22
|
+
const workspace = await new FileTokenStorage(env, signal).resolveWorkspace(workspaceRef);
|
|
23
|
+
const pinnedStorage = new FileTokenStorage(env, signal, {
|
|
24
|
+
activateOnSetTokens: false,
|
|
25
|
+
lockSetTokens: false,
|
|
26
|
+
pinnedWorkspaceId: workspace.credentialWorkspaceId
|
|
27
|
+
});
|
|
28
|
+
if ((await createManagementApiSdk({
|
|
29
|
+
clientId: "cmm3lndn701oo0uefvxzo0ivw",
|
|
30
|
+
redirectUri: "http://localhost:0/auth/callback",
|
|
31
|
+
tokenStorage: pinnedStorage,
|
|
32
|
+
apiBaseUrl: getApiBaseUrl(env)
|
|
33
|
+
}).client.GET("/v1/workspaces", { signal })).error) throw new RecipientSessionInvalidError(workspaceRef);
|
|
34
|
+
const tokens = await pinnedStorage.getTokens();
|
|
35
|
+
if (!tokens) throw new RecipientSessionInvalidError(workspaceRef);
|
|
36
|
+
return {
|
|
37
|
+
workspace,
|
|
38
|
+
accessToken: tokens.accessToken
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { RecipientSessionInvalidError, resolveRecipientWorkspaceSession };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { CliError } from "../../shell/errors.js";
|
|
2
|
+
//#region src/lib/bucket/provider.ts
|
|
3
|
+
function createManagementBucketProvider(client) {
|
|
4
|
+
return {
|
|
5
|
+
async listBuckets(options) {
|
|
6
|
+
const buckets = [];
|
|
7
|
+
let cursor;
|
|
8
|
+
while (true) {
|
|
9
|
+
const result = await client.GET("/v1/buckets", {
|
|
10
|
+
params: { query: {
|
|
11
|
+
projectId: options.projectId,
|
|
12
|
+
branchGitName: options.branchName,
|
|
13
|
+
cursor
|
|
14
|
+
} },
|
|
15
|
+
signal: options.signal
|
|
16
|
+
});
|
|
17
|
+
if (result.error || !result.data) throw bucketApiError("Failed to list buckets", result.response, result.error);
|
|
18
|
+
const data = result.data;
|
|
19
|
+
buckets.push(...data.data);
|
|
20
|
+
if (!data.pagination.hasMore || !data.pagination.nextCursor) break;
|
|
21
|
+
cursor = data.pagination.nextCursor;
|
|
22
|
+
}
|
|
23
|
+
return buckets.map(normalizeBucket);
|
|
24
|
+
},
|
|
25
|
+
async createBucket(options) {
|
|
26
|
+
const result = await client.POST("/v1/buckets", {
|
|
27
|
+
body: {
|
|
28
|
+
projectId: options.projectId,
|
|
29
|
+
...options.name ? { name: options.name } : {},
|
|
30
|
+
...options.branchGitName ? { branchGitName: options.branchGitName } : {}
|
|
31
|
+
},
|
|
32
|
+
signal: options.signal
|
|
33
|
+
});
|
|
34
|
+
if (result.error || !result.data) throw bucketApiError("Failed to create bucket", result.response, result.error);
|
|
35
|
+
const data = result.data;
|
|
36
|
+
return normalizeBucket(data.data);
|
|
37
|
+
},
|
|
38
|
+
async deleteBucket(bucketId, options) {
|
|
39
|
+
const result = await client.DELETE("/v1/buckets/{bucketId}", {
|
|
40
|
+
params: { path: { bucketId } },
|
|
41
|
+
signal: options?.signal
|
|
42
|
+
});
|
|
43
|
+
if (result.error) throw bucketApiError("Failed to delete bucket", result.response, result.error);
|
|
44
|
+
},
|
|
45
|
+
async listKeys(bucketId, options) {
|
|
46
|
+
const keys = [];
|
|
47
|
+
let cursor;
|
|
48
|
+
while (true) {
|
|
49
|
+
const result = await client.GET("/v1/buckets/{bucketId}/keys", {
|
|
50
|
+
params: {
|
|
51
|
+
path: { bucketId },
|
|
52
|
+
query: { cursor }
|
|
53
|
+
},
|
|
54
|
+
signal: options?.signal
|
|
55
|
+
});
|
|
56
|
+
if (result.error || !result.data) throw bucketApiError("Failed to list bucket keys", result.response, result.error);
|
|
57
|
+
const data = result.data;
|
|
58
|
+
keys.push(...data.data);
|
|
59
|
+
if (!data.pagination.hasMore || !data.pagination.nextCursor) break;
|
|
60
|
+
cursor = data.pagination.nextCursor;
|
|
61
|
+
}
|
|
62
|
+
return keys.map(normalizeKey);
|
|
63
|
+
},
|
|
64
|
+
async createKey(options) {
|
|
65
|
+
const result = await client.POST("/v1/buckets/{bucketId}/keys", {
|
|
66
|
+
params: { path: { bucketId: options.bucketId } },
|
|
67
|
+
body: {
|
|
68
|
+
role: options.role,
|
|
69
|
+
...options.name ? { name: options.name } : {}
|
|
70
|
+
},
|
|
71
|
+
signal: options.signal
|
|
72
|
+
});
|
|
73
|
+
if (result.error || !result.data) throw bucketApiError("Failed to create bucket key", result.response, result.error);
|
|
74
|
+
const raw = result.data.data;
|
|
75
|
+
const secretAccessKey = raw.secretAccessKey;
|
|
76
|
+
const accessKeyId = raw.accessKeyId;
|
|
77
|
+
const endpoint = raw.endpoint;
|
|
78
|
+
const bucketName = raw.bucketName;
|
|
79
|
+
if (!secretAccessKey || !accessKeyId || !endpoint || !bucketName) throw new CliError({
|
|
80
|
+
code: "BUCKET_KEY_SECRET_MISSING",
|
|
81
|
+
domain: "bucket",
|
|
82
|
+
summary: "Created bucket key did not return credentials",
|
|
83
|
+
why: "Bucket key credentials are one-time-view secrets, but the Management API did not include them in this create response.",
|
|
84
|
+
fix: "Create another bucket key and store the returned credentials immediately.",
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
nextSteps: [`prisma-cli bucket key create ${options.bucketId}`]
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
key: normalizeKey(raw),
|
|
90
|
+
secretAccessKey,
|
|
91
|
+
accessKeyId,
|
|
92
|
+
endpoint,
|
|
93
|
+
bucketName
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async deleteKey(bucketId, keyId, options) {
|
|
97
|
+
const result = await client.DELETE("/v1/buckets/{bucketId}/keys/{keyId}", {
|
|
98
|
+
params: { path: {
|
|
99
|
+
bucketId,
|
|
100
|
+
keyId
|
|
101
|
+
} },
|
|
102
|
+
signal: options?.signal
|
|
103
|
+
});
|
|
104
|
+
if (result.error) throw bucketApiError("Failed to delete bucket key", result.response, result.error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function normalizeBucket(raw) {
|
|
109
|
+
return {
|
|
110
|
+
id: raw.id,
|
|
111
|
+
name: raw.name,
|
|
112
|
+
status: raw.status,
|
|
113
|
+
branchId: raw.branchId,
|
|
114
|
+
createdAt: raw.createdAt
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function normalizeKey(raw) {
|
|
118
|
+
return {
|
|
119
|
+
id: raw.id,
|
|
120
|
+
name: raw.name,
|
|
121
|
+
role: raw.role,
|
|
122
|
+
valueHint: raw.valueHint,
|
|
123
|
+
createdAt: raw.createdAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function bucketApiError(summary, response, error) {
|
|
127
|
+
const status = response?.status ?? 0;
|
|
128
|
+
return new CliError({
|
|
129
|
+
code: error?.error?.code ?? "BUCKET_API_ERROR",
|
|
130
|
+
domain: "bucket",
|
|
131
|
+
summary,
|
|
132
|
+
why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
|
|
133
|
+
fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
|
|
134
|
+
exitCode: 1,
|
|
135
|
+
nextSteps: []
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { createManagementBucketProvider, normalizeBucket, normalizeKey };
|