@treeseed/sdk 0.8.6 → 0.8.7

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.
@@ -256,7 +256,7 @@ export declare function validateTreeseedCommandEnvironment({ tenantRoot, scope,
256
256
  }): {
257
257
  registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
258
258
  values: {};
259
- validation: import("../../platform/environment.ts").TreeseedEnvironmentValidationResult;
259
+ validation: any;
260
260
  };
261
261
  export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }: {
262
262
  tenantRoot: any;
@@ -265,7 +265,7 @@ export declare function assertTreeseedCommandEnvironment({ tenantRoot, scope, pu
265
265
  }): {
266
266
  registry: import("../../platform/environment.ts").TreeseedResolvedEnvironmentRegistry;
267
267
  values: {};
268
- validation: import("../../platform/environment.ts").TreeseedEnvironmentValidationResult;
268
+ validation: any;
269
269
  };
270
270
  export declare function ensureTreeseedActVerificationTooling({ tenantRoot, installIfMissing, env, write }?: {
271
271
  tenantRoot?: string | undefined;
@@ -34,7 +34,8 @@ import {
34
34
  } from "./railway-deploy.js";
35
35
  import {
36
36
  normalizeRailwayEnvironmentName,
37
- resolveRailwayWorkspace
37
+ resolveRailwayWorkspace,
38
+ resolveRailwayWorkspaceContext
38
39
  } from "./railway-api.js";
39
40
  import {
40
41
  createGitHubApiClient,
@@ -1585,20 +1586,52 @@ function applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override = false
1585
1586
  function validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
1586
1587
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1587
1588
  const values = resolveTreeseedLaunchEnvironment({ tenantRoot, scope });
1588
- const validation = validateTreeseedEnvironmentValues({
1589
+ const validation = filterValidationByWorkflowPlane(validateTreeseedEnvironmentValues({
1589
1590
  values,
1590
1591
  scope,
1591
1592
  purpose,
1592
1593
  deployConfig: registry.context.deployConfig,
1593
1594
  tenantConfig: registry.context.tenantConfig,
1594
1595
  plugins: registry.context.plugins
1595
- });
1596
+ }));
1596
1597
  return {
1597
1598
  registry,
1598
1599
  values,
1599
1600
  validation
1600
1601
  };
1601
1602
  }
1603
+ function filterValidationByWorkflowPlane(validation) {
1604
+ const plane = process.env.TREESEED_WORKFLOW_PLANE;
1605
+ if (plane !== "web" && plane !== "processing") {
1606
+ return validation;
1607
+ }
1608
+ const problemApplies = (problem) => doesEntryApplyToWorkflowPlane(problem.entry, plane);
1609
+ const missing = validation.missing.filter(problemApplies);
1610
+ const invalid = validation.invalid.filter(problemApplies);
1611
+ const entries = validation.entries.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
1612
+ const required = validation.required.filter((entry) => doesEntryApplyToWorkflowPlane(entry, plane));
1613
+ return {
1614
+ ...validation,
1615
+ ok: missing.length === 0 && invalid.length === 0,
1616
+ entries,
1617
+ required,
1618
+ missing,
1619
+ invalid
1620
+ };
1621
+ }
1622
+ function doesEntryApplyToWorkflowPlane(entry, plane) {
1623
+ const targets = new Set(entry.targets ?? []);
1624
+ const hasProcessingTarget = targets.has("railway-secret") || targets.has("railway-var");
1625
+ const hasWebTarget = targets.has("cloudflare-secret") || targets.has("cloudflare-var") || targets.has("local-cloudflare");
1626
+ const hasWorkflowTarget = targets.has("github-secret") || targets.has("github-variable");
1627
+ if (plane === "web") {
1628
+ return !hasProcessingTarget || hasWebTarget || hasWorkflowTarget;
1629
+ }
1630
+ if (plane === "processing") {
1631
+ return !hasWebTarget || hasProcessingTarget || hasWorkflowTarget;
1632
+ }
1633
+ return true;
1634
+ }
1602
1635
  function assertTreeseedCommandEnvironment({ tenantRoot, scope, purpose }) {
1603
1636
  const report = validateTreeseedCommandEnvironment({ tenantRoot, scope, purpose });
1604
1637
  if (report.validation.ok) {
@@ -1894,23 +1927,26 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1894
1927
  const checkPromise = (async () => {
1895
1928
  for (let attempt = 0; attempt < 3; attempt += 1) {
1896
1929
  try {
1897
- const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1898
- const whoami = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "whoami"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1899
- if (!whoami.ok) {
1900
- if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
1901
- return providerConnectionResult(
1902
- "railway",
1903
- false,
1904
- "Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
1905
- { skipped: true, warning: true, rateLimited: true }
1906
- );
1907
- }
1908
- throw new Error(whoami.detail || "Railway CLI authentication check failed.");
1909
- }
1910
- const identity = whoami.stdout.replace(/^logged in as\s+/iu, "").replace(/\s*👋\s*$/u, "").trim() || "an account";
1911
- return providerConnectionResult("railway", true, `Railway authenticated as ${identity} in workspace ${workspaceName}. Project and service existence will be reconciled during bootstrap.`);
1930
+ const workspace = await resolveRailwayWorkspaceContext({ env, workspace: workspaceName });
1931
+ return providerConnectionResult("railway", true, `Railway API token can access workspace ${workspace.name}. Project and service existence will be reconciled during bootstrap.`);
1912
1932
  } catch (error) {
1913
1933
  const detail = error instanceof Error ? error.message : "Railway API check failed.";
1934
+ if (/rate.?limit|too many requests|429/iu.test(detail || "")) {
1935
+ return providerConnectionResult(
1936
+ "railway",
1937
+ false,
1938
+ "Railway connectivity preflight was rate-limited; bootstrap will continue and rely on API-backed reconcile verification.",
1939
+ { skipped: true, warning: true, rateLimited: true }
1940
+ );
1941
+ }
1942
+ if (attempt >= 2 && isTransientProviderConnectionError(detail)) {
1943
+ return providerConnectionResult(
1944
+ "railway",
1945
+ false,
1946
+ "Railway connectivity preflight hit transient API failures; bootstrap will continue and rely on API-backed reconcile verification.",
1947
+ { skipped: true, warning: true, transient: true }
1948
+ );
1949
+ }
1914
1950
  if (attempt >= 2 || !isTransientProviderConnectionError(detail)) {
1915
1951
  return providerConnectionResult("railway", false, detail);
1916
1952
  }
@@ -1044,22 +1044,44 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1044
1044
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1045
1045
  const requestScript = `import { readFileSync } from 'node:fs';
1046
1046
  const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
1047
- const response = await fetch(input.url, {
1048
- method: input.method,
1049
- headers: {
1050
- authorization: 'Bearer ' + input.token,
1051
- 'content-type': 'application/json',
1052
- },
1053
- body: input.body ? JSON.stringify(input.body) : undefined,
1054
- });
1055
- const rawBody = await response.text();
1056
- let payload;
1057
- try {
1058
- payload = rawBody ? JSON.parse(rawBody) : {};
1059
- } catch {
1060
- payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
1047
+ function errorMessage(error) {
1048
+ const parts = [];
1049
+ if (error && typeof error.message === 'string') parts.push(error.message);
1050
+ const cause = error?.cause;
1051
+ if (cause && typeof cause.message === 'string') parts.push(cause.message);
1052
+ if (cause && typeof cause.code === 'string') parts.push(cause.code);
1053
+ if (Array.isArray(cause?.errors)) {
1054
+ for (const entry of cause.errors) {
1055
+ if (entry && typeof entry.message === 'string') parts.push(entry.message);
1056
+ if (entry && typeof entry.code === 'string') parts.push(entry.code);
1057
+ }
1058
+ }
1059
+ return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
1061
1060
  }
1062
- process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1061
+ try {
1062
+ const response = await fetch(input.url, {
1063
+ method: input.method,
1064
+ headers: {
1065
+ authorization: 'Bearer ' + input.token,
1066
+ 'content-type': 'application/json',
1067
+ },
1068
+ body: input.body ? JSON.stringify(input.body) : undefined,
1069
+ });
1070
+ const rawBody = await response.text();
1071
+ let payload;
1072
+ try {
1073
+ payload = rawBody ? JSON.parse(rawBody) : {};
1074
+ } catch {
1075
+ payload = { success: false, errors: [{ message: rawBody || 'empty response' }] };
1076
+ }
1077
+ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));
1078
+ } catch (error) {
1079
+ process.stdout.write(JSON.stringify({
1080
+ ok: false,
1081
+ transient: true,
1082
+ payload: { success: false, errors: [{ message: errorMessage(error) }] },
1083
+ }));
1084
+ }`;
1063
1085
  const requestInput = JSON.stringify({
1064
1086
  url: `https://api.cloudflare.com/client/v4${path}`,
1065
1087
  method,
@@ -1067,6 +1089,11 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1067
1089
  token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1068
1090
  });
1069
1091
  const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
1092
+ const formatPayloadErrors = (payload) => Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "";
1093
+ const summarizeChildError = (text) => {
1094
+ const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
1095
+ return lines.find((line) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|typeerror|error/iu.test(line)) ?? lines[0] ?? "";
1096
+ };
1070
1097
  let attempt = 0;
1071
1098
  for (; ; ) {
1072
1099
  const response = spawnSync(
@@ -1085,29 +1112,48 @@ process.stdout.write(JSON.stringify({ ok: response.ok, payload }));`;
1085
1112
  }
1086
1113
  );
1087
1114
  if (response.error?.code === "ETIMEDOUT") {
1088
- if (attempt < 2) {
1115
+ if (attempt < 4) {
1089
1116
  attempt += 1;
1117
+ sleepSync(500 * attempt);
1090
1118
  continue;
1091
1119
  }
1092
1120
  if (!allowFailure) {
1093
- throw new Error(`Cloudflare API request timed out: ${method} ${path}`);
1121
+ throw new Error(`Cloudflare API request timed out after ${attempt + 1} attempts: ${method} ${path}`);
1094
1122
  }
1095
1123
  return null;
1096
1124
  }
1097
1125
  const stderr = response.stderr?.trim() || "";
1098
1126
  if (response.status !== 0) {
1099
- if (attempt < 2 && isTransient(stderr)) {
1127
+ if (attempt < 4 && isTransient(stderr)) {
1100
1128
  attempt += 1;
1129
+ sleepSync(500 * attempt);
1101
1130
  continue;
1102
1131
  }
1103
1132
  if (!allowFailure) {
1104
- throw new Error(stderr || `Cloudflare API request failed: ${method} ${path}`);
1133
+ const detail = summarizeChildError(stderr);
1134
+ throw new Error(detail ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${detail}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
1105
1135
  }
1106
1136
  }
1107
- const parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
1137
+ let parsed;
1138
+ try {
1139
+ parsed = JSON.parse(response.stdout?.trim() || '{"ok":false,"payload":{"success":false,"errors":[{"message":"empty response"}]}}');
1140
+ } catch {
1141
+ parsed = {
1142
+ ok: false,
1143
+ payload: {
1144
+ success: false,
1145
+ errors: [{ message: response.stdout?.trim() || stderr || "empty response" }]
1146
+ }
1147
+ };
1148
+ }
1149
+ const details = formatPayloadErrors(parsed.payload);
1150
+ if (!parsed.ok && parsed.transient && attempt < 4 && isTransient(details)) {
1151
+ attempt += 1;
1152
+ sleepSync(500 * attempt);
1153
+ continue;
1154
+ }
1108
1155
  if (!parsed.ok && !allowFailure) {
1109
- const details = Array.isArray(parsed.payload?.errors) ? parsed.payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "unknown error";
1110
- throw new Error(details || `Cloudflare API request failed: ${method} ${path}`);
1156
+ throw new Error(details ? `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}: ${details}` : `Cloudflare API request failed after ${attempt + 1} attempts: ${method} ${path}`);
1111
1157
  }
1112
1158
  return parsed.payload;
1113
1159
  }
@@ -506,7 +506,7 @@ function parseFallbackDeployConfig(configPath) {
506
506
  slug: expectString(record.slug, "slug"),
507
507
  siteUrl: expectString(record.siteUrl, "siteUrl"),
508
508
  contactEmail: expectString(record.contactEmail, "contactEmail"),
509
- hosting: parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
509
+ hosting: parsedHosting?.kind === "market_control_plane" ? { ...parsedHosting, registration: "none" } : parsedHosting && record.hub === void 0 && record.runtime === void 0 ? parsedHosting : normalizeLegacyHostingFromPlanes(hub, runtime),
510
510
  hub,
511
511
  runtime,
512
512
  cloudflare: {
@@ -541,7 +541,7 @@ function parseDeployConfig(raw) {
541
541
  const turnstile = optionalRecord(parsed.turnstile, "turnstile") ?? {};
542
542
  optionalBoolean(turnstile.enabled, "turnstile.enabled");
543
543
  const normalizedHosting = normalizeLegacyHostingFromPlanes(hub, runtime);
544
- const compatibilityHosting = hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
544
+ const compatibilityHosting = hosting?.kind === "market_control_plane" ? { ...hosting, registration: "none" } : hosting && !parsed.hub && !parsed.runtime ? hosting : normalizedHosting;
545
545
  return {
546
546
  name: expectString(parsed.name, "name"),
547
547
  slug: expectString(parsed.slug, "slug"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {