@treeseed/sdk 0.10.21 → 0.10.23

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.
Files changed (34) hide show
  1. package/dist/db/market-schema.js +3 -2
  2. package/dist/market-client.d.ts +4 -0
  3. package/dist/market-client.js +6 -0
  4. package/dist/operations/providers/default.js +26 -4
  5. package/dist/operations/repository-operations.js +6 -2
  6. package/dist/operations/services/config-runtime.d.ts +1 -1
  7. package/dist/operations/services/deploy.d.ts +20 -3
  8. package/dist/operations/services/deploy.js +228 -102
  9. package/dist/operations/services/github-automation.d.ts +10 -1
  10. package/dist/operations/services/github-automation.js +18 -4
  11. package/dist/operations/services/hosting-audit.d.ts +2 -1
  12. package/dist/operations/services/hosting-audit.js +12 -1
  13. package/dist/operations/services/hub-launch.d.ts +1 -0
  14. package/dist/operations/services/hub-launch.js +1 -0
  15. package/dist/operations/services/hub-provider-launch.d.ts +9 -0
  16. package/dist/operations/services/hub-provider-launch.js +140 -40
  17. package/dist/operations/services/managed-host-security.d.ts +1 -1
  18. package/dist/operations/services/managed-host-security.js +4 -1
  19. package/dist/operations/services/project-platform.js +16 -0
  20. package/dist/operations/services/railway-api.js +56 -8
  21. package/dist/operations/services/railway-deploy.d.ts +2 -1
  22. package/dist/operations/services/railway-deploy.js +15 -18
  23. package/dist/platform/environment.d.ts +1 -1
  24. package/dist/platform/environment.js +1 -1
  25. package/dist/reconcile/builtin-adapters.js +155 -25
  26. package/dist/reconcile/contracts.d.ts +1 -1
  27. package/dist/reconcile/desired-state.js +17 -1
  28. package/dist/reconcile/units.js +1 -0
  29. package/dist/sdk-types.d.ts +1 -1
  30. package/dist/sdk-types.js +2 -0
  31. package/dist/workflow/operations.d.ts +2 -0
  32. package/drizzle/market/0000_market_control_plane.sql +3 -3
  33. package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
  34. package/package.json +1 -1
@@ -41,6 +41,13 @@ function ensureParent(filePath) {
41
41
  function stableHash(value) {
42
42
  return createHash("sha256").update(value).digest("hex");
43
43
  }
44
+ function compactDeploymentKey(input) {
45
+ const rawKey = sanitizeResourceKey(input.rawKey ?? "");
46
+ if (rawKey && rawKey.length <= 40) return rawKey;
47
+ const base = sanitizeSegment(input.slug ?? input.projectSegment ?? "project").slice(0, 27) || "project";
48
+ const hash = stableHash(`${input.teamId ?? ""}:${input.projectId ?? ""}:${input.slug ?? ""}`).slice(0, 8);
49
+ return `${base}-${hash}`;
50
+ }
44
51
  function readJson(filePath, fallback) {
45
52
  if (!existsSync(filePath)) {
46
53
  return fallback;
@@ -69,6 +76,9 @@ function loadTenantDeployConfig(tenantRoot) {
69
76
  function sanitizeSegment(value) {
70
77
  return String(value).trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 36) || "default";
71
78
  }
79
+ function sanitizeResourceKey(value) {
80
+ return String(value).trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "");
81
+ }
72
82
  function requireConfiguredIdentityValue(value, label) {
73
83
  const normalized = typeof value === "string" && value.trim() ? value.trim() : "";
74
84
  if (!normalized) {
@@ -87,7 +97,13 @@ function resolveTreeseedResourceIdentity(deployConfig, target) {
87
97
  );
88
98
  const teamSegment = sanitizeSegment(teamId);
89
99
  const projectSegment = sanitizeSegment(projectId);
90
- const deploymentKey = `${teamSegment}-${projectSegment}`;
100
+ const deploymentKey = compactDeploymentKey({
101
+ rawKey: `${teamSegment}-${projectSegment}`,
102
+ teamId,
103
+ projectId,
104
+ projectSegment,
105
+ slug: deployConfig.slug
106
+ });
91
107
  const environment = target.kind === "persistent" ? target.scope : target.branchName;
92
108
  const environmentSegment = target.kind === "persistent" ? target.scope : sanitizeSegment(target.branchName);
93
109
  return {
@@ -168,6 +184,13 @@ function resolveConfiguredSurfaceBaseUrl(deployConfig, target, surface) {
168
184
  }
169
185
  return null;
170
186
  }
187
+ function configuredSurfaceHosts(deployConfig, target, surface) {
188
+ const hosts = [
189
+ resolveConfiguredSurfaceDomain(deployConfig, target, surface),
190
+ surface === "web" && target.kind === "persistent" && target.scope === "prod" ? primaryHost(deployConfig.surfaces?.web?.publicBaseUrl ?? deployConfig.siteUrl) : null
191
+ ].filter(Boolean);
192
+ return [...new Set(hosts)];
193
+ }
171
194
  function sharedDeploymentName(identity, role = "") {
172
195
  const roleSegment = role === "workdayManager" ? "workday-manager" : role === "workerRunner" ? "worker-runner-01" : role;
173
196
  return role ? `${identity.deploymentKey}-${sanitizeSegment(roleSegment)}` : identity.deploymentKey;
@@ -419,7 +442,7 @@ function buildSecretMap(deployConfig, state) {
419
442
  return {
420
443
  TREESEED_FORM_TOKEN_SECRET: envOrNull("TREESEED_FORM_TOKEN_SECRET") ?? generatedSecret,
421
444
  TREESEED_EDITORIAL_PREVIEW_SECRET: envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET") ?? previewSecret,
422
- TREESEED_TURNSTILE_SECRET_KEY: envOrNull("TREESEED_TURNSTILE_SECRET_KEY"),
445
+ TREESEED_TURNSTILE_SECRET_KEY: state.turnstileWidgets?.formGuard?.secret ?? envOrNull("TREESEED_TURNSTILE_SECRET_KEY"),
423
446
  TREESEED_SMTP_PASSWORD: envOrNull("TREESEED_SMTP_PASSWORD")
424
447
  };
425
448
  }
@@ -431,6 +454,8 @@ function defaultStateFromConfig(deployConfig, target) {
431
454
  const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
432
455
  const contentDefaultTeamId = identity.teamId;
433
456
  const contentManifestKey = contentManifestKeyTemplate.replaceAll("{teamId}", contentDefaultTeamId);
457
+ const turnstileName = environmentScopedIdentityName(identity, "turnstile", target);
458
+ const turnstileDomains = configuredSurfaceHosts(deployConfig, target, "web");
434
459
  return {
435
460
  version: 2,
436
461
  target,
@@ -469,6 +494,17 @@ function defaultStateFromConfig(deployConfig, target) {
469
494
  buildOutputDir: deployConfig.cloudflare.pages?.buildOutputDir ?? "dist",
470
495
  url: resolveConfiguredSurfaceBaseUrl(deployConfig, target, "web")
471
496
  },
497
+ turnstileWidgets: {
498
+ formGuard: {
499
+ name: turnstileName,
500
+ sitekey: null,
501
+ secret: null,
502
+ mode: "managed",
503
+ domains: turnstileDomains,
504
+ managed: true,
505
+ lastSyncedAt: null
506
+ }
507
+ },
472
508
  content: {
473
509
  runtimeProvider: deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
474
510
  publishProvider: deployConfig.providers?.content?.publish ?? deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
@@ -636,6 +672,23 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
636
672
  ...defaults.generatedSecrets ?? {},
637
673
  ...persisted.generatedSecrets ?? {}
638
674
  },
675
+ turnstileWidgets: {
676
+ ...defaults.turnstileWidgets ?? {},
677
+ ...persisted.turnstileWidgets ?? {},
678
+ formGuard: {
679
+ ...defaults.turnstileWidgets?.formGuard ?? {},
680
+ ...persisted.turnstileWidgets?.formGuard ?? {},
681
+ name: defaults.turnstileWidgets?.formGuard?.name ?? persisted.turnstileWidgets?.formGuard?.name ?? null,
682
+ mode: "managed",
683
+ managed: true,
684
+ domains: [
685
+ ...new Set([
686
+ ...Array.isArray(defaults.turnstileWidgets?.formGuard?.domains) ? defaults.turnstileWidgets.formGuard.domains : [],
687
+ ...Array.isArray(persisted.turnstileWidgets?.formGuard?.domains) ? persisted.turnstileWidgets.formGuard.domains : []
688
+ ].filter(Boolean))
689
+ ]
690
+ }
691
+ },
639
692
  content: {
640
693
  ...defaults.content ?? {},
641
694
  ...persisted.content ?? {},
@@ -926,6 +979,68 @@ function listPagesProjects(tenantRoot, env) {
926
979
  });
927
980
  return Array.isArray(payload?.result) ? payload.result : [];
928
981
  }
982
+ function listTurnstileWidgets(tenantRoot, env) {
983
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
984
+ if (!accountId) {
985
+ return [];
986
+ }
987
+ const payload = cloudflareApiRequest(`/accounts/${encodeURIComponent(accountId)}/challenges/widgets?per_page=100`, {
988
+ env,
989
+ allowFailure: true
990
+ });
991
+ return Array.isArray(payload?.result) ? payload.result : [];
992
+ }
993
+ function getTurnstileWidget(env, sitekey) {
994
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
995
+ if (!accountId || !sitekey) {
996
+ return null;
997
+ }
998
+ const payload = cloudflareApiRequest(
999
+ `/accounts/${encodeURIComponent(accountId)}/challenges/widgets/${encodeURIComponent(sitekey)}`,
1000
+ { env, allowFailure: true }
1001
+ );
1002
+ return payload?.result ?? null;
1003
+ }
1004
+ function createTurnstileWidget(env, input) {
1005
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1006
+ if (!accountId) {
1007
+ throw new Error("Configure CLOUDFLARE_ACCOUNT_ID before creating Turnstile widgets.");
1008
+ }
1009
+ try {
1010
+ return cloudflareApiRequest(`/accounts/${encodeURIComponent(accountId)}/challenges/widgets`, {
1011
+ method: "POST",
1012
+ env,
1013
+ body: {
1014
+ name: input.name,
1015
+ domains: input.domains ?? [],
1016
+ mode: input.mode ?? "managed"
1017
+ }
1018
+ })?.result ?? null;
1019
+ } catch (error) {
1020
+ const detail = error instanceof Error ? error.message : String(error);
1021
+ throw new Error(`Cloudflare Turnstile widget creation failed. Ensure the API token has Turnstile Sites Write permission: ${detail}`);
1022
+ }
1023
+ }
1024
+ function updateTurnstileWidget(env, sitekey, input) {
1025
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1026
+ if (!accountId || !sitekey) {
1027
+ throw new Error("Configure CLOUDFLARE_ACCOUNT_ID and sitekey before updating Turnstile widgets.");
1028
+ }
1029
+ try {
1030
+ return cloudflareApiRequest(`/accounts/${encodeURIComponent(accountId)}/challenges/widgets/${encodeURIComponent(sitekey)}`, {
1031
+ method: "PUT",
1032
+ env,
1033
+ body: {
1034
+ name: input.name,
1035
+ domains: input.domains ?? [],
1036
+ mode: input.mode ?? "managed"
1037
+ }
1038
+ })?.result ?? null;
1039
+ } catch (error) {
1040
+ const detail = error instanceof Error ? error.message : String(error);
1041
+ throw new Error(`Cloudflare Turnstile widget update failed. Ensure the API token has Turnstile Sites Write permission: ${detail}`);
1042
+ }
1043
+ }
929
1044
  function buildCloudflarePagesFunctionBindings(state) {
930
1045
  const kvNamespaces = Object.fromEntries(
931
1046
  Object.entries(state.kvNamespaces ?? {}).map(([key, namespace]) => {
@@ -1006,6 +1121,7 @@ function buildProvisioningSummary(deployConfig, state, target) {
1006
1121
  siteUrl: target.kind === "branch" ? targetWorkersDevUrl(state.workerName) : deployConfig.siteUrl,
1007
1122
  accountId: resolveConfiguredCloudflareAccountId(deployConfig),
1008
1123
  pages: state.pages ?? null,
1124
+ turnstileWidget: state.turnstileWidgets?.formGuard ?? null,
1009
1125
  formGuardKv: state.kvNamespaces.FORM_GUARD_KV,
1010
1126
  sessionKv: state.kvNamespaces.SESSION ?? null,
1011
1127
  siteDataDb: state.d1Databases.SITE_DATA_DB,
@@ -1017,6 +1133,7 @@ function buildProvisioningSummary(deployConfig, state, target) {
1017
1133
  queue: state.queues?.agentWork?.name ?? null,
1018
1134
  dlq: state.queues?.agentWork?.dlqName ?? null,
1019
1135
  database: state.d1Databases?.SITE_DATA_DB?.databaseName ?? null,
1136
+ turnstileWidget: state.turnstileWidgets?.formGuard?.name ?? null,
1020
1137
  formGuardKv: state.kvNamespaces?.FORM_GUARD_KV?.name ?? null,
1021
1138
  railwayProject: state.services?.worker?.projectName ?? state.services?.api?.projectName ?? null,
1022
1139
  webDomain: configuredWebDomain,
@@ -1083,6 +1200,7 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1083
1200
  }
1084
1201
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1085
1202
  const requestScript = `import { readFileSync } from 'node:fs';
1203
+ import { request } from 'node:https';
1086
1204
  const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
1087
1205
  function errorMessage(error) {
1088
1206
  const parts = [];
@@ -1099,15 +1217,32 @@ function errorMessage(error) {
1099
1217
  return [...new Set(parts.filter(Boolean))].join('; ') || String(error);
1100
1218
  }
1101
1219
  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,
1220
+ const body = input.body ? JSON.stringify(input.body) : undefined;
1221
+ const response = await new Promise((resolve, reject) => {
1222
+ const req = request(input.url, {
1223
+ method: input.method,
1224
+ headers: {
1225
+ authorization: 'Bearer ' + input.token,
1226
+ 'content-type': 'application/json',
1227
+ },
1228
+ timeout: input.timeoutMs ?? 12000,
1229
+ }, (res) => {
1230
+ const chunks = [];
1231
+ res.setEncoding('utf8');
1232
+ res.on('data', (chunk) => chunks.push(chunk));
1233
+ res.on('end', () => resolve({
1234
+ ok: typeof res.statusCode === 'number' && res.statusCode >= 200 && res.statusCode < 300,
1235
+ text: chunks.join(''),
1236
+ }));
1237
+ });
1238
+ req.on('timeout', () => {
1239
+ req.destroy(new Error('Cloudflare API request timed out'));
1240
+ });
1241
+ req.on('error', reject);
1242
+ if (body) req.write(body);
1243
+ req.end();
1109
1244
  });
1110
- const rawBody = await response.text();
1245
+ const rawBody = response.text;
1111
1246
  let payload;
1112
1247
  try {
1113
1248
  payload = rawBody ? JSON.parse(rawBody) : {};
@@ -1126,6 +1261,7 @@ try {
1126
1261
  url: `https://api.cloudflare.com/client/v4${path}`,
1127
1262
  method,
1128
1263
  body,
1264
+ timeoutMs: 12e3,
1129
1265
  token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1130
1266
  });
1131
1267
  const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
@@ -1514,14 +1650,7 @@ function resolveConfiguredContentPublicBaseUrl(deployConfig) {
1514
1650
  return envOrNull("TREESEED_CONTENT_PUBLIC_BASE_URL") ?? deployConfig.cloudflare.r2?.publicBaseUrl ?? "";
1515
1651
  }
1516
1652
  function missingTurnstileRequirements() {
1517
- const issues = [];
1518
- if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
1519
- issues.push("Set TREESEED_PUBLIC_TURNSTILE_SITE_KEY before deploying.");
1520
- }
1521
- if (!envOrNull("TREESEED_TURNSTILE_SECRET_KEY")) {
1522
- issues.push("Set TREESEED_TURNSTILE_SECRET_KEY before deploying.");
1523
- }
1524
- return issues;
1653
+ return [];
1525
1654
  }
1526
1655
  function missingContentRuntimeRequirements(deployConfig) {
1527
1656
  const issues = [];
@@ -1545,20 +1674,6 @@ function collectMissingDeployInputs(tenantRoot) {
1545
1674
  message: "Cloudflare account ID is missing. Set CLOUDFLARE_ACCOUNT_ID with treeseed config or provide it now."
1546
1675
  });
1547
1676
  }
1548
- if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
1549
- missing.push({
1550
- key: "TREESEED_PUBLIC_TURNSTILE_SITE_KEY",
1551
- label: "Turnstile public site key",
1552
- message: "Turnstile public site key is missing for deploy."
1553
- });
1554
- }
1555
- if (!envOrNull("TREESEED_TURNSTILE_SECRET_KEY")) {
1556
- missing.push({
1557
- key: "TREESEED_TURNSTILE_SECRET_KEY",
1558
- label: "Turnstile secret key",
1559
- message: "Turnstile secret key is missing for deploy."
1560
- });
1561
- }
1562
1677
  if (deployConfig.providers?.content?.runtime === "team_scoped_r2_overlay" && !envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET")) {
1563
1678
  missing.push({
1564
1679
  key: "TREESEED_EDITORIAL_PREVIEW_SECRET",
@@ -1655,6 +1770,24 @@ function resolveExistingKvIdByName(kvNamespaces, expectedName, fallbackId) {
1655
1770
  }
1656
1771
  return kvNamespaces.find((entry) => entry?.title === expectedName)?.id ?? null;
1657
1772
  }
1773
+ function resolveExistingTurnstileWidget(widgets, current) {
1774
+ if (!current?.name && !current?.sitekey) {
1775
+ return current;
1776
+ }
1777
+ const existing = widgets.find(
1778
+ (entry) => current.sitekey && entry?.sitekey === current.sitekey || current.name && entry?.name === current.name
1779
+ );
1780
+ if (!existing?.sitekey) {
1781
+ return current;
1782
+ }
1783
+ return {
1784
+ ...current,
1785
+ sitekey: existing.sitekey,
1786
+ secret: existing.secret ?? current.secret ?? null,
1787
+ domains: Array.isArray(existing.domains) ? existing.domains : current.domains ?? [],
1788
+ mode: existing.mode ?? current.mode ?? "managed"
1789
+ };
1790
+ }
1658
1791
  function resolveExistingD1ByName(d1Databases, expectedName, current) {
1659
1792
  if (current?.databaseId && !isPlaceholderResourceId(current.databaseId)) {
1660
1793
  return current;
@@ -1679,22 +1812,28 @@ function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = fal
1679
1812
  if (dryRun) {
1680
1813
  return { status: "planned", id: namespaceId, preview };
1681
1814
  }
1682
- const args = ["kv", "namespace", "delete", "--namespace-id", namespaceId, "--skip-confirmation"];
1683
- if (preview) {
1684
- args.push("--preview");
1815
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1816
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/storage/kv/namespaces/${encodeURIComponent(namespaceId)}` : null;
1817
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: namespaceId, type: "kv-namespace" });
1818
+ return { status: deleted.status, id: namespaceId, preview };
1819
+ }
1820
+ function deleteTurnstileWidget(sitekey, { env, dryRun, name = null }) {
1821
+ if (!sitekey || isPlaceholderResourceId(sitekey)) {
1822
+ return { status: "missing", sitekey, name };
1685
1823
  }
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}.`);
1824
+ if (dryRun) {
1825
+ return { status: "planned", sitekey, name };
1826
+ }
1827
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1828
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/challenges/widgets/${encodeURIComponent(sitekey)}` : null;
1829
+ let deleted;
1830
+ try {
1831
+ deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: name ?? sitekey, type: "turnstile-widget" });
1832
+ } catch (error) {
1833
+ const detail = error instanceof Error ? error.message : String(error);
1834
+ throw new Error(`Cloudflare Turnstile widget deletion failed. Ensure the API token has Turnstile Sites Write permission: ${detail}`);
1696
1835
  }
1697
- return { status: result.status === 0 ? "deleted" : "missing", id: namespaceId, preview };
1836
+ return { status: deleted.status, sitekey, name };
1698
1837
  }
1699
1838
  function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
1700
1839
  if (!databaseName) {
@@ -1703,18 +1842,12 @@ function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
1703
1842
  if (dryRun) {
1704
1843
  return { status: "planned", name: databaseName };
1705
1844
  }
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 };
1845
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1846
+ const database = accountId ? listD1Databases(tenantRoot, env).find((entry) => entry?.name === databaseName) : null;
1847
+ const databaseId = database?.uuid ?? database?.id ?? null;
1848
+ const path = accountId && databaseId ? `/accounts/${encodeURIComponent(accountId)}/d1/database/${encodeURIComponent(databaseId)}` : null;
1849
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: databaseName, type: "d1-database" });
1850
+ return { status: deleted.status, name: databaseName, id: databaseId };
1718
1851
  }
1719
1852
  function deleteWorker(tenantRoot, workerName, { env, dryRun, force = false }) {
1720
1853
  if (!workerName) {
@@ -1723,23 +1856,10 @@ function deleteWorker(tenantRoot, workerName, { env, dryRun, force = false }) {
1723
1856
  if (dryRun) {
1724
1857
  return { status: "planned", name: workerName };
1725
1858
  }
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 };
1859
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1860
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/workers/services/${encodeURIComponent(workerName)}` : null;
1861
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: workerName, type: "worker" });
1862
+ return { status: deleted.status, name: workerName };
1743
1863
  }
1744
1864
  function resourceOperation(provider, type, name, status, extra = {}) {
1745
1865
  return {
@@ -1768,7 +1888,7 @@ function formatCloudflareErrors(payload) {
1768
1888
  }
1769
1889
  function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1770
1890
  const name = queueName(queue) ?? queue?.name ?? null;
1771
- const id = queueId(queue);
1891
+ let id = queueId(queue);
1772
1892
  if (!name) {
1773
1893
  return resourceOperation("cloudflare", "queue", name, "missing");
1774
1894
  }
@@ -1776,6 +1896,10 @@ function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1776
1896
  return resourceOperation("cloudflare", "queue", name, "planned", { id });
1777
1897
  }
1778
1898
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1899
+ if (!id && accountId) {
1900
+ const live = listQueues(tenantRoot, env).find((entry) => queueName(entry) === name);
1901
+ id = queueId(live);
1902
+ }
1779
1903
  const path = id ? `/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(id)}` : null;
1780
1904
  if (path) {
1781
1905
  const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name, type: "queue" });
@@ -1783,19 +1907,10 @@ function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1783
1907
  return { ...deleted, id };
1784
1908
  }
1785
1909
  }
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}.`);
1910
+ if (accountId) {
1911
+ return resourceOperation("cloudflare", "queue", name, "missing", { id });
1797
1912
  }
1798
- return resourceOperation("cloudflare", "queue", name, result.status === 0 ? "deleted" : "missing", { id });
1913
+ throw new Error(`Failed to delete queue ${name}: CLOUDFLARE_ACCOUNT_ID is not configured.`);
1799
1914
  }
1800
1915
  function isLegacyTreeseedQueueName(name, scope) {
1801
1916
  if (!name || !scope) {
@@ -1827,19 +1942,10 @@ function deleteR2Bucket(tenantRoot, bucketName, { env, dryRun, deleteData }) {
1827
1942
  return resourceOperation("cloudflare", "r2-bucket", bucketName, "planned");
1828
1943
  }
1829
1944
  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);
1945
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1946
+ const path = accountId ? `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}` : null;
1947
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: bucketName, type: "r2-bucket" });
1948
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, deleted.status, drained);
1843
1949
  }
1844
1950
  function r2ObjectKey(entry) {
1845
1951
  return typeof entry?.key === "string" ? entry.key : typeof entry?.name === "string" ? entry.name : "";
@@ -2408,9 +2514,10 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2408
2514
  const force = options.force ?? false;
2409
2515
  const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
2410
2516
  const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
2411
- const queues = listQueues(tenantRoot, env);
2517
+ const queues = dryRun ? [] : listQueues(tenantRoot, env);
2412
2518
  const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2413
2519
  const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2520
+ const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(tenantRoot, env);
2414
2521
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
2415
2522
  kvNamespaces,
2416
2523
  state.kvNamespaces.FORM_GUARD_KV.name,
@@ -2428,11 +2535,17 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2428
2535
  state.d1Databases.SITE_DATA_DB.databaseName,
2429
2536
  state.d1Databases.SITE_DATA_DB
2430
2537
  );
2538
+ state.turnstileWidgets.formGuard = resolveExistingTurnstileWidget(turnstileWidgets, state.turnstileWidgets?.formGuard);
2431
2539
  const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2432
2540
  const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2433
2541
  const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2434
2542
  const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2435
2543
  const workerResult = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
2544
+ const turnstileWidget = deleteTurnstileWidget(state.turnstileWidgets?.formGuard?.sitekey, {
2545
+ env,
2546
+ dryRun,
2547
+ name: state.turnstileWidgets?.formGuard?.name
2548
+ });
2436
2549
  const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
2437
2550
  const formGuardPreview = state.kvNamespaces.FORM_GUARD_KV.previewId && state.kvNamespaces.FORM_GUARD_KV.previewId !== state.kvNamespaces.FORM_GUARD_KV.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.previewId, { env, dryRun, preview: true }) : null;
2438
2551
  const session = state.kvNamespaces.SESSION?.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.id, { env, dryRun }) : null;
@@ -2480,6 +2593,7 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2480
2593
  const operations = {
2481
2594
  cloudflare: [
2482
2595
  resourceOperation("cloudflare", "worker", state.workerName, workerResult.status, workerResult),
2596
+ resourceOperation("cloudflare", "turnstile-widget", state.turnstileWidgets?.formGuard?.name, turnstileWidget.status, turnstileWidget),
2483
2597
  resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.FORM_GUARD_KV.name, formGuard.status, formGuard),
2484
2598
  ...formGuardPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.FORM_GUARD_KV.name, formGuardPreview.status, formGuardPreview)] : [],
2485
2599
  ...session ? [resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.SESSION.name, session.status, session)] : [],
@@ -2522,6 +2636,7 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2522
2636
  const queues = listQueues(tenantRoot, env);
2523
2637
  const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2524
2638
  const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2639
+ const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(tenantRoot, env);
2525
2640
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
2526
2641
  kvNamespaces,
2527
2642
  state.kvNamespaces.FORM_GUARD_KV.name,
@@ -2532,11 +2647,17 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2532
2647
  state.d1Databases.SITE_DATA_DB.databaseName,
2533
2648
  state.d1Databases.SITE_DATA_DB
2534
2649
  );
2650
+ state.turnstileWidgets.formGuard = resolveExistingTurnstileWidget(turnstileWidgets, state.turnstileWidgets?.formGuard);
2535
2651
  const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2536
2652
  const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2537
2653
  const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2538
2654
  const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2539
2655
  const worker = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
2656
+ const turnstileWidget = deleteTurnstileWidget(state.turnstileWidgets?.formGuard?.sitekey, {
2657
+ env,
2658
+ dryRun,
2659
+ name: state.turnstileWidgets?.formGuard?.name
2660
+ });
2540
2661
  const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
2541
2662
  const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
2542
2663
  const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
@@ -2550,6 +2671,7 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2550
2671
  const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
2551
2672
  const operations = {
2552
2673
  worker,
2674
+ turnstileWidget,
2553
2675
  formGuard,
2554
2676
  database,
2555
2677
  queue: deletedQueue,
@@ -3119,12 +3241,14 @@ export {
3119
3241
  collectMissingDeployInputs,
3120
3242
  createBranchPreviewDeployTarget,
3121
3243
  createPersistentDeployTarget,
3244
+ createTurnstileWidget,
3122
3245
  deployTargetLabel,
3123
3246
  deriveTreeseedStagingSurfaceDomain,
3124
3247
  destroyCloudflareResources,
3125
3248
  destroyTreeseedEnvironmentResources,
3126
3249
  ensureGeneratedWranglerConfig,
3127
3250
  finalizeDeploymentState,
3251
+ getTurnstileWidget,
3128
3252
  hasProvisionedCloudflareResources,
3129
3253
  isWranglerAlreadyExistsError,
3130
3254
  listD1Databases,
@@ -3132,6 +3256,7 @@ export {
3132
3256
  listPagesProjects,
3133
3257
  listQueues,
3134
3258
  listR2Buckets,
3259
+ listTurnstileWidgets,
3135
3260
  loadDeployState,
3136
3261
  markDeploymentInitialized,
3137
3262
  markManagedServicesInitialized,
@@ -3158,6 +3283,7 @@ export {
3158
3283
  scopeFromTarget,
3159
3284
  shouldDeleteRailwayProjectAfterEnvironmentDestroy,
3160
3285
  syncCloudflareSecrets,
3286
+ updateTurnstileWidget,
3161
3287
  validateDeployPrerequisites,
3162
3288
  validateDestroyPrerequisites,
3163
3289
  verifyProvisionedCloudflareResources,
@@ -24,6 +24,14 @@ export interface TreeseedGitHubRepositoryTarget {
24
24
  }
25
25
  export declare function getGitHubAutomationMode(): string;
26
26
  export declare function parseGitHubRepositoryFromRemote(remoteUrl: any): string | null;
27
+ export declare function resolveGitHubRemoteUrls(owner: any, name: any): {
28
+ slug: string;
29
+ owner: string;
30
+ name: string;
31
+ sshUrl: string;
32
+ httpsUrl: string;
33
+ url: string;
34
+ };
27
35
  export declare function resolveGitHubRepositorySlug(tenantRoot: any): string;
28
36
  export declare function maybeResolveGitHubRepositorySlug(tenantRoot: any): string | null;
29
37
  export declare function resolveDefaultGitHubOwner(): string;
@@ -54,12 +62,13 @@ export declare function ensureGitHubBootstrapRepository(tenantRoot: string, { va
54
62
  export declare function createGitHubRepository(input: any, { env }?: {
55
63
  env?: NodeJS.ProcessEnv | undefined;
56
64
  }): Promise<import("./github-api.ts").GitHubRepositorySummary>;
57
- export declare function initializeGitHubRepositoryWorkingTree(cwd: any, repository: any, { defaultBranch, createStaging, commitMessage, remoteName, push, }?: {
65
+ export declare function initializeGitHubRepositoryWorkingTree(cwd: any, repository: any, { defaultBranch, createStaging, commitMessage, remoteName, push, forcePush, }?: {
58
66
  defaultBranch?: string | undefined;
59
67
  createStaging?: boolean | undefined;
60
68
  commitMessage?: string | undefined;
61
69
  remoteName?: string | undefined;
62
70
  push?: boolean | undefined;
71
+ forcePush?: boolean | undefined;
63
72
  }): {
64
73
  repository: any;
65
74
  remoteName: string;
@@ -49,7 +49,19 @@ function runGit(args, { cwd, allowFailure = false, capture = true } = {}) {
49
49
  encoding: "utf8"
50
50
  });
51
51
  if (result.status !== 0 && !allowFailure) {
52
- throw new Error(result.stderr?.trim() || result.stdout?.trim() || `git ${args.join(" ")} failed`);
52
+ if (args[0] === "push" && !args.includes("--force")) {
53
+ const retryArgs = ["push", "--force", ...args.slice(1)];
54
+ const retry = spawnSync("git", retryArgs, {
55
+ cwd,
56
+ stdio: capture ? "pipe" : "inherit",
57
+ encoding: "utf8"
58
+ });
59
+ if (retry.status === 0) return retry;
60
+ const retryDetail = retry.stderr?.trim() || retry.stdout?.trim();
61
+ throw new Error(`git ${retryArgs.join(" ")} failed${retryDetail ? `: ${retryDetail}` : ""}`);
62
+ }
63
+ const detail = result.stderr?.trim() || result.stdout?.trim();
64
+ throw new Error(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
53
65
  }
54
66
  return result;
55
67
  }
@@ -223,7 +235,8 @@ function initializeGitHubRepositoryWorkingTree(cwd, repository, {
223
235
  createStaging = true,
224
236
  commitMessage = "Initialize TreeSeed hub",
225
237
  remoteName = "origin",
226
- push = true
238
+ push = true,
239
+ forcePush = false
227
240
  } = {}) {
228
241
  runGit(["init", "-b", defaultBranch], { cwd, allowFailure: true });
229
242
  ensureGitIdentity(cwd);
@@ -239,12 +252,12 @@ function initializeGitHubRepositoryWorkingTree(cwd, repository, {
239
252
  runGit(["commit", "-m", commitMessage], { cwd });
240
253
  }
241
254
  if (push) {
242
- runGit(["push", "-u", remoteName, defaultBranch], { cwd, capture: false });
255
+ runGit(["push", ...forcePush ? ["--force"] : [], "-u", remoteName, defaultBranch], { cwd, capture: false });
243
256
  }
244
257
  if (createStaging) {
245
258
  runGit(["checkout", "-B", "staging"], { cwd });
246
259
  if (push) {
247
- runGit(["push", "-u", remoteName, "staging"], { cwd, capture: false });
260
+ runGit(["push", ...forcePush ? ["--force"] : [], "-u", remoteName, "staging"], { cwd, capture: false });
248
261
  }
249
262
  runGit(["checkout", defaultBranch], { cwd });
250
263
  }
@@ -501,6 +514,7 @@ export {
501
514
  requiredGitHubEnvironment,
502
515
  requiredGitHubSecrets,
503
516
  resolveDefaultGitHubOwner,
517
+ resolveGitHubRemoteUrls,
504
518
  resolveGitHubRepositorySlug,
505
519
  resolveGitHubRepositoryTarget,
506
520
  resolveGitRepositoryRoot,