@treeseed/sdk 0.10.20 → 0.10.22

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.
@@ -1005,7 +1005,7 @@ export declare function destroyCloudflareResources(tenantRoot: any, options?: {}
1005
1005
  };
1006
1006
  operations: {
1007
1007
  worker: {
1008
- status: string;
1008
+ status: any;
1009
1009
  name: any;
1010
1010
  };
1011
1011
  formGuard: {
@@ -1013,7 +1013,7 @@ export declare function destroyCloudflareResources(tenantRoot: any, options?: {}
1013
1013
  id: any;
1014
1014
  preview?: undefined;
1015
1015
  } | {
1016
- status: string;
1016
+ status: any;
1017
1017
  id: any;
1018
1018
  preview: boolean;
1019
1019
  };
@@ -1083,6 +1083,7 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1083
1083
  }
1084
1084
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1085
1085
  const requestScript = `import { readFileSync } from 'node:fs';
1086
+ import { request } from 'node:https';
1086
1087
  const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
1087
1088
  function errorMessage(error) {
1088
1089
  const parts = [];
@@ -1099,15 +1100,32 @@ function errorMessage(error) {
1099
1100
  return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
1100
1101
  }
1101
1102
  try {
1102
- const response = await fetch(input.url, {
1103
- method: input.method,
1104
- headers: {
1105
- authorization: 'Bearer ' + input.token,
1106
- 'content-type': 'application/json',
1107
- },
1108
- body: input.body ? JSON.stringify(input.body) : undefined,
1103
+ const body = input.body ? JSON.stringify(input.body) : undefined;
1104
+ const response = await new Promise((resolve, reject) => {
1105
+ const req = request(input.url, {
1106
+ method: input.method,
1107
+ headers: {
1108
+ authorization: 'Bearer ' + input.token,
1109
+ 'content-type': 'application/json',
1110
+ },
1111
+ timeout: input.timeoutMs ?? 12000,
1112
+ }, (res) => {
1113
+ const chunks = [];
1114
+ res.setEncoding('utf8');
1115
+ res.on('data', (chunk) => chunks.push(chunk));
1116
+ res.on('end', () => resolve({
1117
+ ok: typeof res.statusCode === 'number' && res.statusCode >= 200 && res.statusCode < 300,
1118
+ text: chunks.join(''),
1119
+ }));
1120
+ });
1121
+ req.on('timeout', () => {
1122
+ req.destroy(new Error('Cloudflare API request timed out'));
1123
+ });
1124
+ req.on('error', reject);
1125
+ if (body) req.write(body);
1126
+ req.end();
1109
1127
  });
1110
- const rawBody = await response.text();
1128
+ const rawBody = response.text;
1111
1129
  let payload;
1112
1130
  try {
1113
1131
  payload = rawBody ? JSON.parse(rawBody) : {};
@@ -1126,6 +1144,7 @@ try {
1126
1144
  url: `https://api.cloudflare.com/client/v4${path}`,
1127
1145
  method,
1128
1146
  body,
1147
+ timeoutMs: 12e3,
1129
1148
  token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1130
1149
  });
1131
1150
  const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
@@ -1679,22 +1698,10 @@ function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = fal
1679
1698
  if (dryRun) {
1680
1699
  return { status: "planned", id: namespaceId, preview };
1681
1700
  }
1682
- const args = ["kv", "namespace", "delete", "--namespace-id", namespaceId, "--skip-confirmation"];
1683
- if (preview) {
1684
- args.push("--preview");
1685
- }
1686
- const result = runWrangler(args, {
1687
- cwd: tenantRoot,
1688
- allowFailure: true,
1689
- capture: true,
1690
- env
1691
- });
1692
- const output = `${result.stdout ?? ""}
1693
- ${result.stderr ?? ""}`;
1694
- if (result.status !== 0 && !looksLikeMissingResource(output)) {
1695
- throw new Error(output.trim() || `Failed to delete KV namespace ${namespaceId}.`);
1696
- }
1697
- return { status: result.status === 0 ? "deleted" : "missing", id: namespaceId, preview };
1701
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1702
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}` : null;
1703
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: namespaceId, type: "kv-namespace" });
1704
+ return { status: deleted.status, id: namespaceId, preview };
1698
1705
  }
1699
1706
  function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
1700
1707
  if (!databaseName) {
@@ -1703,18 +1710,12 @@ function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
1703
1710
  if (dryRun) {
1704
1711
  return { status: "planned", name: databaseName };
1705
1712
  }
1706
- const result = runWrangler(["d1", "delete", databaseName, "--skip-confirmation"], {
1707
- cwd: tenantRoot,
1708
- allowFailure: true,
1709
- capture: true,
1710
- env
1711
- });
1712
- const output = `${result.stdout ?? ""}
1713
- ${result.stderr ?? ""}`;
1714
- if (result.status !== 0 && !looksLikeMissingResource(output)) {
1715
- throw new Error(output.trim() || `Failed to delete D1 database ${databaseName}.`);
1716
- }
1717
- return { status: result.status === 0 ? "deleted" : "missing", name: databaseName };
1713
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1714
+ const database = accountId ? listD1Databases(tenantRoot, env).find((entry) => entry?.name === databaseName) : null;
1715
+ const databaseId = database?.uuid ?? database?.id ?? null;
1716
+ const path = accountId && databaseId ? `/accounts/${encodeURIComponent(accountId)}/d1/database/${encodeURIComponent(databaseId)}` : null;
1717
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: databaseName, type: "d1-database" });
1718
+ return { status: deleted.status, name: databaseName, id: databaseId };
1718
1719
  }
1719
1720
  function deleteWorker(tenantRoot, workerName, { env, dryRun, force = false }) {
1720
1721
  if (!workerName) {
@@ -1723,23 +1724,10 @@ function deleteWorker(tenantRoot, workerName, { env, dryRun, force = false }) {
1723
1724
  if (dryRun) {
1724
1725
  return { status: "planned", name: workerName };
1725
1726
  }
1726
- const args = ["delete", workerName];
1727
- if (force) {
1728
- args.push("--force");
1729
- }
1730
- const result = runWrangler(args, {
1731
- cwd: tenantRoot,
1732
- allowFailure: true,
1733
- capture: true,
1734
- env,
1735
- input: "y\n"
1736
- });
1737
- const output = `${result.stdout ?? ""}
1738
- ${result.stderr ?? ""}`;
1739
- if (result.status !== 0 && !looksLikeMissingResource(output)) {
1740
- throw new Error(output.trim() || `Failed to delete Worker ${workerName}.`);
1741
- }
1742
- return { status: result.status === 0 ? "deleted" : "missing", name: workerName };
1727
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1728
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/workers/services/${encodeURIComponent(workerName)}` : null;
1729
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: workerName, type: "worker" });
1730
+ return { status: deleted.status, name: workerName };
1743
1731
  }
1744
1732
  function resourceOperation(provider, type, name, status, extra = {}) {
1745
1733
  return {
@@ -1768,7 +1756,7 @@ function formatCloudflareErrors(payload) {
1768
1756
  }
1769
1757
  function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1770
1758
  const name = queueName(queue) ?? queue?.name ?? null;
1771
- const id = queueId(queue);
1759
+ let id = queueId(queue);
1772
1760
  if (!name) {
1773
1761
  return resourceOperation("cloudflare", "queue", name, "missing");
1774
1762
  }
@@ -1776,6 +1764,10 @@ function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1776
1764
  return resourceOperation("cloudflare", "queue", name, "planned", { id });
1777
1765
  }
1778
1766
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1767
+ if (!id && accountId) {
1768
+ const live = listQueues(tenantRoot, env).find((entry) => queueName(entry) === name);
1769
+ id = queueId(live);
1770
+ }
1779
1771
  const path = id ? `/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(id)}` : null;
1780
1772
  if (path) {
1781
1773
  const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name, type: "queue" });
@@ -1783,19 +1775,10 @@ function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1783
1775
  return { ...deleted, id };
1784
1776
  }
1785
1777
  }
1786
- const result = runWrangler(["queues", "delete", name], {
1787
- cwd: tenantRoot,
1788
- allowFailure: true,
1789
- capture: true,
1790
- env,
1791
- input: "y\n"
1792
- });
1793
- const output = `${result.stdout ?? ""}
1794
- ${result.stderr ?? ""}`;
1795
- if (result.status !== 0 && !looksLikeMissingResource(output)) {
1796
- throw new Error(output.trim() || `Failed to delete queue ${name}.`);
1778
+ if (accountId) {
1779
+ return resourceOperation("cloudflare", "queue", name, "missing", { id });
1797
1780
  }
1798
- return resourceOperation("cloudflare", "queue", name, result.status === 0 ? "deleted" : "missing", { id });
1781
+ throw new Error(`Failed to delete queue ${name}: CLOUDFLARE_ACCOUNT_ID is not configured.`);
1799
1782
  }
1800
1783
  function isLegacyTreeseedQueueName(name, scope) {
1801
1784
  if (!name || !scope) {
@@ -1827,19 +1810,10 @@ function deleteR2Bucket(tenantRoot, bucketName, { env, dryRun, deleteData }) {
1827
1810
  return resourceOperation("cloudflare", "r2-bucket", bucketName, "planned");
1828
1811
  }
1829
1812
  const drained = drainR2Bucket(bucketName, { env });
1830
- const result = runWrangler(["r2", "bucket", "delete", bucketName], {
1831
- cwd: tenantRoot,
1832
- allowFailure: true,
1833
- capture: true,
1834
- env,
1835
- input: "y\n"
1836
- });
1837
- const output = `${result.stdout ?? ""}
1838
- ${result.stderr ?? ""}`;
1839
- if (result.status !== 0 && !looksLikeMissingResource(output)) {
1840
- throw new Error(output.trim() || `Failed to delete R2 bucket ${bucketName}.`);
1841
- }
1842
- return resourceOperation("cloudflare", "r2-bucket", bucketName, result.status === 0 ? "deleted" : "missing", drained);
1813
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1814
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}` : null;
1815
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: bucketName, type: "r2-bucket" });
1816
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, deleted.status, drained);
1843
1817
  }
1844
1818
  function r2ObjectKey(entry) {
1845
1819
  return typeof entry?.key === "string" ? entry.key : typeof entry?.name === "string" ? entry.name : "";
@@ -2408,7 +2382,7 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2408
2382
  const force = options.force ?? false;
2409
2383
  const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
2410
2384
  const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
2411
- const queues = listQueues(tenantRoot, env);
2385
+ const queues = dryRun ? [] : listQueues(tenantRoot, env);
2412
2386
  const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2413
2387
  const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2414
2388
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
@@ -1,3 +1,4 @@
1
+ import { request as httpsRequest } from "node:https";
1
2
  const DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
2
3
  const DEFAULT_RAILWAY_WORKSPACE = "knowledge-coop";
3
4
  const RAILWAY_POSTGRES_TEMPLATE_ID = "b55da7dc-09be-4140-bc65-1284d15d349c";
@@ -295,7 +296,7 @@ async function railwayGraphqlRequest({
295
296
  const controller = new AbortController();
296
297
  let timer = null;
297
298
  try {
298
- const response = await Promise.race([
299
+ const response = fetchImpl === fetch ? await railwayGraphqlHttpsRequest(apiUrl || resolveRailwayApiUrl(env), token, { query, variables }, timeoutMs) : await Promise.race([
299
300
  fetchImpl(apiUrl || resolveRailwayApiUrl(env), {
300
301
  method: "POST",
301
302
  headers: {
@@ -304,7 +305,12 @@ async function railwayGraphqlRequest({
304
305
  },
305
306
  body: JSON.stringify({ query, variables }),
306
307
  signal: controller.signal
307
- }),
308
+ }).then(async (fetchResponse) => ({
309
+ ok: fetchResponse.ok,
310
+ status: fetchResponse.status,
311
+ payload: await fetchResponse.json().catch(() => ({})),
312
+ retryAfter: fetchResponse.headers.get("retry-after")
313
+ })),
308
314
  new Promise((_, reject) => {
309
315
  timer = setTimeout(() => {
310
316
  controller.abort();
@@ -312,11 +318,11 @@ async function railwayGraphqlRequest({
312
318
  }, timeoutMs);
313
319
  })
314
320
  ]);
315
- const payload = await response.json().catch(() => ({}));
321
+ const payload = response.payload;
316
322
  if (!response.ok || Array.isArray(payload.errors) && payload.errors.length > 0) {
317
323
  const message = normalizeRailwayErrorMessage(payload, response.status);
318
324
  const hasGraphqlErrors = Array.isArray(payload.errors) && payload.errors.length > 0;
319
- const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
325
+ const retryAfterMs = parseRetryAfterMs(response.retryAfter);
320
326
  const shouldRetry = isRetryableRailwayStatus(response.status) || /rate limit|too many requests/iu.test(message);
321
327
  const error = new Error(message);
322
328
  if (shouldRetry || hasGraphqlErrors && /rate limit|too many requests/iu.test(message)) {
@@ -340,6 +346,47 @@ async function railwayGraphqlRequest({
340
346
  }
341
347
  }
342
348
  }
349
+ async function railwayGraphqlHttpsRequest(url, token, body, timeoutMs) {
350
+ const rawBody = JSON.stringify(body);
351
+ return new Promise((resolve, reject) => {
352
+ const req = httpsRequest(url, {
353
+ method: "POST",
354
+ headers: {
355
+ authorization: `Bearer ${token}`,
356
+ "content-type": "application/json",
357
+ "content-length": Buffer.byteLength(rawBody)
358
+ },
359
+ timeout: timeoutMs
360
+ }, (res) => {
361
+ const chunks = [];
362
+ res.setEncoding("utf8");
363
+ res.on("data", (chunk) => chunks.push(chunk));
364
+ res.on("end", () => {
365
+ const text = chunks.join("");
366
+ let payload = {};
367
+ try {
368
+ payload = text ? JSON.parse(text) : {};
369
+ } catch {
370
+ payload = {};
371
+ }
372
+ const status = res.statusCode ?? 0;
373
+ const retryAfterHeader = res.headers["retry-after"];
374
+ resolve({
375
+ ok: status >= 200 && status < 300,
376
+ status,
377
+ payload,
378
+ retryAfter: Array.isArray(retryAfterHeader) ? retryAfterHeader[0] ?? null : retryAfterHeader ?? null
379
+ });
380
+ });
381
+ });
382
+ req.on("timeout", () => {
383
+ req.destroy(markRailwayTransientError(new Error(`Railway API request timed out after ${timeoutMs}ms.`)));
384
+ });
385
+ req.on("error", reject);
386
+ req.write(rawBody);
387
+ req.end();
388
+ });
389
+ }
343
390
  async function getRailwayAuthProfile({
344
391
  env = process.env,
345
392
  fetchImpl = fetch
@@ -839,13 +886,13 @@ async function ensureRailwayServiceInstanceConfiguration({
839
886
  runtimeMode,
840
887
  env = process.env,
841
888
  fetchImpl = fetch,
842
- settleAttempts = 24,
889
+ settleAttempts = 60,
843
890
  settleDelayMs = 5e3
844
891
  }) {
845
892
  let current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
846
893
  if (!current.id) {
847
- for (let attempt = 0; attempt < 8 && !current.id; attempt += 1) {
848
- await new Promise((resolve) => setTimeout(resolve, 1500));
894
+ for (let attempt = 0; attempt < settleAttempts && !current.id; attempt += 1) {
895
+ await new Promise((resolve) => setTimeout(resolve, settleDelayMs));
849
896
  current = await getRailwayServiceInstance({ serviceId, environmentId, env, fetchImpl });
850
897
  }
851
898
  }
@@ -2133,7 +2133,8 @@ async function verifyRailwayUnit(input) {
2133
2133
  const topology = await resolveRailwayTopologyForScope(input, scope, {
2134
2134
  serviceKeys: [serviceKey],
2135
2135
  includeInstances: true,
2136
- includeVariables: true
2136
+ includeVariables: true,
2137
+ refresh: true
2137
2138
  });
2138
2139
  const entry = topology.services.get(serviceKey) ?? null;
2139
2140
  const service = entry?.configuredService ?? null;
@@ -408,7 +408,7 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
408
408
  };
409
409
  ciMode: "hosted" | "off";
410
410
  verifyMode: "action-first" | "local-only" | import("../workflow.ts").TreeseedWorkflowVerifyMode;
411
- workflowGates: Record<string, unknown>[] | {
411
+ workflowGates: (Record<string, unknown> | {
412
412
  name: string;
413
413
  repository: string | null;
414
414
  workflow: string;
@@ -423,7 +423,7 @@ export declare function workflowSave(helpers: WorkflowOperationHelpers, input: T
423
423
  updatedAt: null;
424
424
  timeoutSeconds: number | null;
425
425
  cached: boolean;
426
- }[];
426
+ })[];
427
427
  releaseCandidate: ReleaseCandidateReport | null;
428
428
  hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
429
429
  } & {
@@ -551,7 +551,7 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
551
551
  };
552
552
  ciMode: "hosted" | "off";
553
553
  verifyMode: "action-first" | "local-only" | import("../workflow.ts").TreeseedWorkflowVerifyMode;
554
- workflowGates: Record<string, unknown>[] | {
554
+ workflowGates: (Record<string, unknown> | {
555
555
  name: string;
556
556
  repository: string | null;
557
557
  workflow: string;
@@ -566,7 +566,7 @@ export declare function workflowClose(helpers: WorkflowOperationHelpers, input:
566
566
  updatedAt: null;
567
567
  timeoutSeconds: number | null;
568
568
  cached: boolean;
569
- }[];
569
+ })[];
570
570
  releaseCandidate: ReleaseCandidateReport | null;
571
571
  hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
572
572
  } & {
@@ -735,7 +735,7 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
735
735
  };
736
736
  ciMode: "hosted" | "off";
737
737
  verifyMode: "action-first" | "local-only" | import("../workflow.ts").TreeseedWorkflowVerifyMode;
738
- workflowGates: Record<string, unknown>[] | {
738
+ workflowGates: (Record<string, unknown> | {
739
739
  name: string;
740
740
  repository: string | null;
741
741
  workflow: string;
@@ -750,7 +750,7 @@ export declare function workflowStage(helpers: WorkflowOperationHelpers, input:
750
750
  updatedAt: null;
751
751
  timeoutSeconds: number | null;
752
752
  cached: boolean;
753
- }[];
753
+ })[];
754
754
  releaseCandidate: ReleaseCandidateReport | null;
755
755
  hostingAudit: import("../workflow-support.ts").TreeseedHostingAuditReport | null;
756
756
  } & {
@@ -3134,7 +3134,7 @@ async function workflowSave(helpers, input) {
3134
3134
  const saveWorkflowGates = shouldUseHostedSaveCi(effectiveInput, branch) ? await executeJournalStep(root, workflowRun.runId, "hosted-ci", async () => {
3135
3135
  if (branch === STAGING_BRANCH) {
3136
3136
  const workflowGates = saveResult?.workflowGates ?? [];
3137
- if (workflowGates.length > 0 || effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
3137
+ if (effectiveInput.verifyDeployedResources !== true || scope === "local" || !savedRootRepo.commitSha) {
3138
3138
  return { workflowGates };
3139
3139
  }
3140
3140
  helpers.write("[save][workflow] Dispatching hosted market deploy gate for deployed resource verification.");
@@ -3163,7 +3163,12 @@ async function workflowSave(helpers, input) {
3163
3163
  runId: workflowRun.runId,
3164
3164
  onProgress: (line, stream) => helpers.write(line, stream)
3165
3165
  });
3166
- return { workflowGates: dispatchedGates };
3166
+ return {
3167
+ workflowGates: [
3168
+ ...workflowGates.filter((gate) => !(gate.repository === repository && gate.workflow === "deploy.yml")),
3169
+ ...dispatchedGates
3170
+ ]
3171
+ };
3167
3172
  }
3168
3173
  helpers.write("[save][workflow] Waiting for hosted save workflow gates.");
3169
3174
  return waitForWorkflowGates("save", [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.10.20",
3
+ "version": "0.10.22",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {