@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,190 @@
1
+ export class DeviceError extends Error {
2
+ kind;
3
+ retryAfterSeconds;
4
+ constructor(kind, message, retryAfterSeconds) {
5
+ super(message);
6
+ this.kind = kind;
7
+ this.retryAfterSeconds = retryAfterSeconds;
8
+ this.name = "DeviceError";
9
+ }
10
+ }
11
+ export class PollError extends Error {
12
+ kind;
13
+ constructor(kind, message) {
14
+ super(message);
15
+ this.kind = kind;
16
+ this.name = "PollError";
17
+ }
18
+ }
19
+ function deviceCodeUrl(platformUrl) {
20
+ return `${platformUrl.replace(/\/+$/, "")}/tools/oauth/device/code`;
21
+ }
22
+ function deviceTokenUrl(platformUrl) {
23
+ return `${platformUrl.replace(/\/+$/, "")}/tools/oauth/device/token`;
24
+ }
25
+ function retryAfterSecondsFrom(response) {
26
+ const header = response.headers.get("retry-after");
27
+ const seconds = header ? Number(header) : undefined;
28
+ return Number.isFinite(seconds) ? seconds : undefined;
29
+ }
30
+ /**
31
+ * Requests a device code. Mirrors `exchange.ts`'s `exchangeCode` in shape:
32
+ * every failure is a distinct `DeviceError` kind, a 429 carries
33
+ * `Retry-After` when the platform sent one, and a 200 whose body is not
34
+ * valid JSON or is missing a required field is `malformed` rather than
35
+ * handing the caller `undefined` fields.
36
+ */
37
+ export async function requestDeviceCode({ platformUrl, label, callbackUri, fetchImpl = fetch, }) {
38
+ const url = deviceCodeUrl(platformUrl);
39
+ let response;
40
+ try {
41
+ response = await fetchImpl(url, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify({ label, callback_uri: callbackUri }),
45
+ });
46
+ }
47
+ catch (err) {
48
+ throw new DeviceError("network", `Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`);
49
+ }
50
+ if (response.status === 429) {
51
+ const retryAfterSeconds = retryAfterSecondsFrom(response);
52
+ throw new DeviceError("rate_limited", `${url} is rate-limited` +
53
+ (retryAfterSeconds !== undefined ? ` — retry after ${retryAfterSeconds}s.` : "."), retryAfterSeconds);
54
+ }
55
+ let body;
56
+ try {
57
+ body = await response.json();
58
+ }
59
+ catch {
60
+ throw new DeviceError("malformed", `${url} did not return valid JSON (status ${response.status}).`);
61
+ }
62
+ if (!response.ok) {
63
+ const errBody = (body ?? {});
64
+ const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
65
+ const code = typeof errBody.error === "string" ? errBody.error : `HTTP ${response.status}`;
66
+ throw new DeviceError("invalid_request", description ? `${code}: ${description}` : code);
67
+ }
68
+ const result = (body ?? {});
69
+ if (typeof result.device_code !== "string" ||
70
+ typeof result.user_code !== "string" ||
71
+ typeof result.verification_uri !== "string" ||
72
+ typeof result.verification_uri_complete !== "string" ||
73
+ typeof result.expires_in !== "number" ||
74
+ typeof result.interval !== "number") {
75
+ throw new DeviceError("malformed", `${url} returned 200 but was missing a required device-code field.`);
76
+ }
77
+ return {
78
+ deviceCode: result.device_code,
79
+ userCode: result.user_code,
80
+ verificationUri: result.verification_uri,
81
+ verificationUriComplete: result.verification_uri_complete,
82
+ expiresIn: result.expires_in,
83
+ interval: result.interval,
84
+ };
85
+ }
86
+ /** Network failures while polling get this many retries before `pollForToken` gives up. */
87
+ const MAX_NETWORK_RETRIES = 3;
88
+ /** Exponential backoff for a network retry, capped well under the poll interval's own cadence. */
89
+ function networkBackoffSeconds(attempt) {
90
+ return Math.min(2 ** attempt, 30);
91
+ }
92
+ /**
93
+ * Polls `.../tools/oauth/device/token` on the server's cadence until the
94
+ * user approves (or denies) the request in their browser, the device code
95
+ * expires, or the server rejects it outright.
96
+ *
97
+ * `authorization_pending` keeps polling at the current interval;
98
+ * `slow_down` adds 5 seconds to it and keeps polling (per the wire
99
+ * contract); a 429 waits out `Retry-After` (or the current interval, if the
100
+ * platform sent none) and keeps polling; every other error is terminal.
101
+ * Local expiry (`now() - start >= expiresInSeconds`) is reported the same
102
+ * way as the server's own `expired_token` — the caller does not need to
103
+ * tell the two apart.
104
+ *
105
+ * A network failure while polling retries with backoff up to
106
+ * `MAX_NETWORK_RETRIES` times before it is reported as `PollError("network")`.
107
+ */
108
+ export async function pollForToken({ platformUrl, deviceCode, intervalSeconds, expiresInSeconds, fetchImpl = fetch, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = () => Date.now(), }) {
109
+ const url = deviceTokenUrl(platformUrl);
110
+ const deadline = now() + expiresInSeconds * 1000;
111
+ let interval = intervalSeconds;
112
+ let networkFailures = 0;
113
+ // Set only by a branch that already slept its own wait (a network retry's
114
+ // backoff, or a 429's Retry-After) — skips the ordinary top-of-loop
115
+ // interval sleep so those waits are not doubled up with it.
116
+ let skipIntervalSleep = false;
117
+ while (true) {
118
+ if (now() >= deadline) {
119
+ throw new PollError("expired_token", "The device code expired before it was approved. Run the command again.");
120
+ }
121
+ if (!skipIntervalSleep)
122
+ await sleep(interval * 1000);
123
+ skipIntervalSleep = false;
124
+ let response;
125
+ try {
126
+ response = await fetchImpl(url, {
127
+ method: "POST",
128
+ headers: { "content-type": "application/json" },
129
+ body: JSON.stringify({ device_code: deviceCode }),
130
+ });
131
+ }
132
+ catch (err) {
133
+ networkFailures += 1;
134
+ if (networkFailures > MAX_NETWORK_RETRIES) {
135
+ throw new PollError("network", `Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`);
136
+ }
137
+ await sleep(networkBackoffSeconds(networkFailures) * 1000);
138
+ skipIntervalSleep = true;
139
+ continue;
140
+ }
141
+ networkFailures = 0;
142
+ if (response.status === 429) {
143
+ const retryAfterSeconds = retryAfterSecondsFrom(response);
144
+ await sleep((retryAfterSeconds ?? interval) * 1000);
145
+ skipIntervalSleep = true;
146
+ continue;
147
+ }
148
+ let body;
149
+ try {
150
+ body = await response.json();
151
+ }
152
+ catch {
153
+ throw new PollError("malformed", `${url} did not return valid JSON (status ${response.status}).`);
154
+ }
155
+ if (response.ok) {
156
+ const result = (body ?? {});
157
+ if (typeof result.client_id !== "string" ||
158
+ typeof result.client_secret !== "string" ||
159
+ typeof result.issuer !== "string") {
160
+ throw new PollError("malformed", `${url} returned 200 but was missing client_id, client_secret, or issuer.`);
161
+ }
162
+ return {
163
+ clientId: result.client_id,
164
+ clientSecret: result.client_secret,
165
+ issuer: result.issuer,
166
+ };
167
+ }
168
+ const errBody = (body ?? {});
169
+ const errorCode = typeof errBody.error === "string" ? errBody.error : undefined;
170
+ const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
171
+ switch (errorCode) {
172
+ case "authorization_pending":
173
+ continue;
174
+ case "slow_down":
175
+ interval += 5;
176
+ continue;
177
+ case "access_denied":
178
+ throw new PollError("access_denied", description ?? "Connection declined in the browser.");
179
+ case "expired_token":
180
+ throw new PollError("expired_token", description ?? "The device code expired. Run the command again.");
181
+ case "invalid_grant":
182
+ throw new PollError("invalid_grant", description ?? "invalid_grant");
183
+ case "invalid_request":
184
+ default: {
185
+ const code = errorCode ?? `HTTP ${response.status}`;
186
+ throw new PollError("invalid_request", description ? `${code}: ${description}` : code);
187
+ }
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OIDC discovery for the DevDogs OAuth server.
3
+ *
4
+ * The wizard used to build the issuer it registered with Supabase as
5
+ * `${baseUrl}/auth/v1` — an assumption, not a fact. Supabase Auth's own
6
+ * discovery document at that same base advertises a DIFFERENT issuer (its
7
+ * project ref host, e.g. `https://<ref>.supabase.co/auth/v1`, rather than
8
+ * the custom domain a caller reaches it through), and Supabase's OIDC
9
+ * provider refuses to register a custom provider whose declared issuer
10
+ * disagrees with what discovery reports for it. Reading the issuer FROM
11
+ * discovery, rather than constructing it, keeps this correct regardless of
12
+ * which host the platform's own discovery document happens to advertise —
13
+ * including once the platform starts advertising its custom domain instead
14
+ * of the underlying project host.
15
+ */
16
+ /** Thrown when discovery cannot be fetched or does not shape up as expected. */
17
+ export declare class DiscoveryError extends Error {
18
+ }
19
+ /**
20
+ * Fetches `${baseUrl}/auth/v1/.well-known/openid-configuration` and returns
21
+ * the `issuer` it advertises.
22
+ *
23
+ * Every failure names the discovery URL it was trying to reach — a network
24
+ * failure, a non-2xx response, unparsable JSON, and a missing/non-string
25
+ * `issuer` field are each reported distinctly, so the wizard can print
26
+ * something more useful than "fetch failed".
27
+ */
28
+ export declare function fetchIssuer(baseUrl: string): Promise<string>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * OIDC discovery for the DevDogs OAuth server.
3
+ *
4
+ * The wizard used to build the issuer it registered with Supabase as
5
+ * `${baseUrl}/auth/v1` — an assumption, not a fact. Supabase Auth's own
6
+ * discovery document at that same base advertises a DIFFERENT issuer (its
7
+ * project ref host, e.g. `https://<ref>.supabase.co/auth/v1`, rather than
8
+ * the custom domain a caller reaches it through), and Supabase's OIDC
9
+ * provider refuses to register a custom provider whose declared issuer
10
+ * disagrees with what discovery reports for it. Reading the issuer FROM
11
+ * discovery, rather than constructing it, keeps this correct regardless of
12
+ * which host the platform's own discovery document happens to advertise —
13
+ * including once the platform starts advertising its custom domain instead
14
+ * of the underlying project host.
15
+ */
16
+ /** Thrown when discovery cannot be fetched or does not shape up as expected. */
17
+ export class DiscoveryError extends Error {
18
+ }
19
+ /**
20
+ * Fetches `${baseUrl}/auth/v1/.well-known/openid-configuration` and returns
21
+ * the `issuer` it advertises.
22
+ *
23
+ * Every failure names the discovery URL it was trying to reach — a network
24
+ * failure, a non-2xx response, unparsable JSON, and a missing/non-string
25
+ * `issuer` field are each reported distinctly, so the wizard can print
26
+ * something more useful than "fetch failed".
27
+ */
28
+ export async function fetchIssuer(baseUrl) {
29
+ const discoveryUrl = `${baseUrl}/auth/v1/.well-known/openid-configuration`;
30
+ let response;
31
+ try {
32
+ response = await fetch(discoveryUrl);
33
+ }
34
+ catch (err) {
35
+ throw new DiscoveryError(`Could not reach ${discoveryUrl}: ${err instanceof Error ? err.message : String(err)}`);
36
+ }
37
+ if (!response.ok) {
38
+ throw new DiscoveryError(`${discoveryUrl} returned ${response.status} ${response.statusText}`);
39
+ }
40
+ let body;
41
+ try {
42
+ body = await response.json();
43
+ }
44
+ catch {
45
+ throw new DiscoveryError(`${discoveryUrl} did not return valid JSON`);
46
+ }
47
+ const issuer = body && typeof body === "object"
48
+ ? body.issuer
49
+ : undefined;
50
+ if (typeof issuer !== "string" || issuer.length === 0) {
51
+ throw new DiscoveryError(`${discoveryUrl} response had no "issuer" field`);
52
+ }
53
+ return issuer;
54
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Writes `values` into `.env.local` in `cwd`, updating any existing
3
+ * `KEY=...` lines in place and appending keys that aren't present yet.
4
+ * Leaves all other lines (comments, unrelated vars) untouched.
5
+ */
6
+ export declare function upsertEnvLocal(cwd: string, values: Record<string, string>): void;
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Writes `values` into `.env.local` in `cwd`, updating any existing
5
+ * `KEY=...` lines in place and appending keys that aren't present yet.
6
+ * Leaves all other lines (comments, unrelated vars) untouched.
7
+ */
8
+ export function upsertEnvLocal(cwd, values) {
9
+ const path = join(cwd, ".env.local");
10
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
11
+ const lines = existing.length > 0 ? existing.split("\n") : [];
12
+ const remaining = new Map(Object.entries(values));
13
+ const updated = lines.map((line) => {
14
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
15
+ if (!match)
16
+ return line;
17
+ // The regex has exactly one capture group, so a match always fills it;
18
+ // noUncheckedIndexedAccess can't see that, hence the explicit guard.
19
+ const key = match[1];
20
+ if (key === undefined)
21
+ return line;
22
+ const value = remaining.get(key);
23
+ if (value === undefined)
24
+ return line;
25
+ remaining.delete(key);
26
+ return `${key}=${formatValue(value)}`;
27
+ });
28
+ for (const [key, value] of remaining) {
29
+ updated.push(`${key}=${formatValue(value)}`);
30
+ }
31
+ writeFileSync(path, `${updated.join("\n").replace(/\n*$/, "")}\n`);
32
+ }
33
+ function formatValue(value) {
34
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
35
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The token exchange half of the one-click connect flow: trades the
3
+ * authorization `code` the loopback listener caught, plus the PKCE
4
+ * verifier that matches the challenge sent up front, for the OAuth client
5
+ * credentials the platform minted — see `commands.ts`'s `oauth` node and
6
+ * this repo's launch instructions for the full wire contract this speaks:
7
+ *
8
+ * POST <platform>/tools/oauth/connect/exchange
9
+ * { "code", "code_verifier" }
10
+ *
11
+ * 200 { "client_id", "client_secret", "issuer" }
12
+ * 400 { "error": "invalid_request" | "invalid_grant", "error_description" }
13
+ * 429 { "error": "rate_limited" } (+ Retry-After header)
14
+ */
15
+ export type ExchangeErrorKind = "invalid_request" | "invalid_grant" | "rate_limited" | "network" | "malformed";
16
+ export declare class ExchangeError extends Error {
17
+ readonly kind: ExchangeErrorKind;
18
+ readonly retryAfterSeconds?: number | undefined;
19
+ constructor(kind: ExchangeErrorKind, message: string, retryAfterSeconds?: number | undefined);
20
+ }
21
+ export interface ExchangeResult {
22
+ clientId: string;
23
+ clientSecret: string;
24
+ issuer: string;
25
+ }
26
+ /**
27
+ * Exchanges `code` + `codeVerifier` for OAuth client credentials.
28
+ *
29
+ * Every failure is a distinct `ExchangeError` kind so the caller (the
30
+ * wizard) can print something specific rather than a bare "request
31
+ * failed": a network failure names the URL and underlying error, a 429
32
+ * carries `Retry-After` when the platform sent one, a 400 distinguishes
33
+ * `invalid_grant` (the code was wrong, expired, or already used — the
34
+ * ordinary "try the flow again" case) from any other `invalid_request`, and
35
+ * a 200 with a body that is not valid JSON or is missing a required field
36
+ * is reported as `malformed` rather than silently returning `undefined`
37
+ * fields to the caller.
38
+ */
39
+ export declare function exchangeCode({ platformUrl, code, codeVerifier, }: {
40
+ platformUrl: string;
41
+ code: string;
42
+ codeVerifier: string;
43
+ }): Promise<ExchangeResult>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The token exchange half of the one-click connect flow: trades the
3
+ * authorization `code` the loopback listener caught, plus the PKCE
4
+ * verifier that matches the challenge sent up front, for the OAuth client
5
+ * credentials the platform minted — see `commands.ts`'s `oauth` node and
6
+ * this repo's launch instructions for the full wire contract this speaks:
7
+ *
8
+ * POST <platform>/tools/oauth/connect/exchange
9
+ * { "code", "code_verifier" }
10
+ *
11
+ * 200 { "client_id", "client_secret", "issuer" }
12
+ * 400 { "error": "invalid_request" | "invalid_grant", "error_description" }
13
+ * 429 { "error": "rate_limited" } (+ Retry-After header)
14
+ */
15
+ export class ExchangeError extends Error {
16
+ kind;
17
+ retryAfterSeconds;
18
+ constructor(kind, message, retryAfterSeconds) {
19
+ super(message);
20
+ this.kind = kind;
21
+ this.retryAfterSeconds = retryAfterSeconds;
22
+ this.name = "ExchangeError";
23
+ }
24
+ }
25
+ function exchangeUrl(platformUrl) {
26
+ return `${platformUrl.replace(/\/+$/, "")}/tools/oauth/connect/exchange`;
27
+ }
28
+ /**
29
+ * Exchanges `code` + `codeVerifier` for OAuth client credentials.
30
+ *
31
+ * Every failure is a distinct `ExchangeError` kind so the caller (the
32
+ * wizard) can print something specific rather than a bare "request
33
+ * failed": a network failure names the URL and underlying error, a 429
34
+ * carries `Retry-After` when the platform sent one, a 400 distinguishes
35
+ * `invalid_grant` (the code was wrong, expired, or already used — the
36
+ * ordinary "try the flow again" case) from any other `invalid_request`, and
37
+ * a 200 with a body that is not valid JSON or is missing a required field
38
+ * is reported as `malformed` rather than silently returning `undefined`
39
+ * fields to the caller.
40
+ */
41
+ export async function exchangeCode({ platformUrl, code, codeVerifier, }) {
42
+ const url = exchangeUrl(platformUrl);
43
+ let response;
44
+ try {
45
+ response = await fetch(url, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json" },
48
+ body: JSON.stringify({ code, code_verifier: codeVerifier }),
49
+ });
50
+ }
51
+ catch (err) {
52
+ throw new ExchangeError("network", `Could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ if (response.status === 429) {
55
+ const retryAfterHeader = response.headers.get("retry-after");
56
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : undefined;
57
+ throw new ExchangeError("rate_limited", `${url} is rate-limited` +
58
+ (Number.isFinite(retryAfterSeconds)
59
+ ? ` — retry after ${retryAfterSeconds}s.`
60
+ : "."), Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined);
61
+ }
62
+ let body;
63
+ try {
64
+ body = await response.json();
65
+ }
66
+ catch {
67
+ throw new ExchangeError("malformed", `${url} did not return valid JSON (status ${response.status}).`);
68
+ }
69
+ if (!response.ok) {
70
+ const errBody = (body ?? {});
71
+ const kind = errBody.error === "invalid_grant" ? "invalid_grant" : "invalid_request";
72
+ const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
73
+ const code = typeof errBody.error === "string" ? errBody.error : `HTTP ${response.status}`;
74
+ throw new ExchangeError(kind, description ? `${code}: ${description}` : code);
75
+ }
76
+ const result = (body ?? {});
77
+ if (typeof result.client_id !== "string" ||
78
+ typeof result.client_secret !== "string" ||
79
+ typeof result.issuer !== "string") {
80
+ throw new ExchangeError("malformed", `${url} returned 200 but was missing client_id, client_secret, or issuer.`);
81
+ }
82
+ return {
83
+ clientId: result.client_id,
84
+ clientSecret: result.client_secret,
85
+ issuer: result.issuer,
86
+ };
87
+ }
@@ -0,0 +1 @@
1
+ export declare function defaultLabel(cwd: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The connect flow's default `label` — what a contributor sees on the
3
+ * platform's approval page naming which project is asking to connect.
4
+ * `package.json`'s own `name` wins when present (it is the more deliberate
5
+ * answer — someone typed it into a manifest); the working directory's own
6
+ * name is the fallback for a project with no `package.json` at all, or one
7
+ * that failed to parse.
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { basename, join } from "node:path";
11
+ export function defaultLabel(cwd) {
12
+ const pkgPath = join(cwd, "package.json");
13
+ if (existsSync(pkgPath)) {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
16
+ if (typeof pkg.name === "string" && pkg.name.trim() !== "") {
17
+ return pkg.name.trim();
18
+ }
19
+ }
20
+ catch {
21
+ // Falls through to the directory name below.
22
+ }
23
+ }
24
+ return basename(cwd);
25
+ }
@@ -0,0 +1,52 @@
1
+ /** Default: as long as this repo's OAuth wizard reasonably keeps a browser tab open unattended. */
2
+ export declare const DEFAULT_CALLBACK_TIMEOUT_MS: number;
3
+ /** A successful `?code=...&state=...` callback. */
4
+ export interface CallbackSuccess {
5
+ code: string;
6
+ state: string;
7
+ }
8
+ /** The platform denied the request, or reported some other OAuth `error=`. */
9
+ export declare class CallbackDeniedError extends Error {
10
+ readonly errorCode: string;
11
+ readonly state: string | undefined;
12
+ constructor(errorCode: string, state: string | undefined, description?: string);
13
+ }
14
+ /** No callback arrived within the timeout — the browser was never opened, or the tab was abandoned. */
15
+ export declare class CallbackTimeoutError extends Error {
16
+ constructor(timeoutMs: number);
17
+ }
18
+ /** The callback hit `/callback` but had neither `code` nor `error` — not a shape this flow expects. */
19
+ export declare class CallbackMalformedError extends Error {
20
+ constructor();
21
+ }
22
+ /**
23
+ * The callback's `state` did not match the one this run generated (see
24
+ * `pkce.ts`'s `generateState`) — the defining CSRF defense of this whole
25
+ * flow. Any mismatch is treated the same regardless of cause (a stale tab
26
+ * from a previous run, a forged callback, or a bug): refuse and say so,
27
+ * never silently proceed with the wrong request.
28
+ */
29
+ export declare class StateMismatchError extends Error {
30
+ constructor();
31
+ }
32
+ /** Throws `StateMismatchError` unless `callback.state` matches `expected`; returns `callback` unchanged otherwise. */
33
+ export declare function verifyState(expected: string, callback: CallbackSuccess): CallbackSuccess;
34
+ export interface LoopbackListener {
35
+ /** The port `listen(0, ...)` picked. */
36
+ port: number;
37
+ /** `http://127.0.0.1:<port>/callback` — pass this as the connect URL's `redirect_uri`. */
38
+ redirectUri: string;
39
+ /** Resolves on a successful callback; rejects on denial, malformed callback, or timeout. Always settles exactly once. */
40
+ result: Promise<CallbackSuccess>;
41
+ /** Closes the listener immediately. Safe to call more than once, and safe to call after `result` has already settled. */
42
+ close: () => void;
43
+ }
44
+ /**
45
+ * Starts the listener and resolves once it is actually accepting
46
+ * connections (i.e. once the real port is known). `result` is the separate
47
+ * promise that settles once a callback (or the timeout) has been handled —
48
+ * most callers `await start(...)` once, then `await listener.result`.
49
+ */
50
+ export declare function start(opts?: {
51
+ timeoutMs?: number;
52
+ }): Promise<LoopbackListener>;