@devdogsuga/devtools 0.1.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/README.md +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configures a Supabase project to accept "Sign in with DevDogs".
|
|
3
|
+
*
|
|
4
|
+
* Was `@devdogsuga/oauth-setup`, a package of its own with its own binary. It
|
|
5
|
+
* was separate because it was going to be published for sibling projects to
|
|
6
|
+
* install; nothing here is published, so that separation bought only a second
|
|
7
|
+
* `@clack/prompts` dependency and a second place to look for a CLI.
|
|
8
|
+
*
|
|
9
|
+
* Two axes, chosen independently:
|
|
10
|
+
*
|
|
11
|
+
* * TARGET — which Supabase project gets the custom provider: the one
|
|
12
|
+
* `supabase status` reports for this directory, or a hosted one
|
|
13
|
+
* (staging/production, or someone else's) named by URL + service-role
|
|
14
|
+
* key. See `target.ts`.
|
|
15
|
+
* * CONNECT METHOD — how the OAuth client's `client_id`/`client_secret`
|
|
16
|
+
* are obtained: the one-click flow (default, TASK-351) opens a browser
|
|
17
|
+
* at the platform's `/tools/oauth/connect`, waits on a loopback
|
|
18
|
+
* listener, and exchanges a PKCE-protected code for them; "Paste
|
|
19
|
+
* credentials instead" is the original manual flow, for a platform that
|
|
20
|
+
* is unreachable or a contributor who would rather not open a browser.
|
|
21
|
+
*/
|
|
22
|
+
import { confirm, log, note, password, select, spinner, text, } from "@clack/prompts";
|
|
23
|
+
import { DEFAULT_API_URL, ENV_KEYS, PROVIDER_IDENTIFIER, PROVIDER_NAME, WEBSITE_URL, } from "./config.js";
|
|
24
|
+
import { upsertEnvLocal } from "./env-file.js";
|
|
25
|
+
import { checkProvider, upsertDevDogsProvider } from "./db.js";
|
|
26
|
+
import { DiscoveryError, fetchIssuer } from "./discovery.js";
|
|
27
|
+
import { openBrowser } from "./browser.js";
|
|
28
|
+
import { codeChallengeFor, generateCodeVerifier, generateState, } from "./pkce.js";
|
|
29
|
+
import { CallbackDeniedError, CallbackMalformedError, CallbackTimeoutError, StateMismatchError, start as startLoopback, verifyState, } from "./loopback.js";
|
|
30
|
+
import { buildConnectUrl } from "./connect-url.js";
|
|
31
|
+
import { ExchangeError, exchangeCode } from "./exchange.js";
|
|
32
|
+
import { DeviceError, PollError, pollForToken, requestDeviceCode, } from "./device.js";
|
|
33
|
+
import { decideTransport } from "./transport.js";
|
|
34
|
+
import { defaultLabel } from "./label.js";
|
|
35
|
+
import { HostedCredentialsMissingError, NoTierResolvedError, readHostedTargetFromEnvFiles, resolveLocalTarget, resolveHostedTargetInRepo, } from "./target.js";
|
|
36
|
+
import { recordResolved } from "../invocation.js";
|
|
37
|
+
import { loadEnvSession } from "../repo/peers.js";
|
|
38
|
+
import { discoverRepoRoot } from "../repo/root.js";
|
|
39
|
+
import { resolveTier } from "../tier.js";
|
|
40
|
+
import { bail, errorMessage, unwrap } from "../ui.js";
|
|
41
|
+
/** Runs `supabase status`, retrying if not running. */
|
|
42
|
+
async function detectLocalWithRetry(cwd) {
|
|
43
|
+
while (true) {
|
|
44
|
+
const s = spinner();
|
|
45
|
+
s.start("Detecting local Supabase");
|
|
46
|
+
try {
|
|
47
|
+
const target = await resolveLocalTarget(cwd);
|
|
48
|
+
s.stop(`Connected to ${target.apiUrl}`);
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
s.stop("Could not detect local Supabase");
|
|
53
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
54
|
+
note("1. Make sure Docker is running\n" +
|
|
55
|
+
"2. Run `supabase start` in your project directory\n" +
|
|
56
|
+
"3. Confirm it's running with `supabase status`", "Troubleshooting");
|
|
57
|
+
const retry = unwrap(await confirm({ message: "Ready to retry?", initialValue: true }));
|
|
58
|
+
if (!retry)
|
|
59
|
+
bail("Run `supabase start` then try again.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function requireUrl(v) {
|
|
64
|
+
if (!v)
|
|
65
|
+
return "Required";
|
|
66
|
+
try {
|
|
67
|
+
new URL(v);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "Enter a valid URL";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function requireNonEmpty(v) {
|
|
75
|
+
return v?.trim() ? undefined : "Required";
|
|
76
|
+
}
|
|
77
|
+
/** A hosted target reached from INSIDE a DevDogsUGA checkout: asks which tier, lazily. */
|
|
78
|
+
async function chooseHostedTargetInRepo() {
|
|
79
|
+
try {
|
|
80
|
+
return await resolveHostedTargetInRepo(undefined, {
|
|
81
|
+
resolveTier: (tierArg, message) => resolveTier(tierArg, message, { label: "devtools oauth" }),
|
|
82
|
+
enterEnvironment: async (tier) => {
|
|
83
|
+
const envSession = await loadEnvSession();
|
|
84
|
+
// `override: true` — a DELIBERATE, later choice of tier here must
|
|
85
|
+
// win over the `development` the (envFree) launcher already
|
|
86
|
+
// entered before this command ever ran. See `target.ts`'s
|
|
87
|
+
// `RepoHostedTargetDeps.enterEnvironment`.
|
|
88
|
+
await envSession.enterEnvironment(tier, { override: true });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof HostedCredentialsMissingError)
|
|
94
|
+
bail(err.message);
|
|
95
|
+
if (err instanceof NoTierResolvedError)
|
|
96
|
+
bail("No tier selected.");
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** A hosted target reached from OUTSIDE any checkout: `.env.local`/`.env`, else a prompt. */
|
|
101
|
+
async function chooseHostedTargetFromEnvOrPrompt(cwd) {
|
|
102
|
+
const fromFiles = readHostedTargetFromEnvFiles(cwd);
|
|
103
|
+
if (fromFiles.apiUrl && fromFiles.serviceRoleKey) {
|
|
104
|
+
log.info("Using the Supabase URL and service-role key from .env.local/.env.");
|
|
105
|
+
return { apiUrl: fromFiles.apiUrl, serviceRoleKey: fromFiles.serviceRoleKey, kind: "hosted" };
|
|
106
|
+
}
|
|
107
|
+
const apiUrl = unwrap(await text({ message: "Supabase project URL", validate: requireUrl })).replace(/\/+$/, "");
|
|
108
|
+
const serviceRoleKey = unwrap(await password({
|
|
109
|
+
message: "Supabase service-role key",
|
|
110
|
+
validate: requireNonEmpty,
|
|
111
|
+
})).trim();
|
|
112
|
+
return { apiUrl, serviceRoleKey, kind: "hosted" };
|
|
113
|
+
}
|
|
114
|
+
/** Step 1: choose which Supabase project this run configures. */
|
|
115
|
+
async function chooseTarget(cwd) {
|
|
116
|
+
const insideRepo = discoverRepoRoot(cwd) !== null;
|
|
117
|
+
const kind = unwrap(await select({
|
|
118
|
+
message: "Which Supabase project are you configuring?",
|
|
119
|
+
options: [
|
|
120
|
+
{
|
|
121
|
+
value: "local",
|
|
122
|
+
label: "Local (this directory)",
|
|
123
|
+
hint: "supabase status",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
value: "hosted",
|
|
127
|
+
label: "A hosted project",
|
|
128
|
+
hint: insideRepo
|
|
129
|
+
? "staging, production, or another project"
|
|
130
|
+
: "reads .env.local/.env, or asks",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
}));
|
|
134
|
+
if (kind === "local")
|
|
135
|
+
return detectLocalWithRetry(cwd);
|
|
136
|
+
return insideRepo
|
|
137
|
+
? chooseHostedTargetInRepo()
|
|
138
|
+
: chooseHostedTargetFromEnvOrPrompt(cwd);
|
|
139
|
+
}
|
|
140
|
+
/** One human-readable line per `ExchangeError` kind. */
|
|
141
|
+
function describeExchangeError(err) {
|
|
142
|
+
switch (err.kind) {
|
|
143
|
+
case "invalid_grant":
|
|
144
|
+
return `The authorization code was rejected (${err.message}). Run the command again.`;
|
|
145
|
+
case "rate_limited":
|
|
146
|
+
return err.message;
|
|
147
|
+
case "network":
|
|
148
|
+
case "malformed":
|
|
149
|
+
case "invalid_request":
|
|
150
|
+
return err.message;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** One human-readable line per `DeviceError` kind (see `device.ts`'s `requestDeviceCode`). */
|
|
154
|
+
function describeDeviceError(err) {
|
|
155
|
+
switch (err.kind) {
|
|
156
|
+
case "rate_limited":
|
|
157
|
+
case "network":
|
|
158
|
+
case "malformed":
|
|
159
|
+
case "invalid_request":
|
|
160
|
+
return err.message;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** One human-readable line per `PollError` kind (see `device.ts`'s `pollForToken`). */
|
|
164
|
+
function describePollError(err) {
|
|
165
|
+
switch (err.kind) {
|
|
166
|
+
case "invalid_grant":
|
|
167
|
+
return `The device code was rejected (${err.message}). Run the command again.`;
|
|
168
|
+
case "access_denied":
|
|
169
|
+
case "expired_token":
|
|
170
|
+
case "invalid_request":
|
|
171
|
+
case "network":
|
|
172
|
+
case "malformed":
|
|
173
|
+
return err.message;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Step 2 (one-click branch, device transport): request a device code,
|
|
178
|
+
* show the user code and verification URL prominently, try to open a
|
|
179
|
+
* browser to it (failure is not fatal — the URL is already on screen), and
|
|
180
|
+
* poll until the platform reports the user approved it, denied it, or the
|
|
181
|
+
* code expired.
|
|
182
|
+
*/
|
|
183
|
+
async function connectByDevice(target, platformUrl, cwd, transportReason) {
|
|
184
|
+
const label = defaultLabel(cwd);
|
|
185
|
+
const callbackUri = `${target.apiUrl}/auth/v1/callback`;
|
|
186
|
+
log.info(`Using the device-code flow — ${transportReason}.`);
|
|
187
|
+
const requestSpinner = spinner();
|
|
188
|
+
requestSpinner.start("Requesting a device code");
|
|
189
|
+
let device;
|
|
190
|
+
try {
|
|
191
|
+
device = await requestDeviceCode({ platformUrl, label, callbackUri });
|
|
192
|
+
requestSpinner.stop("Got a device code");
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
requestSpinner.stop("Could not request a device code");
|
|
196
|
+
if (err instanceof DeviceError)
|
|
197
|
+
bail(describeDeviceError(err));
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
note(`Enter this code: ${device.userCode}\n` +
|
|
201
|
+
`At: ${device.verificationUri}\n\n` +
|
|
202
|
+
`Or open this URL directly:\n${device.verificationUriComplete}`, "Approve the connection");
|
|
203
|
+
log.info(`Opening ${device.verificationUriComplete}`);
|
|
204
|
+
openBrowser(device.verificationUriComplete);
|
|
205
|
+
const pollSpinner = spinner();
|
|
206
|
+
pollSpinner.start(`Waiting for you to enter ${device.userCode} and approve the connection`);
|
|
207
|
+
try {
|
|
208
|
+
const result = await pollForToken({
|
|
209
|
+
platformUrl,
|
|
210
|
+
deviceCode: device.deviceCode,
|
|
211
|
+
intervalSeconds: device.interval,
|
|
212
|
+
expiresInSeconds: device.expiresIn,
|
|
213
|
+
});
|
|
214
|
+
pollSpinner.stop("Approved");
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
pollSpinner.stop("Connection not completed");
|
|
219
|
+
if (err instanceof PollError)
|
|
220
|
+
bail(describePollError(err));
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Step 2 (one-click branch): browser + loopback callback + exchange, given
|
|
226
|
+
* an ALREADY-STARTED `listener` — starting it is `connectViaOneClick`'s job,
|
|
227
|
+
* not this function's, so that the one failure that should fall back to the
|
|
228
|
+
* device flow (the listener failing to even start) is never confused with
|
|
229
|
+
* any failure in here, all of which are this flow's own and must propagate
|
|
230
|
+
* (or `bail()`) unchanged rather than triggering a fallback.
|
|
231
|
+
*
|
|
232
|
+
* Always closes the listener, on every exit path — success, denial,
|
|
233
|
+
* malformed callback, state mismatch, timeout, or an exchange error.
|
|
234
|
+
*/
|
|
235
|
+
async function connectOneClick(target, platformUrl, cwd, transportReason, listener) {
|
|
236
|
+
const verifier = generateCodeVerifier();
|
|
237
|
+
const challenge = codeChallengeFor(verifier);
|
|
238
|
+
const state = generateState();
|
|
239
|
+
const label = defaultLabel(cwd);
|
|
240
|
+
const callbackUri = `${target.apiUrl}/auth/v1/callback`;
|
|
241
|
+
log.info(`Using the loopback flow — ${transportReason}.`);
|
|
242
|
+
try {
|
|
243
|
+
const connectUrl = buildConnectUrl({
|
|
244
|
+
platformUrl,
|
|
245
|
+
redirectUri: listener.redirectUri,
|
|
246
|
+
codeChallenge: challenge,
|
|
247
|
+
state,
|
|
248
|
+
label,
|
|
249
|
+
callbackUri,
|
|
250
|
+
});
|
|
251
|
+
log.info(`Opening ${connectUrl}`);
|
|
252
|
+
openBrowser(connectUrl);
|
|
253
|
+
note(`If your browser did not open, visit this URL:\n${connectUrl}`, "Approve the connection");
|
|
254
|
+
const s = spinner();
|
|
255
|
+
s.start("Waiting for you to approve the connection in your browser");
|
|
256
|
+
let callback;
|
|
257
|
+
try {
|
|
258
|
+
callback = await listener.result;
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
s.stop("Connection not completed");
|
|
262
|
+
if (err instanceof CallbackDeniedError) {
|
|
263
|
+
bail(err.errorCode === "access_denied"
|
|
264
|
+
? "Connection declined in the browser."
|
|
265
|
+
: `Connection failed: ${err.message}`);
|
|
266
|
+
}
|
|
267
|
+
if (err instanceof CallbackTimeoutError)
|
|
268
|
+
bail(err.message);
|
|
269
|
+
if (err instanceof CallbackMalformedError)
|
|
270
|
+
bail(err.message);
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
s.stop("Approved");
|
|
274
|
+
let verified;
|
|
275
|
+
try {
|
|
276
|
+
verified = verifyState(state, callback);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (err instanceof StateMismatchError)
|
|
280
|
+
bail(err.message);
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
const exchangeSpinner = spinner();
|
|
284
|
+
exchangeSpinner.start("Exchanging the authorization code");
|
|
285
|
+
try {
|
|
286
|
+
const result = await exchangeCode({
|
|
287
|
+
platformUrl,
|
|
288
|
+
code: verified.code,
|
|
289
|
+
codeVerifier: verifier,
|
|
290
|
+
});
|
|
291
|
+
exchangeSpinner.stop("Connected");
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
exchangeSpinner.stop("Could not complete the exchange");
|
|
296
|
+
if (err instanceof ExchangeError)
|
|
297
|
+
bail(describeExchangeError(err));
|
|
298
|
+
throw err;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
listener.close();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Step 2 (one-click branch): decides loopback vs. device (see `transport.ts`)
|
|
307
|
+
* and runs it.
|
|
308
|
+
*
|
|
309
|
+
* `transportOverride` is `--device`/`--loopback` (see `commands.ts`'s `oauth`
|
|
310
|
+
* node); `cli.ts` refuses passing both, so at most one is set here. Left
|
|
311
|
+
* undefined, the transport is auto-detected from the environment.
|
|
312
|
+
*
|
|
313
|
+
* `startLoopback()` is called HERE, not inside `connectOneClick`, and ONLY
|
|
314
|
+
* its failure falls back to device — every failure that can happen once the
|
|
315
|
+
* listener is actually up (denial, timeout, a bad exchange — all of which
|
|
316
|
+
* `connectOneClick` reports via `bail()`, or an unexpected rethrow) is that
|
|
317
|
+
* flow's own and must propagate unchanged, not be mistaken for "the listener
|
|
318
|
+
* never started" and silently rerouted to a different transport with a
|
|
319
|
+
* misleading warning. Loopback forced via `--loopback` never falls back at
|
|
320
|
+
* all: a start failure there is left to propagate as-is.
|
|
321
|
+
*/
|
|
322
|
+
export async function connectViaOneClick(target, platformUrl, cwd, transportOverride) {
|
|
323
|
+
const decision = decideTransport({
|
|
324
|
+
forceDevice: transportOverride === "device",
|
|
325
|
+
forceLoopback: transportOverride === "loopback",
|
|
326
|
+
});
|
|
327
|
+
if (decision.transport === "device") {
|
|
328
|
+
return connectByDevice(target, platformUrl, cwd, decision.reason);
|
|
329
|
+
}
|
|
330
|
+
let listener;
|
|
331
|
+
try {
|
|
332
|
+
listener = await startLoopback();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
if (transportOverride === "loopback")
|
|
336
|
+
throw err;
|
|
337
|
+
log.warn(`Could not start the local loopback listener (${errorMessage(err)}) — ` +
|
|
338
|
+
"falling back to the device-code flow.");
|
|
339
|
+
return connectByDevice(target, platformUrl, cwd, "the loopback listener failed to start");
|
|
340
|
+
}
|
|
341
|
+
return connectOneClick(target, platformUrl, cwd, decision.reason, listener);
|
|
342
|
+
}
|
|
343
|
+
/** Step 2 (paste branch): the original manual flow — DevDogs API URL, then client ID/secret. */
|
|
344
|
+
async function connectByPasting(baseUrlOverride) {
|
|
345
|
+
let baseUrl = baseUrlOverride ?? process.env[ENV_KEYS.baseUrl] ?? DEFAULT_API_URL;
|
|
346
|
+
if (!baseUrlOverride) {
|
|
347
|
+
baseUrl = unwrap(await text({
|
|
348
|
+
message: "DevDogs API URL",
|
|
349
|
+
initialValue: baseUrl,
|
|
350
|
+
validate: (v) => {
|
|
351
|
+
if (!v)
|
|
352
|
+
return;
|
|
353
|
+
try {
|
|
354
|
+
new URL(v);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return "Enter a valid URL (e.g. https://api.devdogsuga.org)";
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
363
|
+
// A base URL entered at the prompt (not passed as --base-url) is what the
|
|
364
|
+
// rerun line needs to skip this step next time.
|
|
365
|
+
if (!baseUrlOverride)
|
|
366
|
+
recordResolved("--base-url", baseUrl);
|
|
367
|
+
let clientId = process.env[ENV_KEYS.clientId] || undefined;
|
|
368
|
+
let clientSecret = process.env[ENV_KEYS.clientSecret] || undefined;
|
|
369
|
+
if (clientId && clientSecret) {
|
|
370
|
+
const useSaved = unwrap(await confirm({
|
|
371
|
+
message: `Use saved credentials? (client ID: ${clientId})`,
|
|
372
|
+
initialValue: true,
|
|
373
|
+
}));
|
|
374
|
+
if (!useSaved) {
|
|
375
|
+
clientId = undefined;
|
|
376
|
+
clientSecret = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!clientId || !clientSecret) {
|
|
380
|
+
note(`1. Visit ${WEBSITE_URL}/tools/oauth\n` +
|
|
381
|
+
`2. Sign in — link your GitHub account if prompted\n` +
|
|
382
|
+
`3. Enable OAuth and copy your Client ID and Client Secret\n` +
|
|
383
|
+
` (the secret is shown once, immediately after you enable it)`, "Register an OAuth client");
|
|
384
|
+
clientId = unwrap(await text({
|
|
385
|
+
message: "Client ID",
|
|
386
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
387
|
+
})).trim();
|
|
388
|
+
clientSecret = unwrap(await password({
|
|
389
|
+
message: "Client Secret",
|
|
390
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
391
|
+
})).trim();
|
|
392
|
+
}
|
|
393
|
+
// Supabase Auth's own OIDC provider refuses to register a custom provider
|
|
394
|
+
// whose declared issuer disagrees with what ITS discovery document
|
|
395
|
+
// reports — and that need not be `${baseUrl}/auth/v1`; Supabase may
|
|
396
|
+
// advertise its project ref host there instead of the custom domain this
|
|
397
|
+
// ran against. Reading the issuer from discovery, rather than assuming
|
|
398
|
+
// it, is what keeps this correct either way — see `discovery.ts`'s
|
|
399
|
+
// header. (The one-click branch skips this entirely: the exchange
|
|
400
|
+
// response carries the issuer directly.)
|
|
401
|
+
const discoverySpinner = spinner();
|
|
402
|
+
discoverySpinner.start("Discovering issuer");
|
|
403
|
+
let issuer;
|
|
404
|
+
try {
|
|
405
|
+
issuer = await fetchIssuer(baseUrl);
|
|
406
|
+
discoverySpinner.stop(`Discovered issuer: ${issuer}`);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
discoverySpinner.stop("Could not discover the issuer");
|
|
410
|
+
if (err instanceof DiscoveryError)
|
|
411
|
+
bail(err.message);
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
return { clientId, clientSecret, issuer };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The wizard. `baseUrlOverride`/`platformUrlOverride`, when given, skip
|
|
418
|
+
* their respective prompts (only reachable via "Paste credentials instead"
|
|
419
|
+
* and the one-click flow respectively). `transportOverride` (`--device`/
|
|
420
|
+
* `--loopback`) forces the one-click flow's transport — see
|
|
421
|
+
* `connectViaOneClick` — and is ignored on the "Paste credentials instead"
|
|
422
|
+
* branch, which has no transport of its own.
|
|
423
|
+
*
|
|
424
|
+
* Intro and outro are the caller's, so this composes into the devtools menu
|
|
425
|
+
* without drawing a second box inside the first.
|
|
426
|
+
*/
|
|
427
|
+
export async function runOAuthSetup(baseUrlOverride, platformUrlOverride, transportOverride) {
|
|
428
|
+
const cwd = process.cwd();
|
|
429
|
+
log.info('Configures a Supabase project to support "Sign in with DevDogs".');
|
|
430
|
+
// ── Step 1: Choose target ────────────────────────────────────────────────
|
|
431
|
+
const target = await chooseTarget(cwd);
|
|
432
|
+
// ── Step 2: Connect ──────────────────────────────────────────────────────
|
|
433
|
+
const providerName = unwrap(await text({
|
|
434
|
+
message: "Provider display name",
|
|
435
|
+
initialValue: process.env[ENV_KEYS.providerName] ?? PROVIDER_NAME,
|
|
436
|
+
placeholder: PROVIDER_NAME,
|
|
437
|
+
}));
|
|
438
|
+
const connectMethod = unwrap(await select({
|
|
439
|
+
message: "How do you want to connect?",
|
|
440
|
+
options: [
|
|
441
|
+
{
|
|
442
|
+
value: "one-click",
|
|
443
|
+
label: "One-click connect",
|
|
444
|
+
hint: "opens your browser — recommended",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
value: "paste",
|
|
448
|
+
label: "Paste credentials instead",
|
|
449
|
+
hint: "register an OAuth client on the website by hand",
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
}));
|
|
453
|
+
const usedOneClick = connectMethod === "one-click";
|
|
454
|
+
const { clientId, clientSecret, issuer } = usedOneClick
|
|
455
|
+
? await connectViaOneClick(target, platformUrlOverride ?? process.env[ENV_KEYS.platformUrl] ?? WEBSITE_URL, cwd, transportOverride)
|
|
456
|
+
: await connectByPasting(baseUrlOverride);
|
|
457
|
+
// ── Step 3: Configure — resolve the provider identifier, then upsert ──────
|
|
458
|
+
let identifier = PROVIDER_IDENTIFIER;
|
|
459
|
+
{
|
|
460
|
+
const s = spinner();
|
|
461
|
+
s.start(`Checking for existing ${identifier} provider`);
|
|
462
|
+
let existing;
|
|
463
|
+
try {
|
|
464
|
+
existing = await checkProvider(target, identifier);
|
|
465
|
+
s.stop(existing.exists
|
|
466
|
+
? `Found existing ${identifier} provider (${existing.name ?? "unnamed"})`
|
|
467
|
+
: `No existing ${identifier} provider — will create`);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
s.stop("Failed to check for existing provider");
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
if (existing.exists) {
|
|
474
|
+
const action = unwrap(await select({
|
|
475
|
+
message: `Provider "${identifier}" already exists. What would you like to do?`,
|
|
476
|
+
options: [
|
|
477
|
+
{
|
|
478
|
+
value: "update",
|
|
479
|
+
label: "Update existing provider",
|
|
480
|
+
hint: "Overwrites client ID, secret, name, and issuer",
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
value: "new",
|
|
484
|
+
label: "Add a new provider with a different identifier",
|
|
485
|
+
hint: `e.g. custom:devdogsuga-staging`,
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
}));
|
|
489
|
+
if (action === "new") {
|
|
490
|
+
const suffix = unwrap(await text({
|
|
491
|
+
message: `New identifier suffix — will be registered as "custom:<suffix>"`,
|
|
492
|
+
placeholder: "devdogsuga-staging",
|
|
493
|
+
validate: (v) => {
|
|
494
|
+
if (!v?.trim())
|
|
495
|
+
return "Required";
|
|
496
|
+
if (/[^a-z0-9-]/.test(v.trim()))
|
|
497
|
+
return "Use only lowercase letters, numbers, and hyphens";
|
|
498
|
+
},
|
|
499
|
+
})).trim();
|
|
500
|
+
identifier = `custom:${suffix}`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const s = spinner();
|
|
505
|
+
s.start(`Configuring ${identifier}`);
|
|
506
|
+
const row = await upsertDevDogsProvider(target, {
|
|
507
|
+
identifier,
|
|
508
|
+
name: providerName,
|
|
509
|
+
clientId,
|
|
510
|
+
clientSecret,
|
|
511
|
+
issuer,
|
|
512
|
+
});
|
|
513
|
+
s.stop(`Configured ${row.identifier} (issuer: ${row.issuer})`);
|
|
514
|
+
// ── Step 4: Finish — persist to .env.local, print next steps ─────────────
|
|
515
|
+
const envValues = {
|
|
516
|
+
[ENV_KEYS.providerName]: providerName,
|
|
517
|
+
[ENV_KEYS.clientId]: clientId,
|
|
518
|
+
// The secret key never rides in a value string that ships to a client —
|
|
519
|
+
// `.env.local` is a server-side/CLI file, never bundled, the same
|
|
520
|
+
// guarantee the original manual flow already carried; this branch adds
|
|
521
|
+
// nothing new to that boundary.
|
|
522
|
+
[ENV_KEYS.clientSecret]: clientSecret,
|
|
523
|
+
};
|
|
524
|
+
if (!usedOneClick) {
|
|
525
|
+
envValues[ENV_KEYS.baseUrl] =
|
|
526
|
+
baseUrlOverride ?? process.env[ENV_KEYS.baseUrl] ?? DEFAULT_API_URL;
|
|
527
|
+
}
|
|
528
|
+
upsertEnvLocal(cwd, envValues);
|
|
529
|
+
log.success("Credentials saved to .env.local");
|
|
530
|
+
const callbackUri = `${target.apiUrl}/auth/v1/callback`;
|
|
531
|
+
// The one-click flow already told the platform this callback URL as part
|
|
532
|
+
// of `/tools/oauth/connect` (`callback_uri`), so there is nothing left to
|
|
533
|
+
// register by hand. Only the manual "paste credentials" path — which
|
|
534
|
+
// created its OAuth client on the website, where redirect URIs are a
|
|
535
|
+
// separate step — still needs this.
|
|
536
|
+
if (!usedOneClick) {
|
|
537
|
+
const alreadyRegistered = unwrap(await confirm({
|
|
538
|
+
message: "Have you already registered your Supabase callback URL with DevDogs?",
|
|
539
|
+
initialValue: false,
|
|
540
|
+
}));
|
|
541
|
+
if (!alreadyRegistered) {
|
|
542
|
+
const keysUrl = `${WEBSITE_URL}/tools/oauth?add_redirect_uri=${encodeURIComponent(callbackUri)}`;
|
|
543
|
+
log.info(`Opening ${keysUrl}`);
|
|
544
|
+
openBrowser(keysUrl);
|
|
545
|
+
}
|
|
546
|
+
if (!alreadyRegistered) {
|
|
547
|
+
note(`Finish registering your callback URL in the browser that just opened:\n` +
|
|
548
|
+
` ${callbackUri}`, "One more step");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const nextSteps = [
|
|
552
|
+
`Make sure your project's supabase/config.toml allows your app callback:`,
|
|
553
|
+
` additional_redirect_urls = ["http://localhost:<port>/auth/callback"]`,
|
|
554
|
+
``,
|
|
555
|
+
`Trigger sign-in from your app:`,
|
|
556
|
+
``,
|
|
557
|
+
` await supabase.auth.signInWithOAuth({`,
|
|
558
|
+
` provider: "${identifier}",`,
|
|
559
|
+
` options: { redirectTo: \`\${origin}/auth/callback\` },`,
|
|
560
|
+
` });`,
|
|
561
|
+
];
|
|
562
|
+
if (target.kind === "local") {
|
|
563
|
+
nextSteps.push(``,
|
|
564
|
+
// The provider row lives in `auth.custom_oauth_providers`, which a
|
|
565
|
+
// plain `supabase stop`/`supabase start` leaves alone — but `supabase
|
|
566
|
+
// db reset` and `supabase stop --no-backup` both erase it along with
|
|
567
|
+
// the rest of the database, silently turning this sign-in button off.
|
|
568
|
+
`Re-run \`devtools oauth\` after \`supabase db reset\` or \`supabase stop --no-backup\` —`, `either wipes this provider along with the rest of the local database.`);
|
|
569
|
+
}
|
|
570
|
+
note(nextSteps.join("\n"), "Next steps");
|
|
571
|
+
}
|
package/dist/pick.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { VaultTarget } from "@devdogsuga/env";
|
|
2
|
+
/**
|
|
3
|
+
* `null` means the failure has already been explained; set an exit code and
|
|
4
|
+
* stop. It is not the same as a cancel, which exits the process outright.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveVaultTarget(given: string | undefined, message: string): Promise<VaultTarget | null>;
|
package/dist/pick.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing a target when the command line did not name one.
|
|
3
|
+
*
|
|
4
|
+
* `--target` is optional, and when it is absent this asks rather than guessing.
|
|
5
|
+
* There is no safe default to fall back to, because the targets differ
|
|
6
|
+
* precisely in how much damage picking the wrong one does.
|
|
7
|
+
*
|
|
8
|
+
* Three properties the prompt is built around:
|
|
9
|
+
*
|
|
10
|
+
* * Production is never the highlighted option. The list is ordered least-
|
|
11
|
+
* to most-dangerous (that order lives in the target table), so a reflexive
|
|
12
|
+
* Enter selects `preflight`.
|
|
13
|
+
* * It refuses to prompt where nobody can answer. A prompt on a
|
|
14
|
+
* non-interactive stdin hangs until the job times out, which reads as a
|
|
15
|
+
* broken tool rather than a missing argument.
|
|
16
|
+
* * `development` is refused by name, not by silence. It is a real target
|
|
17
|
+
* with a file, and it is the one target with no Bitwarden project, so
|
|
18
|
+
* `pull`/`push`/`audit` cannot do anything with it. Letting it fall into
|
|
19
|
+
* "not a target" would say something false; the message says which fact
|
|
20
|
+
* rules it out.
|
|
21
|
+
*/
|
|
22
|
+
import { select } from "@clack/prompts";
|
|
23
|
+
import { loadEnv } from "./repo/peers.js";
|
|
24
|
+
import { noVaultProjectHints, NoVaultProjectError, } from "./bws/environments.js";
|
|
25
|
+
import { explain, unwrap } from "./ui.js";
|
|
26
|
+
/** Short enough to sit beside the name; the specs' summaries are paragraphs. */
|
|
27
|
+
const VAULT_HINTS = {
|
|
28
|
+
preflight: "read-only credentials for the pre-promotion checks",
|
|
29
|
+
staging: "the everyday one — safe to overwrite",
|
|
30
|
+
production: "⚠️ the live values",
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* `null` means the failure has already been explained; set an exit code and
|
|
34
|
+
* stop. It is not the same as a cancel, which exits the process outright.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveVaultTarget(given, message) {
|
|
37
|
+
const { ENV_TARGETS, VAULT_TARGETS, isEnvTarget, isVaultTarget } = await loadEnv();
|
|
38
|
+
if (given !== undefined) {
|
|
39
|
+
if (isVaultTarget(given))
|
|
40
|
+
return given;
|
|
41
|
+
if (isEnvTarget(given)) {
|
|
42
|
+
explain(new NoVaultProjectError(given).message, "", noVaultProjectHints());
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
explain(`"${given}" is not a target.`, "", [
|
|
46
|
+
`Try one of: ${ENV_TARGETS.join(", ")}`,
|
|
47
|
+
`Only ${VAULT_TARGETS.join(", ")} have a Bitwarden project.`,
|
|
48
|
+
]);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!process.stdin.isTTY) {
|
|
52
|
+
explain("No target given, and there is nobody here to ask.", "", [
|
|
53
|
+
`Pass --target: ${VAULT_TARGETS.join(" | ")}`,
|
|
54
|
+
]);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return unwrap(await select({
|
|
58
|
+
message,
|
|
59
|
+
options: VAULT_TARGETS.map((value) => ({
|
|
60
|
+
value,
|
|
61
|
+
hint: VAULT_HINTS[value],
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlannerDb } from "./db.js";
|
|
2
|
+
/** Constants, so `PlannerDb.run` never sees a composed string. */
|
|
3
|
+
export declare const CHECK_IDENTITY = "select current_user as who";
|
|
4
|
+
/**
|
|
5
|
+
* `platform.profile` because it exists in every tier and holds real member
|
|
6
|
+
* data in production: the exact rows §3.5 refuses to put behind `main`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CHECK_OVERREACH = "select * from platform.profile limit 1";
|
|
9
|
+
export declare const CHECK_MIGRATIONS = "select count(*)::int as n from supabase_migrations.schema_migrations";
|
|
10
|
+
export interface PlannerVerdict {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
/** One line per check, in the order run; the report either way. */
|
|
13
|
+
lines: string[];
|
|
14
|
+
/** Set when `ok` is false: what to go and fix. */
|
|
15
|
+
problem?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs the three checks over an OPEN connection; never closes it.
|
|
19
|
+
*
|
|
20
|
+
* Any error from check 2 counts as "cannot read": the interesting distinction
|
|
21
|
+
* is readable versus not, and a role that cannot even enumerate the schema
|
|
22
|
+
* (the validated state) throws just as loudly as one denied on the table.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkPlanner(db: PlannerDb): Promise<PlannerVerdict>;
|
|
25
|
+
/** Postgres 3F000 (invalid_schema_name), with a text fallback. */
|
|
26
|
+
export declare function isMissingSchema(error: unknown): boolean;
|