create-op-node 0.10.11 → 0.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,18 +12,38 @@ import { readFileSync } from 'fs';
12
12
  import * as tls from 'tls';
13
13
  import { Validator } from '@cfworker/json-schema';
14
14
 
15
+ // src/lib/constants.ts
16
+ var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
17
+ var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
18
+ var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
19
+ var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
20
+ var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
21
+ var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
22
+ var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
23
+ var API_REQUEST_TIMEOUT_MS = 15e3;
24
+ var BODY_PREVIEW_MAX = 200;
25
+
15
26
  // src/lib/cloudflare.ts
16
27
  var CF_API = "https://api.cloudflare.com/client/v4";
17
28
  async function get(token, path, fetchImpl = fetch) {
18
- const res = await fetchImpl(`${CF_API}${path}`, {
19
- headers: { Authorization: `Bearer ${token}` }
20
- });
21
- let body = null;
29
+ const ctrl = new AbortController();
30
+ const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
22
31
  try {
23
- body = await res.json();
32
+ const res = await fetchImpl(`${CF_API}${path}`, {
33
+ headers: { Authorization: `Bearer ${token}` },
34
+ signal: ctrl.signal
35
+ });
36
+ let body = null;
37
+ try {
38
+ body = await res.json();
39
+ } catch {
40
+ }
41
+ return { status: res.status, body };
24
42
  } catch {
43
+ return { status: 0, body: null };
44
+ } finally {
45
+ clearTimeout(timer);
25
46
  }
26
- return { status: res.status, body };
27
47
  }
28
48
  function shortError(body) {
29
49
  if (typeof body === "object" && body !== null && "errors" in body && Array.isArray(body.errors)) {
@@ -38,6 +58,12 @@ function shortError(body) {
38
58
  async function probeCloudflareToken(input) {
39
59
  const issues = [];
40
60
  const verify = await get(input.token, `/accounts/${input.accountId}/tokens/verify`);
61
+ if (verify.status === 0) {
62
+ issues.push(
63
+ "Couldn't reach Cloudflare (network error or timeout). Check your connection and retry."
64
+ );
65
+ return { ok: false, issues };
66
+ }
41
67
  if (verify.status !== 200) {
42
68
  issues.push(
43
69
  `Token verify failed (HTTP ${verify.status}): ${shortError(verify.body)}`
@@ -55,7 +81,9 @@ async function probeCloudflareToken(input) {
55
81
  const res = await get(input.token, check.path);
56
82
  if (res.status === 200) continue;
57
83
  if (res.status === 404 && check.treat404AsPass) continue;
58
- if (res.status === 401 || res.status === 403) {
84
+ if (res.status === 0) {
85
+ issues.push(`Couldn't reach Cloudflare while checking ${check.name} (network error or timeout).`);
86
+ } else if (res.status === 401 || res.status === 403) {
59
87
  issues.push(`Missing scope: ${check.name}`);
60
88
  } else if (res.status === 400 && typeof res.body === "object" && res.body !== null && "errors" in res.body && Array.isArray(res.body.errors) && res.body.errors.some((e) => e.code === 10042)) {
61
89
  issues.push(
@@ -75,6 +103,9 @@ async function tunnelStatus(input) {
75
103
  `/accounts/${input.accountId}/cfd_tunnel/${input.tunnelId}`,
76
104
  fetchImpl
77
105
  );
106
+ if (res.status === 0) {
107
+ return { ok: false, reason: "couldn't reach Cloudflare (network error or timeout)" };
108
+ }
78
109
  if (res.status !== 200) {
79
110
  return {
80
111
  ok: false,
@@ -103,13 +134,24 @@ function authHeaders(token) {
103
134
  };
104
135
  }
105
136
  async function getJson(token, path) {
106
- const res = await fetch(`${TFC_API}${path}`, { headers: authHeaders(token) });
107
- let body = null;
137
+ const ctrl = new AbortController();
138
+ const timer = setTimeout(() => ctrl.abort(), API_REQUEST_TIMEOUT_MS);
108
139
  try {
109
- body = await res.json();
140
+ const res = await fetch(`${TFC_API}${path}`, {
141
+ headers: authHeaders(token),
142
+ signal: ctrl.signal
143
+ });
144
+ let body = null;
145
+ try {
146
+ body = await res.json();
147
+ } catch {
148
+ }
149
+ return { status: res.status, body };
110
150
  } catch {
151
+ return { status: 0, body: null };
152
+ } finally {
153
+ clearTimeout(timer);
111
154
  }
112
- return { status: res.status, body };
113
155
  }
114
156
  async function probeTfcToken(input) {
115
157
  const issues = [];
@@ -120,6 +162,12 @@ async function probeTfcToken(input) {
120
162
  return { ok: false, issues };
121
163
  }
122
164
  const account = await getJson(input.token, "/account/details");
165
+ if (account.status === 0) {
166
+ issues.push(
167
+ "Couldn't reach Terraform Cloud (network error or timeout). Check your connection and retry."
168
+ );
169
+ return { ok: false, issues };
170
+ }
123
171
  if (account.status !== 200) {
124
172
  issues.push(
125
173
  `TFC token invalid (HTTP ${account.status}). Regenerate at https://app.terraform.io/app/settings/tokens.`
@@ -131,7 +179,11 @@ async function probeTfcToken(input) {
131
179
  input.token,
132
180
  `/organizations/${encodeURIComponent(input.organization)}`
133
181
  );
134
- if (org.status === 404) {
182
+ if (org.status === 0) {
183
+ issues.push(
184
+ "Couldn't reach Terraform Cloud while checking the organization (network error or timeout)."
185
+ );
186
+ } else if (org.status === 404) {
135
187
  issues.push(
136
188
  `TFC organization "${input.organization}" not found, or this token lacks access. Check the org slug at https://app.terraform.io.`
137
189
  );
@@ -582,16 +634,27 @@ async function waitForApply(input, budgets = DEFAULT_BUDGETS, deps = realDeps) {
582
634
  }
583
635
  return waitForRunOutput(input, budgets, deps, runId);
584
636
  }
637
+ async function safePoll(fn, onRetry) {
638
+ try {
639
+ return await fn();
640
+ } catch {
641
+ onRetry?.();
642
+ return null;
643
+ }
644
+ }
585
645
  async function discoverRunId(input, budgets, deps) {
586
646
  deps.onProgress?.("discovery");
587
647
  const discoveryStart = deps.now();
588
648
  while (deps.now() - discoveryStart < budgets.discoveryMs) {
589
649
  await deps.sleep(budgets.pollMs);
590
- const ws = await deps.findWorkspace({
591
- token: input.token,
592
- organization: input.organization,
593
- tags: input.workspaceTags
594
- });
650
+ const ws = await safePoll(
651
+ () => deps.findWorkspace({
652
+ token: input.token,
653
+ organization: input.organization,
654
+ tags: input.workspaceTags
655
+ }),
656
+ () => deps.onProgress?.("retry")
657
+ );
595
658
  if (ws?.currentRunId) return ws.currentRunId;
596
659
  }
597
660
  return null;
@@ -600,17 +663,20 @@ async function waitForRunOutput(input, budgets, deps, runId) {
600
663
  deps.onProgress?.("run");
601
664
  const runStart = deps.now();
602
665
  while (deps.now() - runStart < budgets.runMs) {
603
- const r = await deps.getRunStatus(
604
- { token: input.token, organization: input.organization },
605
- runId
666
+ const r = await safePoll(
667
+ () => deps.getRunStatus({ token: input.token, organization: input.organization }, runId),
668
+ () => deps.onProgress?.("retry")
606
669
  );
607
670
  if (r?.finished) {
608
671
  if (!r.succeeded) return { kind: "run-failed", status: r.status };
609
672
  deps.onProgress?.("fetching");
610
- const value = await deps.fetchOutput(
611
- { token: input.token, organization: input.organization },
612
- input.workspaceId,
613
- input.outputName
673
+ const value = await safePoll(
674
+ () => deps.fetchOutput(
675
+ { token: input.token, organization: input.organization },
676
+ input.workspaceId,
677
+ input.outputName
678
+ ),
679
+ () => deps.onProgress?.("retry")
614
680
  );
615
681
  return value !== null ? { kind: "success", value } : { kind: "output-missing" };
616
682
  }
@@ -1165,14 +1231,25 @@ function isoStampUtc(d) {
1165
1231
  async function waitForApplyAndFetchTunnelToken(input) {
1166
1232
  const spin = p3.spinner();
1167
1233
  spin.start("Waiting for terraform apply to finish (polling every 10s)\u2026");
1168
- const outcome = await waitForApply({
1169
- token: input.token,
1170
- organization: input.organization,
1171
- workspaceId: input.workspaceId,
1172
- runId: input.runId,
1173
- workspaceTags: ["opuspopuli", "cloudflare"],
1174
- outputName: "tunnel_token"
1175
- });
1234
+ const outcome = await waitForApply(
1235
+ {
1236
+ token: input.token,
1237
+ organization: input.organization,
1238
+ workspaceId: input.workspaceId,
1239
+ runId: input.runId,
1240
+ workspaceTags: ["opuspopuli", "cloudflare"],
1241
+ outputName: "tunnel_token"
1242
+ },
1243
+ void 0,
1244
+ {
1245
+ ...realDeps,
1246
+ // Surface a transient network hiccup instead of silently re-polling, so
1247
+ // a slow wait doesn't look like a hang.
1248
+ onProgress: (phase) => {
1249
+ if (phase === "retry") spin.message("Transient network error \u2014 retrying next poll\u2026");
1250
+ }
1251
+ }
1252
+ );
1176
1253
  switch (outcome.kind) {
1177
1254
  case "success":
1178
1255
  spin.stop(pc2.green("\u2713 Tunnel token retrieved."));
@@ -1192,16 +1269,6 @@ async function waitForApplyAndFetchTunnelToken(input) {
1192
1269
  }
1193
1270
  }
1194
1271
 
1195
- // src/lib/constants.ts
1196
- var PGSODIUM_KEY_RE = /^[a-f0-9]{64}$/;
1197
- var TUNNEL_TOKEN_RE = /^[A-Za-z0-9_\-.=]+$/;
1198
- var SAFE_PATH_RE = /^[A-Za-z0-9_\-./ ]+$/;
1199
- var SAFE_LAUNCHCTL_VALUE_RE = /^[A-Za-z0-9+/=._-]+$/;
1200
- var URL_SAFE_PASSWORD_RE = /^[A-Za-z0-9_-]+$/;
1201
- var SAFE_URL_RE = /^[A-Za-z0-9:/_.-]+$/;
1202
- var VERIFY_NETWORK_TIMEOUT_MS = 1e4;
1203
- var BODY_PREVIEW_MAX = 200;
1204
-
1205
1272
  // src/lib/homebrew.ts
1206
1273
  var STUDIO_PACKAGES = [
1207
1274
  { name: "git", kind: "formula" },
@@ -4784,7 +4851,7 @@ async function looksLikeRegionsRepo(dir) {
4784
4851
  }
4785
4852
 
4786
4853
  // src/cli.ts
4787
- var VERSION = "0.10.11";
4854
+ var VERSION = "0.10.13";
4788
4855
  var program = new Command();
4789
4856
  program.name("create-op-node").description(
4790
4857
  "Interactive bootstrap for an Opus Populi federation node.\nCloudflare infrastructure \u2192 Mac Studio \u2192 live public API."