@treeseed/sdk 0.12.37 → 0.12.38

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.
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { dirname, resolve } from "node:path";
3
4
  import { resolveTreeseedLaunchEnvironment } from "../operations/services/config-runtime.js";
4
5
  import { cloudflareApiRequest, resolveCloudflareZoneIdForHost, resolveConfiguredCloudflareAccountId, runWrangler } from "../operations/services/deploy.js";
@@ -161,6 +162,62 @@ function cloudflarePagesDomain(input) {
161
162
  function cloudflarePagesDeploymentUrl(projectName, branchName, environment) {
162
163
  return environment === "prod" ? `https://${projectName}.pages.dev` : `https://${branchName}.${projectName}.pages.dev`;
163
164
  }
165
+ function probeCloudflarePagesPublicUrl(url) {
166
+ if (!url) {
167
+ return { ok: false, status: null, finalUrl: null, headers: {}, error: "missing_url" };
168
+ }
169
+ const script = `
170
+ const url = process.argv[1];
171
+ try {
172
+ const response = await fetch(url, {
173
+ redirect: 'follow',
174
+ headers: {
175
+ 'user-agent': 'treeseed-hosting-verifier/1.0',
176
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
177
+ },
178
+ signal: AbortSignal.timeout(15000),
179
+ });
180
+ const headers = {};
181
+ for (const key of ['cf-cache-status', 'age', 'server', 'cache-control', 'content-type']) {
182
+ const value = response.headers.get(key);
183
+ if (value) headers[key] = value;
184
+ }
185
+ process.stdout.write(JSON.stringify({
186
+ ok: response.ok,
187
+ status: response.status,
188
+ finalUrl: response.url,
189
+ headers,
190
+ error: null,
191
+ }));
192
+ } catch (error) {
193
+ process.stdout.write(JSON.stringify({
194
+ ok: false,
195
+ status: null,
196
+ finalUrl: url,
197
+ headers: {},
198
+ error: error instanceof Error ? error.message : String(error),
199
+ }));
200
+ }
201
+ `;
202
+ const result = spawnSync(process.execPath, ["--input-type=module", "-e", script, url], {
203
+ encoding: "utf8",
204
+ timeout: 2e4
205
+ });
206
+ if (result.error) {
207
+ return { ok: false, status: null, finalUrl: url, headers: {}, error: result.error.message };
208
+ }
209
+ try {
210
+ return JSON.parse(result.stdout || "{}");
211
+ } catch {
212
+ return {
213
+ ok: false,
214
+ status: null,
215
+ finalUrl: url,
216
+ headers: {},
217
+ error: result.stderr?.trim() || result.stdout?.trim() || "invalid_probe_output"
218
+ };
219
+ }
220
+ }
164
221
  function cloudflarePagesDnsTarget(projectName, branchName, environment) {
165
222
  return environment === "prod" ? `${projectName}.pages.dev` : `${branchName}.${projectName}.pages.dev`;
166
223
  }
@@ -337,6 +394,8 @@ function createCloudflareHostAdapter() {
337
394
  const observedDomain = observedState.observedDomain ?? null;
338
395
  const observedDnsRecord = observedState.observedDnsRecord ?? null;
339
396
  const observedDeployment = observedState.observedDeployment ?? null;
397
+ const publicUrl = domain ? `https://${domain}` : projectName && branchName ? cloudflarePagesDeploymentUrl(projectName, branchName, input.environment) : null;
398
+ const publicProbe = input.environment === "prod" ? probeCloudflarePagesPublicUrl(publicUrl) : null;
340
399
  const checks = [
341
400
  {
342
401
  key: "pages-project.exists",
@@ -376,6 +435,16 @@ function createCloudflareHostAdapter() {
376
435
  issues: !domain || observedDnsRecord ? [] : [`Cloudflare DNS record ${domain} -> ${projectName && branchName ? cloudflarePagesDnsTarget(projectName, branchName, input.environment) : "(unset)"} is missing.`]
377
436
  }
378
437
  ];
438
+ if (input.environment === "prod") {
439
+ checks.push({
440
+ key: "pages-public-url.ok",
441
+ label: "Cloudflare Pages public URL responds successfully",
442
+ ok: publicProbe?.ok === true,
443
+ expected: { url: publicUrl, ok: true },
444
+ observed: publicProbe,
445
+ issues: publicProbe?.ok === true ? [] : [`Cloudflare Pages public URL ${publicUrl ?? "(unset)"} did not return a successful status.`]
446
+ });
447
+ }
379
448
  return {
380
449
  unitId: input.unit.id,
381
450
  status: checks.every((check) => check.ok) ? "ready" : "pending",
@@ -300,11 +300,17 @@ export declare function purgeSourcePageCaches(tenantRoot: any, options?: {}): {
300
300
  results: never[];
301
301
  } | {
302
302
  urls: (string | null)[];
303
- results: {
303
+ results: ({
304
304
  zoneId: any;
305
305
  count: number;
306
306
  success: boolean;
307
- }[];
307
+ errors: any;
308
+ } | {
309
+ zoneId: unknown;
310
+ count: string;
311
+ success: boolean;
312
+ errors: any;
313
+ })[];
308
314
  skipped?: undefined;
309
315
  reason?: undefined;
310
316
  };
@@ -319,6 +325,7 @@ export declare function purgePublishedContentCaches(tenantRoot: any, urls: any,
319
325
  zoneId: any;
320
326
  count: number;
321
327
  success: boolean;
328
+ errors: any;
322
329
  }[];
323
330
  skipped?: undefined;
324
331
  reason?: undefined;
@@ -1249,8 +1249,8 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1249
1249
  if ((deployConfig.surfaces?.web?.provider ?? deployConfig.providers?.deploy) !== "cloudflare") {
1250
1250
  return false;
1251
1251
  }
1252
- const webTarget = resolvePublicWebCacheTarget(deployConfig);
1253
- return Boolean(webTarget?.host && !webTarget.host.endsWith(".workers.dev") && !webTarget.host.endsWith(".pages.dev"));
1252
+ const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1253
+ return Boolean(webTarget2?.host && !webTarget2.host.endsWith(".workers.dev") && !webTarget2.host.endsWith(".pages.dev"));
1254
1254
  }
1255
1255
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1256
1256
  const token = env?.TREESEED_CLOUDFLARE_API_TOKEN ?? env?.CLOUDFLARE_API_TOKEN ?? process.env.TREESEED_CLOUDFLARE_API_TOKEN ?? "";
@@ -1476,7 +1476,7 @@ function buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, kin
1476
1476
  ];
1477
1477
  if (sourcePathExpression) {
1478
1478
  rules.push({
1479
- description: "treeseed-managed: cache source html routes",
1479
+ description: "treeseed-managed: bypass source html routes",
1480
1480
  expression: joinCloudflareAndExpression([
1481
1481
  hostExpression,
1482
1482
  pathExpression,
@@ -1485,15 +1485,7 @@ function buildTreeseedManagedCloudflareCacheRules(deployConfig, cacheTarget, kin
1485
1485
  ]),
1486
1486
  action: "set_cache_settings",
1487
1487
  action_parameters: {
1488
- cache: true,
1489
- edge_ttl: {
1490
- mode: "override_origin",
1491
- default: policy.sourcePages.edgeTtlSeconds
1492
- },
1493
- browser_ttl: {
1494
- mode: "override_origin",
1495
- default: policy.sourcePages.browserTtlSeconds
1496
- }
1488
+ cache: false
1497
1489
  },
1498
1490
  enabled: true
1499
1491
  });
@@ -1576,10 +1568,10 @@ function reconcileCloudflareCacheRulesForTarget(role, deployConfig, state, cache
1576
1568
  }
1577
1569
  function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, target, { planOnly = false, env: providedEnv } = {}) {
1578
1570
  if (!shouldManageCloudflareWebCacheRules(deployConfig, target)) {
1579
- const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1580
- const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
1581
- state.webCache.webHost = webTarget2?.host ?? null;
1582
- state.webCache.contentHost = contentTarget2?.host ?? null;
1571
+ const webTarget3 = resolvePublicWebCacheTarget(deployConfig);
1572
+ const contentTarget3 = resolvePublicContentCacheTarget(deployConfig);
1573
+ state.webCache.webHost = webTarget3?.host ?? null;
1574
+ state.webCache.contentHost = contentTarget3?.host ?? null;
1583
1575
  state.webCache.rulesManaged = false;
1584
1576
  state.webCache.lastError = null;
1585
1577
  return { managed: false, skipped: true, reason: "unsupported_target_or_host" };
@@ -1594,15 +1586,15 @@ function reconcileCloudflareWebCacheRules(tenantRoot, deployConfig, state, targe
1594
1586
  state.webCache.lastError = "CLOUDFLARE_API_TOKEN is required to manage Cloudflare Cache Rules.";
1595
1587
  return { managed: false, skipped: true, reason: "missing_api_token" };
1596
1588
  }
1597
- const webTarget = resolvePublicWebCacheTarget(deployConfig);
1598
- const contentTarget = resolvePublicContentCacheTarget(deployConfig);
1589
+ const webTarget2 = resolvePublicWebCacheTarget(deployConfig);
1590
+ const contentTarget2 = resolvePublicContentCacheTarget(deployConfig);
1599
1591
  try {
1600
1592
  const results = [];
1601
- if (webTarget?.host) {
1602
- results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget, env, { planOnly }));
1593
+ if (webTarget2?.host) {
1594
+ results.push(reconcileCloudflareCacheRulesForTarget("web", deployConfig, state, webTarget2, env, { planOnly }));
1603
1595
  }
1604
- if (contentTarget?.host) {
1605
- results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget, env, { planOnly }));
1596
+ if (contentTarget2?.host) {
1597
+ results.push(reconcileCloudflareCacheRulesForTarget("content", deployConfig, state, contentTarget2, env, { planOnly }));
1606
1598
  }
1607
1599
  state.webCache.rulesManaged = true;
1608
1600
  state.webCache.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1643,13 +1635,41 @@ function purgeCloudflareCacheByUrls(urls, deployConfig, { env } = {}) {
1643
1635
  body: { files: [...new Set(files)] },
1644
1636
  env
1645
1637
  });
1638
+ const errors = Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean) : [];
1646
1639
  return {
1647
1640
  zoneId,
1648
1641
  count: [...new Set(files)].length,
1649
- success: payload?.success === true
1642
+ success: payload?.success === true,
1643
+ errors
1650
1644
  };
1651
1645
  });
1652
1646
  }
1647
+ function purgeCloudflareCacheEverythingByHosts(hosts, deployConfig, { env } = {}) {
1648
+ const zoneIds = [...new Set((hosts ?? []).map((host) => typeof host === "string" ? host.trim() : "").filter(Boolean).map((host) => resolveCloudflareZoneIdForHost(deployConfig, host, env)).filter(Boolean))];
1649
+ return zoneIds.map((zoneId) => {
1650
+ const payload = cloudflareApiRequest(`/zones/${zoneId}/purge_cache`, {
1651
+ method: "POST",
1652
+ body: { purge_everything: true },
1653
+ env
1654
+ });
1655
+ const errors = Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean) : [];
1656
+ return {
1657
+ zoneId,
1658
+ count: "everything",
1659
+ success: payload?.success === true,
1660
+ errors
1661
+ };
1662
+ });
1663
+ }
1664
+ function assertCloudflareCachePurgeSucceeded(results) {
1665
+ const failures = (results ?? []).filter((result) => result?.success !== true);
1666
+ if (failures.length === 0) return;
1667
+ const detail = failures.map((result) => {
1668
+ const errors = Array.isArray(result?.errors) && result.errors.length > 0 ? `: ${result.errors.join("; ")}` : "";
1669
+ return `${result?.zoneId ?? "unknown-zone"} (${result?.count ?? 0} urls)${errors}`;
1670
+ }).join(", ");
1671
+ throw new Error(`Cloudflare cache purge did not succeed for ${detail}.`);
1672
+ }
1653
1673
  function queueName(entry) {
1654
1674
  return entry?.queue_name ?? entry?.queueName ?? entry?.name ?? null;
1655
1675
  }
@@ -1708,9 +1728,11 @@ function purgeSourcePageCaches(tenantRoot, options = {}) {
1708
1728
  const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
1709
1729
  env
1710
1730
  });
1711
- recordCachePurgeResult(state.webCache.deployPurge, results);
1731
+ const allResults = target.scope === "prod" ? purgeCloudflareCacheEverythingByHosts([webTarget.host, contentTarget?.host], deployConfig, { env }) : [];
1732
+ assertCloudflareCachePurgeSucceeded([...results, ...allResults]);
1733
+ recordCachePurgeResult(state.webCache.deployPurge, [...results, ...allResults]);
1712
1734
  writeDeployState(tenantRoot, state, { target });
1713
- return { urls, results };
1735
+ return { urls, results: [...results, ...allResults] };
1714
1736
  } catch (error) {
1715
1737
  recordCachePurgeResult(state.webCache.deployPurge, [], error);
1716
1738
  writeDeployState(tenantRoot, state, { target });
@@ -1736,6 +1758,7 @@ function purgePublishedContentCaches(tenantRoot, urls, options = {}) {
1736
1758
  const results = purgeCloudflareCacheByUrls(urls, deployConfig, {
1737
1759
  env
1738
1760
  });
1761
+ assertCloudflareCachePurgeSucceeded(results);
1739
1762
  recordCachePurgeResult(state.webCache.contentPurge, results);
1740
1763
  writeDeployState(tenantRoot, state, { target });
1741
1764
  return { urls, results };
@@ -79,12 +79,14 @@ function variableObserved(observed, key) {
79
79
  function valuePresence(values, observed, key, serviceTarget) {
80
80
  const providerPresent = variableObserved(observed, key);
81
81
  const machinePresent = hasConfiguredValue(values, key);
82
+ const providerObserved = Boolean(observed);
83
+ const present = providerObserved ? providerPresent : machinePresent;
82
84
  return {
83
85
  key,
84
86
  serviceTarget,
85
- present: providerPresent || machinePresent,
86
- source: providerPresent ? "provider" : machinePresent ? "machine-config" : "missing",
87
- observation: observed ? "provider-live" : "not-observed"
87
+ present,
88
+ source: providerPresent ? "provider" : !providerObserved && machinePresent ? "machine-config" : "missing",
89
+ observation: providerObserved ? "provider-live" : "not-observed"
88
90
  };
89
91
  }
90
92
  function serviceTypeFor(key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.12.37",
3
+ "version": "0.12.38",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {