@treeseed/sdk 0.10.21 → 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.10.21",
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": {