@devdogsuga/devtools 0.1.25 → 0.1.27

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