@treeseed/sdk 0.10.22 → 0.10.24

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 (47) 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/bootstrap-runner.d.ts +5 -1
  7. package/dist/operations/services/bootstrap-runner.js +34 -5
  8. package/dist/operations/services/config-runtime.d.ts +2 -1
  9. package/dist/operations/services/deploy.d.ts +18 -1
  10. package/dist/operations/services/deploy.js +176 -24
  11. package/dist/operations/services/github-automation.d.ts +10 -1
  12. package/dist/operations/services/github-automation.js +18 -4
  13. package/dist/operations/services/hosting-audit.d.ts +2 -1
  14. package/dist/operations/services/hosting-audit.js +12 -1
  15. package/dist/operations/services/hub-launch.d.ts +1 -0
  16. package/dist/operations/services/hub-launch.js +1 -0
  17. package/dist/operations/services/hub-provider-launch.d.ts +9 -0
  18. package/dist/operations/services/hub-provider-launch.js +140 -40
  19. package/dist/operations/services/managed-host-security.d.ts +1 -1
  20. package/dist/operations/services/managed-host-security.js +4 -1
  21. package/dist/operations/services/project-platform.d.ts +25 -0
  22. package/dist/operations/services/project-platform.js +91 -23
  23. package/dist/operations/services/railway-api.js +2 -1
  24. package/dist/operations/services/railway-deploy.d.ts +32 -2
  25. package/dist/operations/services/railway-deploy.js +94 -27
  26. package/dist/operations/services/template-registry.js +33 -3
  27. package/dist/platform/contracts.d.ts +1 -0
  28. package/dist/platform/deploy-config.js +8 -1
  29. package/dist/platform/deploy-runtime.js +1 -0
  30. package/dist/platform/environment.d.ts +1 -1
  31. package/dist/platform/environment.js +1 -1
  32. package/dist/reconcile/builtin-adapters.js +155 -25
  33. package/dist/reconcile/contracts.d.ts +1 -1
  34. package/dist/reconcile/desired-state.js +17 -1
  35. package/dist/reconcile/engine.d.ts +2 -0
  36. package/dist/reconcile/engine.js +58 -3
  37. package/dist/reconcile/units.js +1 -0
  38. package/dist/sdk-types.d.ts +1 -1
  39. package/dist/sdk-types.js +2 -0
  40. package/dist/timing.d.ts +20 -0
  41. package/dist/timing.js +73 -0
  42. package/dist/treeseed/template-catalog/catalog.fixture.json +150 -0
  43. package/dist/workflow/operations.d.ts +2 -0
  44. package/drizzle/market/0000_market_control_plane.sql +3 -3
  45. package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
  46. package/package.json +1 -1
  47. package/templates/github/deploy-web.workflow.yml +4 -0
@@ -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,
@@ -1533,14 +1650,7 @@ function resolveConfiguredContentPublicBaseUrl(deployConfig) {
1533
1650
  return envOrNull("TREESEED_CONTENT_PUBLIC_BASE_URL") ?? deployConfig.cloudflare.r2?.publicBaseUrl ?? "";
1534
1651
  }
1535
1652
  function missingTurnstileRequirements() {
1536
- const issues = [];
1537
- if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
1538
- issues.push("Set TREESEED_PUBLIC_TURNSTILE_SITE_KEY before deploying.");
1539
- }
1540
- if (!envOrNull("TREESEED_TURNSTILE_SECRET_KEY")) {
1541
- issues.push("Set TREESEED_TURNSTILE_SECRET_KEY before deploying.");
1542
- }
1543
- return issues;
1653
+ return [];
1544
1654
  }
1545
1655
  function missingContentRuntimeRequirements(deployConfig) {
1546
1656
  const issues = [];
@@ -1564,20 +1674,6 @@ function collectMissingDeployInputs(tenantRoot) {
1564
1674
  message: "Cloudflare account ID is missing. Set CLOUDFLARE_ACCOUNT_ID with treeseed config or provide it now."
1565
1675
  });
1566
1676
  }
1567
- if (!envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY")) {
1568
- missing.push({
1569
- key: "TREESEED_PUBLIC_TURNSTILE_SITE_KEY",
1570
- label: "Turnstile public site key",
1571
- message: "Turnstile public site key is missing for deploy."
1572
- });
1573
- }
1574
- if (!envOrNull("TREESEED_TURNSTILE_SECRET_KEY")) {
1575
- missing.push({
1576
- key: "TREESEED_TURNSTILE_SECRET_KEY",
1577
- label: "Turnstile secret key",
1578
- message: "Turnstile secret key is missing for deploy."
1579
- });
1580
- }
1581
1677
  if (deployConfig.providers?.content?.runtime === "team_scoped_r2_overlay" && !envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET")) {
1582
1678
  missing.push({
1583
1679
  key: "TREESEED_EDITORIAL_PREVIEW_SECRET",
@@ -1674,6 +1770,24 @@ function resolveExistingKvIdByName(kvNamespaces, expectedName, fallbackId) {
1674
1770
  }
1675
1771
  return kvNamespaces.find((entry) => entry?.title === expectedName)?.id ?? null;
1676
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
+ }
1677
1791
  function resolveExistingD1ByName(d1Databases, expectedName, current) {
1678
1792
  if (current?.databaseId && !isPlaceholderResourceId(current.databaseId)) {
1679
1793
  return current;
@@ -1703,6 +1817,24 @@ function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = fal
1703
1817
  const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name: namespaceId, type: "kv-namespace" });
1704
1818
  return { status: deleted.status, id: namespaceId, preview };
1705
1819
  }
1820
+ function deleteTurnstileWidget(sitekey, { env, dryRun, name = null }) {
1821
+ if (!sitekey || isPlaceholderResourceId(sitekey)) {
1822
+ return { status: "missing", sitekey, name };
1823
+ }
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}`);
1835
+ }
1836
+ return { status: deleted.status, sitekey, name };
1837
+ }
1706
1838
  function deleteD1Database(tenantRoot, databaseName, { env, dryRun }) {
1707
1839
  if (!databaseName) {
1708
1840
  return { status: "missing", name: databaseName };
@@ -2385,6 +2517,7 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2385
2517
  const queues = dryRun ? [] : listQueues(tenantRoot, env);
2386
2518
  const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2387
2519
  const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2520
+ const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(tenantRoot, env);
2388
2521
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
2389
2522
  kvNamespaces,
2390
2523
  state.kvNamespaces.FORM_GUARD_KV.name,
@@ -2402,11 +2535,17 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2402
2535
  state.d1Databases.SITE_DATA_DB.databaseName,
2403
2536
  state.d1Databases.SITE_DATA_DB
2404
2537
  );
2538
+ state.turnstileWidgets.formGuard = resolveExistingTurnstileWidget(turnstileWidgets, state.turnstileWidgets?.formGuard);
2405
2539
  const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2406
2540
  const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2407
2541
  const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2408
2542
  const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2409
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
+ });
2410
2549
  const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
2411
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;
2412
2551
  const session = state.kvNamespaces.SESSION?.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.id, { env, dryRun }) : null;
@@ -2454,6 +2593,7 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2454
2593
  const operations = {
2455
2594
  cloudflare: [
2456
2595
  resourceOperation("cloudflare", "worker", state.workerName, workerResult.status, workerResult),
2596
+ resourceOperation("cloudflare", "turnstile-widget", state.turnstileWidgets?.formGuard?.name, turnstileWidget.status, turnstileWidget),
2457
2597
  resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.FORM_GUARD_KV.name, formGuard.status, formGuard),
2458
2598
  ...formGuardPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.FORM_GUARD_KV.name, formGuardPreview.status, formGuardPreview)] : [],
2459
2599
  ...session ? [resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.SESSION.name, session.status, session)] : [],
@@ -2496,6 +2636,7 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2496
2636
  const queues = listQueues(tenantRoot, env);
2497
2637
  const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2498
2638
  const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2639
+ const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(tenantRoot, env);
2499
2640
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
2500
2641
  kvNamespaces,
2501
2642
  state.kvNamespaces.FORM_GUARD_KV.name,
@@ -2506,11 +2647,17 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2506
2647
  state.d1Databases.SITE_DATA_DB.databaseName,
2507
2648
  state.d1Databases.SITE_DATA_DB
2508
2649
  );
2650
+ state.turnstileWidgets.formGuard = resolveExistingTurnstileWidget(turnstileWidgets, state.turnstileWidgets?.formGuard);
2509
2651
  const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2510
2652
  const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2511
2653
  const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2512
2654
  const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2513
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
+ });
2514
2661
  const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
2515
2662
  const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
2516
2663
  const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
@@ -2524,6 +2671,7 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
2524
2671
  const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
2525
2672
  const operations = {
2526
2673
  worker,
2674
+ turnstileWidget,
2527
2675
  formGuard,
2528
2676
  database,
2529
2677
  queue: deletedQueue,
@@ -3093,12 +3241,14 @@ export {
3093
3241
  collectMissingDeployInputs,
3094
3242
  createBranchPreviewDeployTarget,
3095
3243
  createPersistentDeployTarget,
3244
+ createTurnstileWidget,
3096
3245
  deployTargetLabel,
3097
3246
  deriveTreeseedStagingSurfaceDomain,
3098
3247
  destroyCloudflareResources,
3099
3248
  destroyTreeseedEnvironmentResources,
3100
3249
  ensureGeneratedWranglerConfig,
3101
3250
  finalizeDeploymentState,
3251
+ getTurnstileWidget,
3102
3252
  hasProvisionedCloudflareResources,
3103
3253
  isWranglerAlreadyExistsError,
3104
3254
  listD1Databases,
@@ -3106,6 +3256,7 @@ export {
3106
3256
  listPagesProjects,
3107
3257
  listQueues,
3108
3258
  listR2Buckets,
3259
+ listTurnstileWidgets,
3109
3260
  loadDeployState,
3110
3261
  markDeploymentInitialized,
3111
3262
  markManagedServicesInitialized,
@@ -3132,6 +3283,7 @@ export {
3132
3283
  scopeFromTarget,
3133
3284
  shouldDeleteRailwayProjectAfterEnvironmentDestroy,
3134
3285
  syncCloudflareSecrets,
3286
+ updateTurnstileWidget,
3135
3287
  validateDeployPrerequisites,
3136
3288
  validateDestroyPrerequisites,
3137
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,
@@ -53,6 +53,7 @@ export type TreeseedHostingAuditOptions = {
53
53
  valuesOverlay?: Record<string, string | undefined>;
54
54
  hostKinds?: TreeseedHostingAuditHostKind[];
55
55
  providerConnectionChecks?: boolean;
56
+ resourceChecks?: boolean;
56
57
  write?: (line: string) => void;
57
58
  };
58
59
  export declare function resolveTreeseedHostingAuditTarget({ tenantRoot, environment, }: {
@@ -64,5 +65,5 @@ export declare function resolveTreeseedHostingAuditTarget({ tenantRoot, environm
64
65
  target: TreeseedReconcileTarget;
65
66
  branchName: string | null;
66
67
  };
67
- export declare function runTreeseedHostingAudit({ tenantRoot, environment, repair, env, valuesOverlay, hostKinds: requestedHostKinds, providerConnectionChecks: shouldCheckProviderConnections, write, }: TreeseedHostingAuditOptions): Promise<TreeseedHostingAuditReport>;
68
+ export declare function runTreeseedHostingAudit({ tenantRoot, environment, repair, env, valuesOverlay, hostKinds: requestedHostKinds, providerConnectionChecks: shouldCheckProviderConnections, resourceChecks: shouldCheckResources, write, }: TreeseedHostingAuditOptions): Promise<TreeseedHostingAuditReport>;
68
69
  export declare function formatTreeseedHostingAuditReport(report: TreeseedHostingAuditReport): string;
@@ -463,6 +463,7 @@ async function runTreeseedHostingAudit({
463
463
  valuesOverlay = {},
464
464
  hostKinds: requestedHostKinds,
465
465
  providerConnectionChecks: shouldCheckProviderConnections = true,
466
+ resourceChecks: shouldCheckResources = true,
466
467
  write
467
468
  }) {
468
469
  const resolved = resolveTreeseedHostingAuditTarget({ tenantRoot, environment });
@@ -509,7 +510,17 @@ async function runTreeseedHostingAudit({
509
510
  const systems = reconcileSystemsForHostKinds(hostKinds);
510
511
  let resources = {};
511
512
  let repaired = false;
512
- if (resolved.environment !== "local" && systems.length > 0) {
513
+ if (!shouldCheckResources) {
514
+ checks.push({
515
+ id: "resources.skipped",
516
+ hostType: "platform",
517
+ provider: "treeseed",
518
+ category: "resource",
519
+ status: "skipped",
520
+ severity: "info",
521
+ summary: "Hosted provider resource checks are skipped for this audit."
522
+ });
523
+ } else if (resolved.environment !== "local" && systems.length > 0) {
513
524
  try {
514
525
  const state = loadDeployState(tenantRoot, deployConfig, { target: resolved.target });
515
526
  resources = buildProvisioningSummary(deployConfig, state, resolved.target);
@@ -42,6 +42,7 @@ export interface KnowledgeHubLaunchIntent {
42
42
  name: string;
43
43
  slug: string;
44
44
  purpose?: string | null;
45
+ coreObjective?: string | null;
45
46
  visibility?: 'private' | 'team' | 'public';
46
47
  };
47
48
  source?: {
@@ -193,6 +193,7 @@ function providerLaunchInputFromIntent(plan) {
193
193
  projectSlug: intent.hub.slug,
194
194
  projectName: intent.hub.name,
195
195
  summary: intent.hub.purpose ?? null,
196
+ coreObjective: providerInput.coreObjective ?? intent.hub.coreObjective ?? intent.hub.purpose ?? null,
196
197
  sourceKind: sourceKind === "blank_hub" ? "blank" : sourceKind === "market_listing" ? "template" : sourceKind,
197
198
  sourceRef: intent.source?.ref ?? null,
198
199
  hostingMode: intent.hosting?.mode === "treeseed_managed" ? "managed" : intent.hosting?.mode ?? "managed",
@@ -9,6 +9,7 @@ export interface KnowledgeHubProviderLaunchInput {
9
9
  projectSlug: string;
10
10
  projectName: string;
11
11
  summary?: string | null;
12
+ coreObjective?: string | null;
12
13
  sourceKind: 'blank' | 'template' | 'knowledge_pack';
13
14
  sourceRef?: string | null;
14
15
  hostingMode?: 'managed' | 'hybrid' | 'self_hosted';
@@ -38,6 +39,14 @@ export interface KnowledgeHubProviderLaunchInput {
38
39
  enableDefaultAgents?: boolean;
39
40
  preserveWorkingTree?: boolean;
40
41
  cloudflareHost?: KnowledgeHubCloudflareHostLaunchInput | null;
42
+ domains?: {
43
+ productionDomain?: string | null;
44
+ stagingDomain?: string | null;
45
+ zoneName?: string | null;
46
+ zoneId?: string | null;
47
+ manageDns?: boolean;
48
+ provider?: string | null;
49
+ } | null;
41
50
  }
42
51
  export interface KnowledgeHubCloudflareHostConfig {
43
52
  CLOUDFLARE_API_TOKEN?: string;