@userreason/wizard 0.1.0 → 0.2.1

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.
@@ -0,0 +1,153 @@
1
+ import { createRequire as createWizardRequire } from 'node:module'; import { fileURLToPath as wizardFileURLToPath } from 'node:url'; import { dirname as wizardDirname } from 'node:path'; const require = createWizardRequire(import.meta.url); const __filename = wizardFileURLToPath(import.meta.url); const __dirname = wizardDirname(__filename);
2
+ import {
3
+ CredentialStoreError,
4
+ object,
5
+ string,
6
+ uuid
7
+ } from "./chunk-GGARVSPT.js";
8
+
9
+ // src/connection.ts
10
+ import { createHash, randomBytes } from "node:crypto";
11
+ var BrowserConnectionUnavailable = class extends Error {
12
+ };
13
+ function retryableNetworkError(error) {
14
+ if (!(error instanceof Error)) return false;
15
+ const cause = error.cause;
16
+ const rawCode = error.code ?? cause?.code;
17
+ const code = typeof rawCode === "string" ? rawCode : void 0;
18
+ if (code && !["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ECONNREFUSED", "ENETUNREACH"].includes(code)) return false;
19
+ return error.name === "TimeoutError" || error.name === "AbortError" || error instanceof TypeError && error.message === "fetch failed";
20
+ }
21
+ function memoryCredentials(session = null) {
22
+ let value = session;
23
+ return {
24
+ load: async () => value,
25
+ save: async (next) => {
26
+ value = next;
27
+ },
28
+ clear: async () => {
29
+ value = null;
30
+ }
31
+ };
32
+ }
33
+ async function compatibilityCredentials(existing, sessionOnly, confirm) {
34
+ if (sessionOnly) return { credentials: memoryCredentials(), session: null, sessionOnly: true };
35
+ try {
36
+ return { credentials: existing, session: await existing.load(), sessionOnly: false };
37
+ } catch (error) {
38
+ if (!(error instanceof CredentialStoreError) || error.code === "invalid_session" || !await confirm(error)) throw error;
39
+ return { credentials: memoryCredentials(), session: null, sessionOnly: true };
40
+ }
41
+ }
42
+ async function connectInstallation(config, input, onBrowser, options = {}) {
43
+ const fetcher = options.fetcher ?? fetch, now = options.now ?? Date.now;
44
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
45
+ const discovery = await fetcher(`${config.apiUrl}/onboarding/v1/auth-config?installation=1`, {
46
+ redirect: "error",
47
+ signal: AbortSignal.timeout(1e4)
48
+ });
49
+ if (!discovery.ok) throw new Error(`Unable to discover installation login (${discovery.status})`);
50
+ const capabilities = object(await discovery.json(), "installation capabilities");
51
+ if (capabilities.client_id !== config.clientId)
52
+ throw new Error("Installation authentication client does not match the configured API");
53
+ options.onCapabilities?.({ unrestrictedOrigins: capabilities.unrestricted_origins === 1 });
54
+ if (capabilities.installation_login !== 1)
55
+ throw new BrowserConnectionUnavailable("This API uses device authentication");
56
+ const verifier = randomBytes(32).toString("hex"), challenge = createHash("sha256").update(verifier).digest("hex");
57
+ async function post(path, body, timeout = 2e4) {
58
+ return fetcher(`${config.apiUrl}/onboarding/v1/installations/${path}`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(body),
62
+ redirect: "error",
63
+ signal: AbortSignal.timeout(timeout)
64
+ });
65
+ }
66
+ const started = await post("start", {
67
+ challenge,
68
+ project_name: input.projectName,
69
+ workspace_id: input.workspaceId ?? null,
70
+ workspace_name: input.workspaceName ?? null,
71
+ site_id: input.siteId ?? null
72
+ });
73
+ if (started.status === 404)
74
+ throw new BrowserConnectionUnavailable("This API does not support browser installation yet");
75
+ if (!started.ok)
76
+ throw new Error(`Unable to start installation (${started.status}). Check the API deployment.`);
77
+ const data = object(await started.json(), "installation request"), id = uuid(string(data.id, "installation ID"));
78
+ const target = new URL(string(data.browser_url, "browser URL"));
79
+ const ticket = string(data.ticket, "installation ticket");
80
+ if (target.protocol !== "https:" && !(target.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(target.hostname)) || target.username || target.password || target.pathname !== "/install" || target.searchParams.get("id") !== id || new URLSearchParams(target.hash.slice(1)).get("request") !== ticket)
81
+ throw new Error("Invalid installation browser URL");
82
+ const expires = Date.parse(string(data.expires_at, "installation expiry"));
83
+ if (!Number.isFinite(expires) || expires <= now() || expires > now() + 901e3 || data.code !== challenge.slice(0, 8).toUpperCase())
84
+ throw new Error("Invalid installation request");
85
+ await onBrowser(target.href, String(data.code));
86
+ const waitingSince = now();
87
+ const deadline = Math.min(expires, waitingSince + 6e5);
88
+ let failures = 0, lastFailure;
89
+ const progress = (phase, httpStatus) => options.onProgress?.({
90
+ phase,
91
+ elapsedSeconds: Math.max(0, Math.floor((now() - waitingSince) / 1e3)),
92
+ consecutiveFailures: failures,
93
+ ...httpStatus === void 0 ? {} : { httpStatus }
94
+ });
95
+ progress("waiting");
96
+ while (now() < deadline) {
97
+ await sleep(Math.min(failures ? 5e3 : 3e3, deadline - now()));
98
+ if (now() >= deadline) break;
99
+ let response;
100
+ try {
101
+ response = await post("exchange", { id, ticket, verifier }, Math.max(1, Math.min(2e4, deadline - now())));
102
+ } catch (error) {
103
+ if (!retryableNetworkError(error)) throw error;
104
+ failures++;
105
+ lastFailure = "network connection";
106
+ progress("retrying");
107
+ continue;
108
+ }
109
+ if (response.status === 429 || response.status >= 500) {
110
+ failures++;
111
+ lastFailure = `HTTP ${response.status}`;
112
+ progress("retrying", response.status);
113
+ continue;
114
+ }
115
+ if (!response.ok)
116
+ throw new Error(
117
+ `Installation connection expired or failed (${response.status}). Run the wizard again.`
118
+ );
119
+ const result = object(await response.json(), "installation connection");
120
+ failures = 0;
121
+ lastFailure = void 0;
122
+ if (result.status === "pending") {
123
+ progress("waiting");
124
+ continue;
125
+ }
126
+ if (result.status !== "approved") throw new Error("Installation was declined.");
127
+ const user = object(result.user, "installation user"), expiry = Date.parse(string(result.expires_at, "access expiry"));
128
+ if (!Number.isFinite(expiry) || expiry > expires || expiry < now() + 6e4)
129
+ throw new Error("Installation access expired. Run the wizard again.");
130
+ return {
131
+ session: {
132
+ ...config,
133
+ accessToken: string(result.access_token, "installation token"),
134
+ refreshToken: "",
135
+ expiresAt: expiry,
136
+ userId: uuid(string(user.id, "user ID")),
137
+ email: string(user.email, "email")
138
+ },
139
+ workspaceId: result.workspace_id === null ? null : uuid(string(result.workspace_id, "workspace ID")),
140
+ workspaceName: result.workspace_name === null ? null : string(result.workspace_name, "workspace name"),
141
+ grantId: uuid(string(result.grant_id, "grant ID"))
142
+ };
143
+ }
144
+ if (failures) throw new Error(`Unable to confirm browser approval: the API is still failing (${lastFailure}). Run the wizard again when the connection is available.`);
145
+ throw new Error("Browser approval timed out. Finish sign-in and approval in the browser, then run the wizard again.");
146
+ }
147
+
148
+ export {
149
+ BrowserConnectionUnavailable,
150
+ memoryCredentials,
151
+ compatibilityCredentials,
152
+ connectInstallation
153
+ };