@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.
Files changed (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The loopback HTTP listener the one-click connect flow waits on.
3
+ *
4
+ * `devtools oauth`'s connect flow opens the platform's `/tools/oauth/connect`
5
+ * page in the contributor's browser, and the platform redirects back to a
6
+ * `redirect_uri` of `http://127.0.0.1:<port>/callback` this process itself
7
+ * is listening on — a random, ephemeral port, requested by passing `0` to
8
+ * `listen()`, so two `devtools oauth` runs on the same machine never
9
+ * collide. `127.0.0.1` specifically, not `0.0.0.0`: this must never accept a
10
+ * connection from anything but the browser running ON this machine.
11
+ *
12
+ * Every exit path — success, `error=access_denied` (or any other `error=`),
13
+ * a request with neither, and the 5-minute timeout — closes the server
14
+ * exactly once. That is the one invariant every caller of `start()` may
15
+ * lean on: `close()` (also exposed for a caller that gives up early, e.g.
16
+ * Ctrl-C) is always safe to call more than once, and the server is never
17
+ * left listening after `result` settles.
18
+ */
19
+ import { createServer } from "node:http";
20
+ /** Default: as long as this repo's OAuth wizard reasonably keeps a browser tab open unattended. */
21
+ export const DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60_000;
22
+ /** The platform denied the request, or reported some other OAuth `error=`. */
23
+ export class CallbackDeniedError extends Error {
24
+ errorCode;
25
+ state;
26
+ constructor(errorCode, state, description) {
27
+ super(description ? `${errorCode}: ${description}` : errorCode);
28
+ this.errorCode = errorCode;
29
+ this.state = state;
30
+ this.name = "CallbackDeniedError";
31
+ }
32
+ }
33
+ /** No callback arrived within the timeout — the browser was never opened, or the tab was abandoned. */
34
+ export class CallbackTimeoutError extends Error {
35
+ constructor(timeoutMs) {
36
+ super(`No response from the browser after ${Math.round(timeoutMs / 1000)}s. ` +
37
+ "Run the command again, and complete the connection in the browser tab it opens.");
38
+ this.name = "CallbackTimeoutError";
39
+ }
40
+ }
41
+ /** The callback hit `/callback` but had neither `code` nor `error` — not a shape this flow expects. */
42
+ export class CallbackMalformedError extends Error {
43
+ constructor() {
44
+ super("The browser's callback had neither a code nor an error.");
45
+ this.name = "CallbackMalformedError";
46
+ }
47
+ }
48
+ /**
49
+ * The callback's `state` did not match the one this run generated (see
50
+ * `pkce.ts`'s `generateState`) — the defining CSRF defense of this whole
51
+ * flow. Any mismatch is treated the same regardless of cause (a stale tab
52
+ * from a previous run, a forged callback, or a bug): refuse and say so,
53
+ * never silently proceed with the wrong request.
54
+ */
55
+ export class StateMismatchError extends Error {
56
+ constructor() {
57
+ super("The browser's callback carried a different state than this run generated " +
58
+ "— the connection may have been tampered with. Run the command again.");
59
+ this.name = "StateMismatchError";
60
+ }
61
+ }
62
+ /** Throws `StateMismatchError` unless `callback.state` matches `expected`; returns `callback` unchanged otherwise. */
63
+ export function verifyState(expected, callback) {
64
+ if (callback.state !== expected)
65
+ throw new StateMismatchError();
66
+ return callback;
67
+ }
68
+ function renderCallbackPage(heading, body) {
69
+ return ("<!doctype html><html><head><meta charset=\"utf-8\"><title>DevDogs devtools</title></head>" +
70
+ `<body style="font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; text-align: center;">` +
71
+ `<h1>${heading}</h1><p>${body}</p></body></html>`);
72
+ }
73
+ const SUCCESS_PAGE = renderCallbackPage("Connected", "You can close this tab and return to your terminal.");
74
+ const DENIED_PAGE = renderCallbackPage("Connection not completed", "You can close this tab and return to your terminal.");
75
+ /**
76
+ * Starts the listener and resolves once it is actually accepting
77
+ * connections (i.e. once the real port is known). `result` is the separate
78
+ * promise that settles once a callback (or the timeout) has been handled —
79
+ * most callers `await start(...)` once, then `await listener.result`.
80
+ */
81
+ export function start(opts = {}) {
82
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_CALLBACK_TIMEOUT_MS;
83
+ return new Promise((resolveListener, rejectListener) => {
84
+ let settled = false;
85
+ let resolveResult;
86
+ let rejectResult;
87
+ const result = new Promise((res, rej) => {
88
+ resolveResult = res;
89
+ rejectResult = rej;
90
+ });
91
+ // `result` settles the instant the callback request arrives, which can
92
+ // be before the caller gets around to `await`ing it (e.g. this
93
+ // function's own `start(...)` promise has to resolve first). Node
94
+ // reports an unhandled rejection for any promise that goes a full
95
+ // microtask turn with no listener at all, even one a caller attaches
96
+ // moments later — this dummy handler exists purely to be that first
97
+ // listener; it does not consume the rejection for real callers, who
98
+ // still get their own `await`/`.catch` on the same promise.
99
+ result.catch(() => { });
100
+ const server = createServer((req, res) => {
101
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
102
+ if (url.pathname !== "/callback") {
103
+ res.writeHead(404).end();
104
+ return;
105
+ }
106
+ const code = url.searchParams.get("code");
107
+ const state = url.searchParams.get("state") ?? undefined;
108
+ const error = url.searchParams.get("error");
109
+ if (error) {
110
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DENIED_PAGE);
111
+ settle(() => rejectResult(new CallbackDeniedError(error, state, url.searchParams.get("error_description") ?? undefined)));
112
+ return;
113
+ }
114
+ if (!code || !state) {
115
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(DENIED_PAGE);
116
+ settle(() => rejectResult(new CallbackMalformedError()));
117
+ return;
118
+ }
119
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(SUCCESS_PAGE);
120
+ settle(() => resolveResult({ code, state }));
121
+ });
122
+ const timer = setTimeout(() => {
123
+ settle(() => rejectResult(new CallbackTimeoutError(timeoutMs)));
124
+ }, timeoutMs);
125
+ // Never keep the process alive on this timer alone — a caller that
126
+ // already got its result (or gave up and called close()) must be able
127
+ // to exit immediately.
128
+ timer.unref();
129
+ const close = () => {
130
+ clearTimeout(timer);
131
+ server.close();
132
+ };
133
+ /** Runs `settleFn` exactly once, always closing the listener. */
134
+ function settle(settleFn) {
135
+ if (settled)
136
+ return;
137
+ settled = true;
138
+ settleFn();
139
+ close();
140
+ }
141
+ server.once("error", (err) => {
142
+ if (!settled) {
143
+ settled = true;
144
+ rejectListener(err);
145
+ return;
146
+ }
147
+ // An error after the listener already started (e.g. a second request
148
+ // racing the close()) has nowhere useful to go; `result` already
149
+ // settled, so this is not the caller's problem.
150
+ });
151
+ server.listen(0, "127.0.0.1", () => {
152
+ const address = server.address();
153
+ const port = typeof address === "object" && address !== null ? address.port : 0;
154
+ resolveListener({
155
+ port,
156
+ redirectUri: `http://127.0.0.1:${port}/callback`,
157
+ result,
158
+ close,
159
+ });
160
+ });
161
+ });
162
+ }
@@ -0,0 +1,6 @@
1
+ /** A cryptographically random PKCE code verifier: 32 bytes, base64url-encoded (43 characters). */
2
+ export declare function generateCodeVerifier(): string;
3
+ /** The S256 code challenge for `verifier` — `BASE64URL(SHA256(verifier))`. */
4
+ export declare function codeChallengeFor(verifier: string): string;
5
+ /** A random `state` value, opaque to the caller, tying an authorization request to its callback. */
6
+ export declare function generateState(): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PKCE (RFC 7636) helpers for the one-click OAuth connect flow.
3
+ *
4
+ * `devtools oauth` is a public client with no way to hold a secret (it is a
5
+ * CLI running on a contributor's machine), so the platform's `/tools/oauth/
6
+ * connect` flow relies on PKCE instead: a random verifier stays local, only
7
+ * its SHA-256 challenge crosses the network in the authorization request,
8
+ * and the verifier itself is sent once, at the very end, to the token
9
+ * exchange — see `exchange.ts`. `state` is the separate, unrelated defense
10
+ * against a forged callback (`loopback.ts` checks it), not part of PKCE
11
+ * proper, but generated the same way and kept here since every caller wants
12
+ * both together.
13
+ */
14
+ import { createHash, randomBytes } from "node:crypto";
15
+ /** Base64url, no padding — the encoding both the verifier and challenge use (RFC 7636 §4.1, §4.2). */
16
+ function base64url(buf) {
17
+ return buf
18
+ .toString("base64")
19
+ .replace(/\+/g, "-")
20
+ .replace(/\//g, "_")
21
+ .replace(/=+$/, "");
22
+ }
23
+ /** A cryptographically random PKCE code verifier: 32 bytes, base64url-encoded (43 characters). */
24
+ export function generateCodeVerifier() {
25
+ return base64url(randomBytes(32));
26
+ }
27
+ /** The S256 code challenge for `verifier` — `BASE64URL(SHA256(verifier))`. */
28
+ export function codeChallengeFor(verifier) {
29
+ return base64url(createHash("sha256").update(verifier).digest());
30
+ }
31
+ /** A random `state` value, opaque to the caller, tying an authorization request to its callback. */
32
+ export function generateState() {
33
+ return base64url(randomBytes(16));
34
+ }
@@ -0,0 +1,54 @@
1
+ import type { DeployEnvironment } from "@devdogsuga/env";
2
+ import { type LocalSupabaseConfig } from "./db.js";
3
+ export type TargetKind = "local" | "hosted";
4
+ export interface ConnectTarget extends LocalSupabaseConfig {
5
+ kind: TargetKind;
6
+ }
7
+ /** The env var names a hosted target's URL/service-role key carry OUTSIDE a checkout. */
8
+ export declare const HOSTED_TARGET_ENV_KEYS: {
9
+ readonly url: "SUPABASE_URL";
10
+ readonly serviceRoleKey: "SUPABASE_SERVICE_ROLE_KEY";
11
+ };
12
+ /** `supabase status` failed, or the two keys it reports are incomplete — see `db.ts`. */
13
+ export declare function resolveLocalTarget(cwd: string): Promise<ConnectTarget>;
14
+ /**
15
+ * Reads a hosted target's URL/service-role key from `.env.local`, then
16
+ * `.env`, in `cwd`. Returns an empty object (not a throw) when neither file
17
+ * has both keys — that is the ordinary "prompt for them" case, not an error.
18
+ */
19
+ export declare function readHostedTargetFromEnvFiles(cwd: string): Partial<LocalSupabaseConfig>;
20
+ /** A checkout's env handling has no `API_URL`/`SECRET_KEY` for the chosen tier. */
21
+ export declare class HostedCredentialsMissingError extends Error {
22
+ readonly tier: DeployEnvironment;
23
+ constructor(tier: DeployEnvironment);
24
+ }
25
+ /** The chosen tier had no tier to resolve to (declined the picker, or none available). */
26
+ export declare class NoTierResolvedError extends Error {
27
+ constructor();
28
+ }
29
+ export interface RepoHostedTargetDeps {
30
+ /** `tier.ts`'s `resolveTier`, or a test double with the same shape. */
31
+ resolveTier: (tierArg: string | undefined, message: string) => Promise<DeployEnvironment | null>;
32
+ /**
33
+ * Loads the chosen tier's env into `process.env` (or `env`, when given) —
34
+ * `@devdogsuga/env/session`'s `enterEnvironment`, called with
35
+ * `override: true`: the launcher already entered `development` for this
36
+ * envFree command (see `launch.ts`), and a DELIBERATE, later choice of a
37
+ * different tier here must win over that earlier one, not lose to it.
38
+ */
39
+ enterEnvironment: (tier: DeployEnvironment) => Promise<void>;
40
+ /** Injectable for tests; defaults to `process.env`. */
41
+ env?: NodeJS.ProcessEnv;
42
+ }
43
+ /**
44
+ * Resolves a hosted target from INSIDE a DevDogsUGA checkout: asks which
45
+ * tier (via `resolveTier`, which itself only prompts when more than one
46
+ * tier's env file is present), enters that tier's environment, and reads
47
+ * the platform app's `API_URL`/`SECRET_KEY` back out of it.
48
+ *
49
+ * Throws `NoTierResolvedError` when no tier was resolved (an invalid
50
+ * `--tier`, or a declined picker — `resolveTier` already reported the
51
+ * reason on stderr) and `HostedCredentialsMissingError` when the resolved
52
+ * tier's env has no `API_URL`/`SECRET_KEY` at all.
53
+ */
54
+ export declare function resolveHostedTargetInRepo(tierArg: string | undefined, deps: RepoHostedTargetDeps): Promise<ConnectTarget>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Resolving the Supabase project `devtools oauth` configures — the "target".
3
+ *
4
+ * Two kinds:
5
+ *
6
+ * * `local` — whatever Supabase project `supabase status` reports for
7
+ * `cwd` (`db.ts`'s `detectLocalSupabase`, unchanged from the original
8
+ * wizard).
9
+ * * `hosted` — a Supabase project this machine has no local stack for at
10
+ * all: staging, production, or someone else's. Its URL and service-role
11
+ * key come from wherever devtools already knows to look, in order:
12
+ *
13
+ * 1. Inside a DevDogsUGA checkout, from the checked-out repo's own env
14
+ * handling: the platform app's `API_URL`/`SECRET_KEY` for a tier
15
+ * the contributor picks (TASK-345's forward note — tier resolution
16
+ * stays LAZY, done here rather than by the launcher, and only when
17
+ * "hosted" is actually chosen).
18
+ * 2. Outside a checkout — a workshop repo, most likely — from
19
+ * `.env.local` or `.env` in `cwd`, read for `SUPABASE_URL` /
20
+ * `SUPABASE_SERVICE_ROLE_KEY` (the names the wider Supabase
21
+ * ecosystem already uses for exactly this pair, so a project that
22
+ * already has them needs no new convention).
23
+ * 3. Neither: the wizard prompts, same as the original manual flow
24
+ * always did for local.
25
+ *
26
+ * Every function below returns data or throws; none of them prompt. Prompting
27
+ * is `wizard.ts`'s job — this module stays testable without `@clack/prompts`
28
+ * in the loop.
29
+ */
30
+ import { existsSync, readFileSync } from "node:fs";
31
+ import { join } from "node:path";
32
+ import { parse } from "dotenv";
33
+ import { detectLocalSupabase } from "./db.js";
34
+ /** The env var names a hosted target's URL/service-role key carry OUTSIDE a checkout. */
35
+ export const HOSTED_TARGET_ENV_KEYS = {
36
+ url: "SUPABASE_URL",
37
+ serviceRoleKey: "SUPABASE_SERVICE_ROLE_KEY",
38
+ };
39
+ /** `supabase status` failed, or the two keys it reports are incomplete — see `db.ts`. */
40
+ export async function resolveLocalTarget(cwd) {
41
+ const config = detectLocalSupabase(cwd);
42
+ return { ...config, kind: "local" };
43
+ }
44
+ /**
45
+ * Reads a hosted target's URL/service-role key from `.env.local`, then
46
+ * `.env`, in `cwd`. Returns an empty object (not a throw) when neither file
47
+ * has both keys — that is the ordinary "prompt for them" case, not an error.
48
+ */
49
+ export function readHostedTargetFromEnvFiles(cwd) {
50
+ for (const file of [".env.local", ".env"]) {
51
+ const path = join(cwd, file);
52
+ if (!existsSync(path))
53
+ continue;
54
+ const parsed = parse(readFileSync(path, "utf-8"));
55
+ const apiUrl = parsed[HOSTED_TARGET_ENV_KEYS.url];
56
+ const serviceRoleKey = parsed[HOSTED_TARGET_ENV_KEYS.serviceRoleKey];
57
+ if (apiUrl && serviceRoleKey)
58
+ return { apiUrl, serviceRoleKey };
59
+ }
60
+ return {};
61
+ }
62
+ /** A checkout's env handling has no `API_URL`/`SECRET_KEY` for the chosen tier. */
63
+ export class HostedCredentialsMissingError extends Error {
64
+ tier;
65
+ constructor(tier) {
66
+ super(`${tier}'s API_URL/SECRET_KEY are not available in this checkout — run ` +
67
+ `\`pnpm devtools env pull --target ${tier}\` first.`);
68
+ this.tier = tier;
69
+ this.name = "HostedCredentialsMissingError";
70
+ }
71
+ }
72
+ /** The chosen tier had no tier to resolve to (declined the picker, or none available). */
73
+ export class NoTierResolvedError extends Error {
74
+ constructor() {
75
+ super("No tier was resolved for the hosted Supabase project.");
76
+ this.name = "NoTierResolvedError";
77
+ }
78
+ }
79
+ /**
80
+ * Resolves a hosted target from INSIDE a DevDogsUGA checkout: asks which
81
+ * tier (via `resolveTier`, which itself only prompts when more than one
82
+ * tier's env file is present), enters that tier's environment, and reads
83
+ * the platform app's `API_URL`/`SECRET_KEY` back out of it.
84
+ *
85
+ * Throws `NoTierResolvedError` when no tier was resolved (an invalid
86
+ * `--tier`, or a declined picker — `resolveTier` already reported the
87
+ * reason on stderr) and `HostedCredentialsMissingError` when the resolved
88
+ * tier's env has no `API_URL`/`SECRET_KEY` at all.
89
+ */
90
+ export async function resolveHostedTargetInRepo(tierArg, deps) {
91
+ const tier = await deps.resolveTier(tierArg, "Which tier's Supabase project are you connecting?");
92
+ if (!tier)
93
+ throw new NoTierResolvedError();
94
+ await deps.enterEnvironment(tier);
95
+ const env = deps.env ?? process.env;
96
+ const apiUrl = env.API_URL;
97
+ const serviceRoleKey = env.SECRET_KEY;
98
+ if (!apiUrl || !serviceRoleKey) {
99
+ throw new HostedCredentialsMissingError(tier);
100
+ }
101
+ return { apiUrl, serviceRoleKey, kind: "hosted" };
102
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Chooses which connect transport `devtools oauth`'s one-click branch uses:
3
+ * `loopback` (TASK-351 — a local HTTP listener catches the platform's
4
+ * redirect) or `device` (TASK-352 — a short code, approved in a browser
5
+ * running anywhere, polled for). The device flow's browser step need not run
6
+ * on this machine at all, which is exactly what a loopback redirect cannot
7
+ * offer: nothing can redirect back to a listener the approving browser can't
8
+ * reach.
9
+ *
10
+ * Three ways this gets decided, in order:
11
+ *
12
+ * 1. `--device`/`--loopback` on the command line — an explicit, later
13
+ * choice always wins over any inference below.
14
+ * 2. The environment: SSH (`SSH_CONNECTION`/`SSH_TTY`), a GitHub Codespace
15
+ * (`CODESPACES=true`), or a dev container (`REMOTE_CONTAINERS`/
16
+ * `DEVCONTAINER`) all mean the browser that will approve the
17
+ * connection is almost certainly NOT running on this machine, so a
18
+ * loopback redirect would just hang.
19
+ * 3. Neither of the above: try loopback. `wizard.ts` itself handles the
20
+ * remaining case — the loopback listener failing to start — by
21
+ * falling back to device at that point, since that failure is only
22
+ * known once `loopback.ts`'s `start()` has actually been tried.
23
+ */
24
+ export type ConnectTransport = "loopback" | "device";
25
+ export interface TransportDecision {
26
+ transport: ConnectTransport;
27
+ reason: string;
28
+ }
29
+ /** Human-readable reason a remote/headless environment was detected, or `null` if none was. */
30
+ export declare function detectRemoteEnvironmentReason(env?: NodeJS.ProcessEnv): string | null;
31
+ /**
32
+ * Resolves the transport BEFORE a loopback listener is ever attempted.
33
+ * `forceDevice`/`forceLoopback` come from `--device`/`--loopback`; `cli.ts`
34
+ * refuses both being set at once before this is ever called, so at most one
35
+ * is true here.
36
+ */
37
+ export declare function decideTransport({ forceDevice, forceLoopback, env, }: {
38
+ forceDevice?: boolean;
39
+ forceLoopback?: boolean;
40
+ env?: NodeJS.ProcessEnv;
41
+ }): TransportDecision;
@@ -0,0 +1,32 @@
1
+ /** Human-readable reason a remote/headless environment was detected, or `null` if none was. */
2
+ export function detectRemoteEnvironmentReason(env = process.env) {
3
+ if (env.SSH_CONNECTION || env.SSH_TTY) {
4
+ return "an SSH session (SSH_CONNECTION/SSH_TTY is set)";
5
+ }
6
+ if (env.CODESPACES === "true") {
7
+ return "a GitHub Codespace (CODESPACES=true)";
8
+ }
9
+ if (env.REMOTE_CONTAINERS || env.DEVCONTAINER) {
10
+ return "a dev container (REMOTE_CONTAINERS/DEVCONTAINER is set)";
11
+ }
12
+ return null;
13
+ }
14
+ /**
15
+ * Resolves the transport BEFORE a loopback listener is ever attempted.
16
+ * `forceDevice`/`forceLoopback` come from `--device`/`--loopback`; `cli.ts`
17
+ * refuses both being set at once before this is ever called, so at most one
18
+ * is true here.
19
+ */
20
+ export function decideTransport({ forceDevice, forceLoopback, env = process.env, }) {
21
+ if (forceDevice) {
22
+ return { transport: "device", reason: "--device was passed" };
23
+ }
24
+ if (forceLoopback) {
25
+ return { transport: "loopback", reason: "--loopback was passed" };
26
+ }
27
+ const remoteReason = detectRemoteEnvironmentReason(env);
28
+ if (remoteReason) {
29
+ return { transport: "device", reason: `detected ${remoteReason}` };
30
+ }
31
+ return { transport: "loopback", reason: "no remote environment detected" };
32
+ }
@@ -0,0 +1,33 @@
1
+ import { type ExchangeResult } from "./exchange.js";
2
+ import { type ConnectTransport } from "./transport.js";
3
+ import { type ConnectTarget } from "./target.js";
4
+ /**
5
+ * Step 2 (one-click branch): decides loopback vs. device (see `transport.ts`)
6
+ * and runs it.
7
+ *
8
+ * `transportOverride` is `--device`/`--loopback` (see `commands.ts`'s `oauth`
9
+ * node); `cli.ts` refuses passing both, so at most one is set here. Left
10
+ * undefined, the transport is auto-detected from the environment.
11
+ *
12
+ * `startLoopback()` is called HERE, not inside `connectOneClick`, and ONLY
13
+ * its failure falls back to device — every failure that can happen once the
14
+ * listener is actually up (denial, timeout, a bad exchange — all of which
15
+ * `connectOneClick` reports via `bail()`, or an unexpected rethrow) is that
16
+ * flow's own and must propagate unchanged, not be mistaken for "the listener
17
+ * never started" and silently rerouted to a different transport with a
18
+ * misleading warning. Loopback forced via `--loopback` never falls back at
19
+ * all: a start failure there is left to propagate as-is.
20
+ */
21
+ export declare function connectViaOneClick(target: ConnectTarget, platformUrl: string, cwd: string, transportOverride: ConnectTransport | undefined): Promise<ExchangeResult>;
22
+ /**
23
+ * The wizard. `baseUrlOverride`/`platformUrlOverride`, when given, skip
24
+ * their respective prompts (only reachable via "Paste credentials instead"
25
+ * and the one-click flow respectively). `transportOverride` (`--device`/
26
+ * `--loopback`) forces the one-click flow's transport — see
27
+ * `connectViaOneClick` — and is ignored on the "Paste credentials instead"
28
+ * branch, which has no transport of its own.
29
+ *
30
+ * Intro and outro are the caller's, so this composes into the devtools menu
31
+ * without drawing a second box inside the first.
32
+ */
33
+ export declare function runOAuthSetup(baseUrlOverride?: string, platformUrlOverride?: string, transportOverride?: ConnectTransport): Promise<void>;