@treeseed/sdk 0.10.22 → 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 +18 -1
  8. package/dist/operations/services/deploy.js +176 -24
  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 +2 -1
  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
@@ -297,13 +297,14 @@ const webSessions = pgTable("web_sessions", {
297
297
  const projects = pgTable("projects", {
298
298
  id: text("id").primaryKey(),
299
299
  teamId: text("team_id").notNull(),
300
- slug: text("slug").notNull().unique(),
300
+ slug: text("slug").notNull(),
301
301
  name: text("name").notNull(),
302
302
  description: text("description"),
303
303
  metadataJson: text("metadata_json"),
304
304
  createdAt: text("created_at").notNull(),
305
305
  updatedAt: text("updated_at").notNull()
306
306
  }, (table) => [
307
+ uniqueIndex("idx_projects_team_slug").on(table.teamId, table.slug),
307
308
  index("idx_projects_team_id").on(table.teamId)
308
309
  ]);
309
310
  const projectConnections = pgTable("project_connections", {
@@ -445,7 +446,7 @@ const catalogItems = pgTable("catalog_items", {
445
446
  createdAt: text("created_at").notNull(),
446
447
  updatedAt: text("updated_at").notNull()
447
448
  }, (table) => [
448
- uniqueIndex("idx_catalog_items_kind_slug").on(table.kind, table.slug),
449
+ uniqueIndex("idx_catalog_items_team_kind_slug").on(table.teamId, table.kind, table.slug),
449
450
  index("idx_catalog_items_team_kind").on(table.teamId, table.kind, table.updatedAt),
450
451
  index("idx_catalog_items_visibility_listing").on(table.visibility, table.listingEnabled, table.updatedAt)
451
452
  ]);
@@ -374,6 +374,10 @@ export declare class MarketClient {
374
374
  ok: true;
375
375
  payload: ProjectDeployment;
376
376
  }>;
377
+ projectDeploymentById(deploymentId: string): Promise<{
378
+ ok: true;
379
+ payload: ProjectDeployment;
380
+ }>;
377
381
  projectDeploymentEvents(projectId: string, deploymentId: string, options?: {
378
382
  limit?: number | string | null;
379
383
  }): Promise<{
@@ -485,6 +485,12 @@ class MarketClient {
485
485
  { requireAuth: true }
486
486
  );
487
487
  }
488
+ projectDeploymentById(deploymentId) {
489
+ return this.request(
490
+ `/v1/project-deployments/${encodeURIComponent(deploymentId)}`,
491
+ { requireAuth: true }
492
+ );
493
+ }
488
494
  projectDeploymentEvents(projectId, deploymentId, options = {}) {
489
495
  const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
490
496
  return this.request(
@@ -91,6 +91,28 @@ function failureResult(metadata, message, options = {}) {
91
91
  function contextEnv(context) {
92
92
  return { ...process.env, ...context.env ?? {} };
93
93
  }
94
+ async function withTemporaryProcessEnv(env, action) {
95
+ const previous = /* @__PURE__ */ new Map();
96
+ for (const [key, value] of Object.entries(env)) {
97
+ previous.set(key, process.env[key]);
98
+ if (value === void 0) {
99
+ delete process.env[key];
100
+ } else {
101
+ process.env[key] = value;
102
+ }
103
+ }
104
+ try {
105
+ return await action();
106
+ } finally {
107
+ for (const [key, value] of previous) {
108
+ if (value === void 0) {
109
+ delete process.env[key];
110
+ } else {
111
+ process.env[key] = value;
112
+ }
113
+ }
114
+ }
115
+ }
94
116
  function operationEnv(context) {
95
117
  const tenantConfigPath = resolve(context.cwd, "treeseed.site.yaml");
96
118
  return existsSync(tenantConfigPath) ? resolveTreeseedLaunchEnvironment({ tenantRoot: context.cwd, scope: "local", baseEnv: contextEnv(context) }) : contextEnv(context);
@@ -399,21 +421,21 @@ class HubValidateLaunchOperation extends BaseOperation {
399
421
  class HubExecuteLaunchOperation extends BaseOperation {
400
422
  async execute(input, context) {
401
423
  const intent = input.intent && typeof input.intent === "object" ? input.intent : input;
402
- const result = await executeKnowledgeHubLaunch(intent, {
424
+ const result = await withTemporaryProcessEnv(contextEnv(context), () => executeKnowledgeHubLaunch(intent, {
403
425
  onPhase: async (phase) => {
404
426
  await context.onProgress?.({
405
427
  kind: "hub_launch_phase",
406
428
  ...phase
407
429
  });
408
430
  }
409
- });
431
+ }));
410
432
  return operationResult(this.metadata, result);
411
433
  }
412
434
  }
413
435
  class HubResumeLaunchOperation extends BaseOperation {
414
436
  async execute(input, context) {
415
437
  const intent = input.intent && typeof input.intent === "object" ? input.intent : input;
416
- const result = await executeKnowledgeHubLaunch(intent, {
438
+ const result = await withTemporaryProcessEnv(contextEnv(context), () => executeKnowledgeHubLaunch(intent, {
417
439
  onPhase: async (phase) => {
418
440
  await context.onProgress?.({
419
441
  kind: "hub_launch_phase",
@@ -421,7 +443,7 @@ class HubResumeLaunchOperation extends BaseOperation {
421
443
  ...phase
422
444
  });
423
445
  }
424
- });
446
+ }));
425
447
  return operationResult(this.metadata, {
426
448
  resumed: true,
427
449
  ...result
@@ -310,15 +310,19 @@ async function writeContentRecord(repoPath, collection, input, normalizedInput)
310
310
  const existingTarget = input.overwrite === true ? [`${normalized.slug}.mdx`, `${normalized.slug}.md`].map((file) => resolve(root, file)).find((candidate) => existsSync(candidate)) : null;
311
311
  const target = existingTarget ?? safeContentPath(repoPath, collection, normalized.slug, normalized.extension);
312
312
  if (existsSync(target) && input.overwrite !== true) throw new Error("A content record with that slug already exists.");
313
+ const frontmatter = existingTarget && input.preserveFrontmatter === true ? {
314
+ ...normalized.frontmatter,
315
+ ...(await readContentRecord(repoPath, collection, normalized.slug)).frontmatter
316
+ } : normalized.frontmatter;
313
317
  const relativePath = await writeParsedRecord(repoPath, {
314
318
  path: target,
315
- frontmatter: normalized.frontmatter,
319
+ frontmatter,
316
320
  body: normalized.body
317
321
  });
318
322
  return {
319
323
  collection,
320
324
  slug: normalized.slug,
321
- id: normalized.frontmatter.id,
325
+ id: frontmatter.id,
322
326
  path: relativePath,
323
327
  href: collection === "agents" ? `/app/projects/${encodeURIComponent(String(input.projectId ?? ""))}/agents/${encodeURIComponent(normalized.slug)}` : `/app/work/${collection}/${encodeURIComponent(normalized.slug)}`
324
328
  };
@@ -55,7 +55,7 @@ export type TreeseedConfigEntrySnapshot = {
55
55
  group: string;
56
56
  cluster: string;
57
57
  startupProfile: 'core' | 'optional' | 'advanced';
58
- requirement: 'required' | 'conditional' | 'optional';
58
+ requirement: 'required' | 'conditional' | 'optional' | 'generated';
59
59
  description: string;
60
60
  howToGet: string;
61
61
  sensitivity: 'secret' | 'plain' | 'derived';
@@ -85,7 +85,7 @@ export declare function buildPublicVars(deployConfig: any, options?: {}): {
85
85
  export declare function buildSecretMap(deployConfig: any, state: any): {
86
86
  TREESEED_FORM_TOKEN_SECRET: any;
87
87
  TREESEED_EDITORIAL_PREVIEW_SECRET: any;
88
- TREESEED_TURNSTILE_SECRET_KEY: string | null;
88
+ TREESEED_TURNSTILE_SECRET_KEY: any;
89
89
  TREESEED_SMTP_PASSWORD: string | null;
90
90
  };
91
91
  export declare function loadDeployState(tenantRoot: any, deployConfig: any, options?: {}): any;
@@ -307,6 +307,10 @@ export declare function listD1Databases(tenantRoot: any, env: any): any;
307
307
  export declare function listQueues(tenantRoot: any, env: any): any;
308
308
  export declare function listR2Buckets(tenantRoot: any, env: any): any;
309
309
  export declare function listPagesProjects(tenantRoot: any, env: any): any;
310
+ export declare function listTurnstileWidgets(tenantRoot: any, env: any): any;
311
+ export declare function getTurnstileWidget(env: any, sitekey: any): any;
312
+ export declare function createTurnstileWidget(env: any, input: any): any;
313
+ export declare function updateTurnstileWidget(env: any, sitekey: any, input: any): any;
310
314
  export declare function buildCloudflarePagesFunctionBindings(state: any): {
311
315
  r2_buckets?: {
312
316
  [x: number]: {
@@ -330,6 +334,7 @@ export declare function buildProvisioningSummary(deployConfig: any, state: any,
330
334
  siteUrl: any;
331
335
  accountId: any;
332
336
  pages: any;
337
+ turnstileWidget: any;
333
338
  formGuardKv: any;
334
339
  sessionKv: any;
335
340
  siteDataDb: any;
@@ -341,6 +346,7 @@ export declare function buildProvisioningSummary(deployConfig: any, state: any,
341
346
  queue: any;
342
347
  dlq: any;
343
348
  database: any;
349
+ turnstileWidget: any;
344
350
  formGuardKv: any;
345
351
  railwayProject: any;
346
352
  webDomain: any;
@@ -891,6 +897,7 @@ export declare function destroyTreeseedEnvironmentResources(tenantRoot: any, opt
891
897
  siteUrl: any;
892
898
  accountId: any;
893
899
  pages: any;
900
+ turnstileWidget: any;
894
901
  formGuardKv: any;
895
902
  sessionKv: any;
896
903
  siteDataDb: any;
@@ -902,6 +909,7 @@ export declare function destroyTreeseedEnvironmentResources(tenantRoot: any, opt
902
909
  queue: any;
903
910
  dlq: any;
904
911
  database: any;
912
+ turnstileWidget: any;
905
913
  formGuardKv: any;
906
914
  railwayProject: any;
907
915
  webDomain: any;
@@ -963,6 +971,7 @@ export declare function destroyCloudflareResources(tenantRoot: any, options?: {}
963
971
  siteUrl: any;
964
972
  accountId: any;
965
973
  pages: any;
974
+ turnstileWidget: any;
966
975
  formGuardKv: any;
967
976
  sessionKv: any;
968
977
  siteDataDb: any;
@@ -974,6 +983,7 @@ export declare function destroyCloudflareResources(tenantRoot: any, options?: {}
974
983
  queue: any;
975
984
  dlq: any;
976
985
  database: any;
986
+ turnstileWidget: any;
977
987
  formGuardKv: any;
978
988
  railwayProject: any;
979
989
  webDomain: any;
@@ -1008,6 +1018,11 @@ export declare function destroyCloudflareResources(tenantRoot: any, options?: {}
1008
1018
  status: any;
1009
1019
  name: any;
1010
1020
  };
1021
+ turnstileWidget: {
1022
+ status: any;
1023
+ sitekey: any;
1024
+ name: null;
1025
+ };
1011
1026
  formGuard: {
1012
1027
  status: string;
1013
1028
  id: any;
@@ -1064,6 +1079,7 @@ export declare function provisionCloudflareResources(tenantRoot: any, options?:
1064
1079
  siteUrl: any;
1065
1080
  accountId: any;
1066
1081
  pages: any;
1082
+ turnstileWidget: any;
1067
1083
  formGuardKv: any;
1068
1084
  sessionKv: any;
1069
1085
  siteDataDb: any;
@@ -1075,6 +1091,7 @@ export declare function provisionCloudflareResources(tenantRoot: any, options?:
1075
1091
  queue: any;
1076
1092
  dlq: any;
1077
1093
  database: any;
1094
+ turnstileWidget: any;
1078
1095
  formGuardKv: any;
1079
1096
  railwayProject: any;
1080
1097
  webDomain: any;
@@ -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;