@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
@@ -191,7 +191,7 @@ function collectRailwayDeploymentStatusChecks(statusPayload, scope, services) {
191
191
  };
192
192
  });
193
193
  }
194
- function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackName, fallbackMountPath }) {
194
+ function normalizeRailwayCliVolume(value, { serviceId, serviceName, environmentId, fallbackName, fallbackMountPath }) {
195
195
  if (!value || typeof value !== "object") {
196
196
  return null;
197
197
  }
@@ -200,6 +200,10 @@ function normalizeRailwayCliVolume(value, { serviceId, environmentId, fallbackNa
200
200
  if (!id) {
201
201
  return null;
202
202
  }
203
+ const listedServiceName = typeof record.serviceName === "string" && record.serviceName.trim() ? record.serviceName.trim() : "";
204
+ if (listedServiceName && serviceName && listedServiceName !== serviceName) {
205
+ return null;
206
+ }
203
207
  const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : fallbackName;
204
208
  const mountPath = typeof record.mountPath === "string" && record.mountPath.trim() ? record.mountPath.trim() : fallbackMountPath;
205
209
  const sizeMb = typeof record.sizeMB === "number" ? record.sizeMB : null;
@@ -228,6 +232,7 @@ function normalizeRailwayCliVolumeList(value, options) {
228
232
  function listRailwayServiceVolumesWithCli({
229
233
  cwd,
230
234
  serviceId,
235
+ serviceName,
231
236
  environmentId,
232
237
  name,
233
238
  mountPath,
@@ -244,6 +249,7 @@ function listRailwayServiceVolumesWithCli({
244
249
  }
245
250
  return normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
246
251
  serviceId,
252
+ serviceName,
247
253
  environmentId,
248
254
  fallbackName: name,
249
255
  fallbackMountPath: mountPath
@@ -1776,6 +1782,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1776
1782
  const listResult = runRailway([...volumeArgs, "list", "--json"], cliOptions);
1777
1783
  const existingVolumes = normalizeRailwayCliVolumeList(parseRailwayJsonOutput(listResult.stdout ?? ""), {
1778
1784
  serviceId,
1785
+ serviceName,
1779
1786
  environmentId,
1780
1787
  fallbackName: name,
1781
1788
  fallbackMountPath: mountPath
@@ -1787,6 +1794,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1787
1794
  const createResult = runRailway([...volumeArgs, "add", "--mount-path", mountPath, "--json"], cliOptions);
1788
1795
  volume = normalizeRailwayCliVolume(parseRailwayJsonOutput(createResult.stdout ?? ""), {
1789
1796
  serviceId,
1797
+ serviceName,
1790
1798
  environmentId,
1791
1799
  fallbackName: name,
1792
1800
  fallbackMountPath: mountPath
@@ -1810,6 +1818,7 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1810
1818
  }
1811
1819
  const attachedVolume = (attachResult.status ?? 1) === 0 ? normalizeRailwayCliVolume(parseRailwayJsonOutput(attachResult.stdout ?? ""), {
1812
1820
  serviceId,
1821
+ serviceName,
1813
1822
  environmentId,
1814
1823
  fallbackName: name,
1815
1824
  fallbackMountPath: mountPath
@@ -1833,21 +1842,6 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1833
1842
  instance = volume.instances.find((entry) => entry.serviceId === serviceId && entry.environmentId === environmentId) ?? volume.instances[0] ?? null;
1834
1843
  updated = true;
1835
1844
  }
1836
- if (volume.name !== name || instance?.mountPath !== mountPath) {
1837
- const updateResult = runRailway([...volumeArgs, "update", "--volume", volume.id, "--name", name, "--mount-path", mountPath, "--json"], cliOptions);
1838
- const updatedVolume = normalizeRailwayCliVolume(parseRailwayJsonOutput(updateResult.stdout ?? ""), {
1839
- serviceId,
1840
- environmentId,
1841
- fallbackName: name,
1842
- fallbackMountPath: mountPath
1843
- });
1844
- volume = updatedVolume ?? {
1845
- ...volume,
1846
- name,
1847
- instances: volume.instances.map((entry) => ({ ...entry, mountPath }))
1848
- };
1849
- updated = true;
1850
- }
1851
1845
  const apiVolume = await waitForRailwayServiceVolumeMount({
1852
1846
  projectId,
1853
1847
  volumeId: volume.id,
@@ -1859,6 +1853,8 @@ async function ensureRailwayServiceVolumeWithCliFallback({
1859
1853
  });
1860
1854
  if (apiVolume) {
1861
1855
  volume = apiVolume;
1856
+ } else {
1857
+ throw new Error(`Railway volume ${name} was not attached to ${serviceName} at ${mountPath}.`);
1862
1858
  }
1863
1859
  return {
1864
1860
  volume,
@@ -1878,11 +1874,12 @@ async function waitForRailwayServiceVolumeMount({
1878
1874
  }) {
1879
1875
  for (let attempt = 0; attempt <= 24; attempt += 1) {
1880
1876
  const volumes = await listRailwayVolumes({ projectId, env });
1881
- const match = volumes.find(
1882
- (entry) => entry.id === volumeId || entry.name === volumeName || entry.instances.some(
1877
+ const mounted = volumes.find(
1878
+ (entry) => entry.instances.some(
1883
1879
  (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1884
1880
  )
1885
1881
  ) ?? null;
1882
+ const match = mounted ?? volumes.find((entry) => entry.id === volumeId) ?? volumes.find((entry) => entry.name === volumeName) ?? null;
1886
1883
  if (match?.instances.some(
1887
1884
  (instance) => instance.serviceId === serviceId && instance.environmentId === environmentId && instance.mountPath === mountPath
1888
1885
  )) {
@@ -1,7 +1,7 @@
1
1
  import type { TreeseedDeployConfig, TreeseedTenantConfig } from './contracts.ts';
2
2
  import { type LoadedTreeseedPluginEntry } from './plugins.ts';
3
3
  export declare const TREESEED_ENVIRONMENT_SCOPES: readonly ["local", "staging", "prod"];
4
- export declare const TREESEED_ENVIRONMENT_REQUIREMENTS: readonly ["required", "conditional", "optional"];
4
+ export declare const TREESEED_ENVIRONMENT_REQUIREMENTS: readonly ["required", "conditional", "optional", "generated"];
5
5
  export declare const TREESEED_ENVIRONMENT_TARGETS: readonly ["local-runtime", "local-cloudflare", "github-secret", "github-variable", "cloudflare-secret", "cloudflare-var", "railway-secret", "railway-var", "config-file"];
6
6
  export declare const TREESEED_ENVIRONMENT_PURPOSES: readonly ["dev", "save", "deploy", "destroy", "config"];
7
7
  export declare const TREESEED_ENVIRONMENT_SENSITIVITY: readonly ["secret", "plain", "derived"];
@@ -8,7 +8,7 @@ import { loadTreeseedDeployConfig } from "./deploy-config.js";
8
8
  import { loadTreeseedPlugins } from "./plugins.js";
9
9
  import { loadTreeseedManifest } from "./tenant-config.js";
10
10
  const TREESEED_ENVIRONMENT_SCOPES = ["local", "staging", "prod"];
11
- const TREESEED_ENVIRONMENT_REQUIREMENTS = ["required", "conditional", "optional"];
11
+ const TREESEED_ENVIRONMENT_REQUIREMENTS = ["required", "conditional", "optional", "generated"];
12
12
  const TREESEED_ENVIRONMENT_TARGETS = [
13
13
  "local-runtime",
14
14
  "local-cloudflare",
@@ -7,9 +7,11 @@ import {
7
7
  buildProvisioningSummary,
8
8
  buildSecretMap,
9
9
  cloudflareApiRequest,
10
+ createTurnstileWidget,
10
11
  createBranchPreviewDeployTarget,
11
12
  createPersistentDeployTarget,
12
13
  destroyCloudflareResources,
14
+ getTurnstileWidget,
13
15
  hasProvisionedCloudflareResources,
14
16
  isWranglerAlreadyExistsError,
15
17
  listD1Databases,
@@ -17,6 +19,7 @@ import {
17
19
  listPagesProjects,
18
20
  listQueues,
19
21
  listR2Buckets,
22
+ listTurnstileWidgets,
20
23
  mergeCloudflarePagesDeploymentConfig,
21
24
  loadDeployState,
22
25
  queueId,
@@ -26,6 +29,7 @@ import {
26
29
  resolveCloudflareZoneIdForHost,
27
30
  runWrangler,
28
31
  scopeFromTarget,
32
+ updateTurnstileWidget,
29
33
  writeDeployState
30
34
  } from "../operations/services/deploy.js";
31
35
  import {
@@ -33,7 +37,6 @@ import {
33
37
  deriveRailwayMarketOperationsRunnerVolumeName,
34
38
  ensureRailwayProjectContext,
35
39
  ensureRailwayServiceVolumeWithCliFallback,
36
- listRailwayServiceVolumesWithCli,
37
40
  runRailway,
38
41
  validateRailwayDeployPrerequisites
39
42
  } from "../operations/services/railway-deploy.js";
@@ -293,6 +296,30 @@ function getCloudflareKvById(env, namespaceId) {
293
296
  );
294
297
  return payload?.result ?? null;
295
298
  }
299
+ function normalizeTurnstileDomains(value) {
300
+ return [...new Set((Array.isArray(value) ? value : []).map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean))].sort();
301
+ }
302
+ function turnstileDomainsEqual(left, right) {
303
+ return JSON.stringify(normalizeTurnstileDomains(left)) === JSON.stringify(normalizeTurnstileDomains(right));
304
+ }
305
+ function mergeTurnstileWidget(...widgets) {
306
+ const merged = {};
307
+ for (const widget of widgets) {
308
+ if (!widget) continue;
309
+ for (const [key, value] of Object.entries(widget)) {
310
+ if (value === void 0 || value === null) continue;
311
+ if (key === "domains" && !Array.isArray(value)) continue;
312
+ merged[key] = value;
313
+ }
314
+ }
315
+ return Object.keys(merged).length > 0 ? merged : null;
316
+ }
317
+ function findTurnstileWidget(widgets, current, desiredName) {
318
+ return widgets.find((entry) => {
319
+ if (!entry || typeof entry !== "object") return false;
320
+ return current?.sitekey && entry.sitekey === current.sitekey || desiredName && entry.name === desiredName;
321
+ });
322
+ }
296
323
  function listCloudflareQueuesViaApi(env) {
297
324
  const accountId = env.CLOUDFLARE_ACCOUNT_ID;
298
325
  if (!accountId) {
@@ -314,7 +341,8 @@ function cloudflareObservationSnapshot(input, forceRefresh = false) {
314
341
  d1Databases: listD1Databases(input.context.tenantRoot, env),
315
342
  queues: listCloudflareQueuesViaApi(env),
316
343
  buckets: listR2Buckets(input.context.tenantRoot, env),
317
- pagesProjects: listPagesProjects(input.context.tenantRoot, env)
344
+ pagesProjects: listPagesProjects(input.context.tenantRoot, env),
345
+ turnstileWidgets: listTurnstileWidgets(input.context.tenantRoot, env)
318
346
  };
319
347
  }, forceRefresh);
320
348
  }
@@ -591,7 +619,10 @@ function collectCloudflareEnvironmentSync(input) {
591
619
  const generatedSecrets = buildSecretMap(input.context.deployConfig, state);
592
620
  const publicVars = buildPublicVars(input.context.deployConfig, { target });
593
621
  const secrets = {};
594
- const vars = { ...publicVars };
622
+ const generatedTurnstileSiteKey = typeof state.turnstileWidgets?.formGuard?.sitekey === "string" ? state.turnstileWidgets.formGuard.sitekey : "";
623
+ const vars = {
624
+ ...publicVars
625
+ };
595
626
  const secretNames = /* @__PURE__ */ new Set();
596
627
  const varNames = new Set(Object.keys(publicVars));
597
628
  for (const entry of registry.entries) {
@@ -611,8 +642,13 @@ function collectCloudflareEnvironmentSync(input) {
611
642
  varNames.add(entry.id);
612
643
  }
613
644
  }
645
+ if (generatedTurnstileSiteKey) {
646
+ vars.TREESEED_PUBLIC_TURNSTILE_SITE_KEY = generatedTurnstileSiteKey;
647
+ varNames.add("TREESEED_PUBLIC_TURNSTILE_SITE_KEY");
648
+ }
614
649
  for (const [key, value] of Object.entries(generatedSecrets)) {
615
- if (typeof value === "string" && value.length > 0 && shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, key)) {
650
+ const exposeRuntimeSecret = key === "TREESEED_TURNSTILE_SECRET_KEY" || shouldExposeManagedHostRuntimeSecret(input.context.deployConfig, key);
651
+ if (typeof value === "string" && value.length > 0 && exposeRuntimeSecret) {
616
652
  secrets[key] = value;
617
653
  secretNames.add(key);
618
654
  }
@@ -735,6 +771,7 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
735
771
  const queues = dryRun ? [] : listQueues(input.context.tenantRoot, env);
736
772
  const buckets = dryRun ? [] : listR2Buckets(input.context.tenantRoot, env);
737
773
  const pagesProjects = dryRun ? [] : listPagesProjects(input.context.tenantRoot, env);
774
+ const turnstileWidgets = dryRun ? [] : listTurnstileWidgets(input.context.tenantRoot, env);
738
775
  const runStep = (label, fn) => {
739
776
  try {
740
777
  return fn();
@@ -945,11 +982,59 @@ function reconcileCloudflareTarget(input, { dryRun = false } = {}) {
945
982
  }
946
983
  current.url = `https://${current.projectName}.pages.dev`;
947
984
  };
985
+ const ensureTurnstileWidget = () => {
986
+ if (deployConfig.turnstile?.enabled !== true) {
987
+ return;
988
+ }
989
+ const current = state.turnstileWidgets?.formGuard;
990
+ if (!current?.name) {
991
+ return;
992
+ }
993
+ const pagesHost = state.pages?.url ? new URL(state.pages.url).hostname : null;
994
+ const desiredDomains = normalizeTurnstileDomains([
995
+ ...Array.isArray(current.domains) ? current.domains : [],
996
+ pagesHost
997
+ ]);
998
+ current.domains = desiredDomains;
999
+ current.mode = "managed";
1000
+ current.managed = true;
1001
+ const existing = findTurnstileWidget(turnstileWidgets, current, current.name);
1002
+ if (dryRun) {
1003
+ current.sitekey = current.sitekey ?? `dryrun-${current.name}-sitekey`;
1004
+ current.secret = current.secret ?? `dryrun-${current.name}-secret`;
1005
+ current.lastSyncedAt = nowIso();
1006
+ return;
1007
+ }
1008
+ if (existing?.sitekey) {
1009
+ const needsUpdate = existing.name !== current.name || existing.mode !== "managed" || !turnstileDomainsEqual(existing.domains, desiredDomains);
1010
+ const updated = needsUpdate ? updateTurnstileWidget(env, String(existing.sitekey), {
1011
+ name: current.name,
1012
+ domains: desiredDomains,
1013
+ mode: "managed"
1014
+ }) : existing;
1015
+ current.sitekey = String(updated?.sitekey ?? existing.sitekey);
1016
+ current.secret = String(updated?.secret ?? current.secret ?? "");
1017
+ current.lastSyncedAt = nowIso();
1018
+ return;
1019
+ }
1020
+ const created = createTurnstileWidget(env, {
1021
+ name: current.name,
1022
+ domains: desiredDomains,
1023
+ mode: "managed"
1024
+ });
1025
+ if (!created?.sitekey || !created?.secret) {
1026
+ throw new Error(`Unable to resolve created Turnstile widget keys for ${current.name}.`);
1027
+ }
1028
+ current.sitekey = String(created.sitekey);
1029
+ current.secret = String(created.secret);
1030
+ current.lastSyncedAt = nowIso();
1031
+ };
948
1032
  runStep("kv-form-guard", () => ensureKv("FORM_GUARD_KV"));
949
1033
  runStep("d1", ensureD1);
950
1034
  runStep("queue", ensureQueue);
951
1035
  runStep("r2", ensureR2Bucket);
952
1036
  runStep("pages", ensurePagesProject);
1037
+ runStep("turnstile-widget", ensureTurnstileWidget);
953
1038
  runStep("web-cache", () => reconcileCloudflareWebCacheRules(input.context.tenantRoot, deployConfig, state, target, { dryRun, env }));
954
1039
  state.readiness.configured = true;
955
1040
  state.readiness.provisioned = hasProvisionedCloudflareResources(state);
@@ -989,7 +1074,7 @@ function syncCloudflareSecretsForTarget(input, { dryRun = false } = {}) {
989
1074
  }
990
1075
  function observeCloudflareUnit(input) {
991
1076
  const snapshot = cloudflareObservationSnapshot(input);
992
- const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects } = snapshot;
1077
+ const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets } = snapshot;
993
1078
  switch (input.unit.unitType) {
994
1079
  case "queue": {
995
1080
  const liveQueue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
@@ -1042,6 +1127,17 @@ function observeCloudflareUnit(input) {
1042
1127
  warnings: []
1043
1128
  };
1044
1129
  }
1130
+ case "turnstile-widget": {
1131
+ const current = state.turnstileWidgets?.formGuard ?? {};
1132
+ const liveWidget = findTurnstileWidget(turnstileWidgets, current, input.unit.spec.name);
1133
+ return {
1134
+ exists: Boolean(liveWidget?.sitekey || current?.sitekey),
1135
+ status: liveWidget?.sitekey ? "ready" : "pending",
1136
+ live: { ...current, ...liveWidget ?? {} },
1137
+ locators: { sitekey: String(liveWidget?.sitekey ?? current?.sitekey ?? "") || null },
1138
+ warnings: []
1139
+ };
1140
+ }
1045
1141
  case "pages-project": {
1046
1142
  const liveProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
1047
1143
  return {
@@ -1081,7 +1177,7 @@ function verifyCloudflareUnitOnce(input, postconditions) {
1081
1177
  ]);
1082
1178
  }
1083
1179
  const snapshot = cloudflareObservationSnapshot(input, true);
1084
- const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, env } = snapshot;
1180
+ const { state, kvNamespaces, d1Databases, queues, buckets, pagesProjects, turnstileWidgets, env } = snapshot;
1085
1181
  switch (input.unit.unitType) {
1086
1182
  case "queue": {
1087
1183
  const queue = state.queues?.agentWork;
@@ -1149,6 +1245,49 @@ function verifyCloudflareUnitOnce(input, postconditions) {
1149
1245
  })
1150
1246
  ]);
1151
1247
  }
1248
+ case "turnstile-widget": {
1249
+ const current = state.turnstileWidgets?.formGuard ?? {};
1250
+ const cachedLive = findTurnstileWidget(turnstileWidgets, current, input.unit.spec.name);
1251
+ const refreshedListedLive = findTurnstileWidget(
1252
+ listTurnstileWidgets(input.context.tenantRoot, env),
1253
+ current,
1254
+ input.unit.spec.name
1255
+ );
1256
+ const refreshedLive = current.sitekey ? getTurnstileWidget(env, String(current.sitekey)) : null;
1257
+ const live = mergeTurnstileWidget(
1258
+ cachedLive,
1259
+ refreshedListedLive,
1260
+ refreshedLive
1261
+ );
1262
+ const pagesHost = state.pages?.url ? new URL(state.pages.url).hostname : null;
1263
+ const desiredDomains = normalizeTurnstileDomains([
1264
+ ...Array.isArray(input.unit.spec.domains) ? input.unit.spec.domains : [],
1265
+ ...Array.isArray(current.domains) ? current.domains : [],
1266
+ pagesHost
1267
+ ]);
1268
+ return summarizeVerification(input.unit.unitId, [
1269
+ verificationCheck("turnstile.exists", "Turnstile widget exists by name and sitekey", "api", {
1270
+ exists: Boolean(live?.sitekey),
1271
+ expected: input.unit.spec.name ?? null,
1272
+ observed: live ? { name: live.name, sitekey: live.sitekey } : null,
1273
+ issues: live?.sitekey ? [] : [`Cloudflare Turnstile widget ${String(input.unit.spec.name ?? "(unset)")} was not found after reconcile.`]
1274
+ }),
1275
+ verificationCheck("turnstile.mode", "Turnstile widget mode is managed", "api", {
1276
+ exists: Boolean(live?.sitekey),
1277
+ configured: live?.mode === "managed",
1278
+ expected: "managed",
1279
+ observed: live?.mode ?? null,
1280
+ issues: live?.mode === "managed" ? [] : ["Turnstile widget mode does not match managed."]
1281
+ }),
1282
+ verificationCheck("turnstile.domains", "Turnstile widget domains match desired config", "api", {
1283
+ exists: Boolean(live?.sitekey),
1284
+ configured: turnstileDomainsEqual(live?.domains, desiredDomains),
1285
+ expected: desiredDomains,
1286
+ observed: normalizeTurnstileDomains(live?.domains),
1287
+ issues: turnstileDomainsEqual(live?.domains, desiredDomains) ? [] : ["Turnstile widget domains do not match desired config."]
1288
+ })
1289
+ ]);
1290
+ }
1152
1291
  case "content-store": {
1153
1292
  const bucketName = state.content?.bucketName;
1154
1293
  const live = buckets.find((entry) => entry?.name === bucketName);
@@ -1344,6 +1483,12 @@ function buildCloudflareAdapter(unitType) {
1344
1483
  { key: "kv.exists", description: "KV namespace exists by title and id" },
1345
1484
  { key: "kv.binding", description: "KV binding matches desired config" }
1346
1485
  ];
1486
+ case "turnstile-widget":
1487
+ return [
1488
+ { key: "turnstile.exists", description: "Turnstile widget exists by name and sitekey" },
1489
+ { key: "turnstile.mode", description: "Turnstile widget mode is managed" },
1490
+ { key: "turnstile.domains", description: "Turnstile widget domains match desired config" }
1491
+ ];
1347
1492
  case "content-store":
1348
1493
  return [
1349
1494
  { key: "r2.exists", description: "R2 bucket exists by name" },
@@ -1621,7 +1766,7 @@ async function syncRailwayEnvironmentForScope(input, { dryRun = false } = {}) {
1621
1766
  preferCli: entry.configuredService.key === "marketOperationsRunner",
1622
1767
  env: topology.env
1623
1768
  });
1624
- if (!volume.instance?.serviceId) {
1769
+ if (volume.instance?.serviceId !== entry.service.id || volume.instance?.environmentId !== entry.environment.id || volume.instance?.mountPath !== entry.configuredService.volumeMountPath) {
1625
1770
  ensureRailwayProjectContext(entry.configuredService, { env: topology.env, capture: true });
1626
1771
  let attachMessage = "";
1627
1772
  let attached = false;
@@ -2258,25 +2403,9 @@ async function verifyRailwayUnit(input) {
2258
2403
  const volumes = entry.project?.id ? await listRailwayVolumes({ projectId: entry.project.id, env: topology.env }) : [];
2259
2404
  const expectedServiceId = entry.service?.id ?? null;
2260
2405
  const expectedEnvironmentId = entry.environment?.id ?? null;
2261
- let mountedVolume = volumes.find((volume) => volume.instances.some(
2406
+ const mountedVolume = volumes.find((volume) => volume.instances.some(
2262
2407
  (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
2263
2408
  )) ?? null;
2264
- let mountedVolumeSource = "api";
2265
- if (!mountedVolume && serviceKey === "marketOperationsRunner" && expectedServiceId && expectedEnvironmentId) {
2266
- ensureRailwayProjectContext(service, { env: topology.env, capture: true });
2267
- const cliVolumes = listRailwayServiceVolumesWithCli({
2268
- cwd: service.rootDir,
2269
- serviceId: expectedServiceId,
2270
- environmentId: expectedEnvironmentId,
2271
- name: deriveRailwayMarketOperationsRunnerVolumeName(entry.service?.name ?? service.serviceName ?? service.key, entry.environment?.name ?? service.railwayEnvironment),
2272
- mountPath: service.volumeMountPath,
2273
- env: topology.env
2274
- });
2275
- mountedVolume = cliVolumes.find((volume) => volume.instances.some(
2276
- (instance) => instance.serviceId === expectedServiceId && instance.environmentId === expectedEnvironmentId && instance.mountPath === service.volumeMountPath
2277
- )) ?? null;
2278
- mountedVolumeSource = mountedVolume ? "cli" : "api";
2279
- }
2280
2409
  checks.push(verificationCheck("railway.volume:data", "Railway service has persistent data volume mounted", "api", {
2281
2410
  exists: Boolean(mountedVolume),
2282
2411
  configured: Boolean(mountedVolume),
@@ -2284,7 +2413,7 @@ async function verifyRailwayUnit(input) {
2284
2413
  observed: mountedVolume ? {
2285
2414
  name: mountedVolume.name,
2286
2415
  mountPath: service.volumeMountPath,
2287
- source: mountedVolumeSource
2416
+ source: "api"
2288
2417
  } : null,
2289
2418
  issues: mountedVolume ? [] : [`Railway service ${service.serviceName ?? service.key} is missing a persistent volume mounted at ${service.volumeMountPath}.`]
2290
2419
  }));
@@ -2494,6 +2623,7 @@ function createCloudflareReconcileAdapters() {
2494
2623
  buildCloudflareAdapter("database"),
2495
2624
  buildCloudflareAdapter("content-store"),
2496
2625
  buildCloudflareAdapter("kv-form-guard"),
2626
+ buildCloudflareAdapter("turnstile-widget"),
2497
2627
  buildCloudflareAdapter("pages-project"),
2498
2628
  buildCloudflareAdapter("edge-worker"),
2499
2629
  buildCustomDomainAdapter("custom-domain:web", "cloudflare"),
@@ -3,7 +3,7 @@ export type TreeseedReconcileProviderId = string;
3
3
  export type TreeseedReconcileActionKind = 'noop' | 'create' | 'update' | 'reuse' | 'drift_correct' | 'destroy';
4
4
  export type TreeseedReconcileStatusKind = 'pending' | 'ready' | 'drifted' | 'error';
5
5
  export type TreeseedReconcileVerificationSource = 'cli' | 'api' | 'sdk' | 'derived';
6
- export type TreeseedReconcileUnitType = 'web-ui' | 'api-runtime' | 'market-operations-runner-runtime' | 'workday-manager-runtime' | 'worker-runner-runtime' | 'edge-worker' | 'content-store' | 'queue' | 'database' | 'kv-form-guard' | 'pages-project' | 'custom-domain:web' | 'custom-domain:api' | 'dns-record' | 'railway-service:api' | 'railway-service:market-operations-runner' | 'railway-service:workday-manager' | 'railway-service:worker-runner';
6
+ export type TreeseedReconcileUnitType = 'web-ui' | 'api-runtime' | 'market-operations-runner-runtime' | 'workday-manager-runtime' | 'worker-runner-runtime' | 'edge-worker' | 'content-store' | 'queue' | 'database' | 'kv-form-guard' | 'turnstile-widget' | 'pages-project' | 'custom-domain:web' | 'custom-domain:api' | 'dns-record' | 'railway-service:api' | 'railway-service:market-operations-runner' | 'railway-service:workday-manager' | 'railway-service:worker-runner';
7
7
  export type TreeseedReconcileTarget = {
8
8
  kind: 'persistent';
9
9
  scope: 'local' | 'staging' | 'prod';
@@ -76,6 +76,22 @@ function deriveTreeseedDesiredUnits({
76
76
  secrets: {},
77
77
  metadata: { bootstrapSystem: "web" }
78
78
  });
79
+ const turnstileWidgetId = legacyState.turnstileWidgets?.formGuard?.name && deployConfig.turnstile?.enabled === true ? add({
80
+ unitId: createTreeseedReconcileUnitId("turnstile-widget", legacyState.turnstileWidgets.formGuard.name),
81
+ unitType: "turnstile-widget",
82
+ provider: "cloudflare",
83
+ identity,
84
+ target,
85
+ logicalName: legacyState.turnstileWidgets.formGuard.name,
86
+ dependencies: [],
87
+ spec: {
88
+ name: legacyState.turnstileWidgets.formGuard.name,
89
+ domains: legacyState.turnstileWidgets.formGuard.domains ?? [],
90
+ mode: "managed"
91
+ },
92
+ secrets: {},
93
+ metadata: { bootstrapSystem: "web" }
94
+ }) : null;
79
95
  const contentStoreId = add({
80
96
  unitId: createTreeseedReconcileUnitId("content-store", legacyState.content.bucketName ?? deployConfig.slug),
81
97
  unitType: "content-store",
@@ -118,7 +134,7 @@ function deriveTreeseedDesiredUnits({
118
134
  identity,
119
135
  target,
120
136
  logicalName: legacyState.workerName,
121
- dependencies: [queueId, databaseId, formGuardKvId, contentStoreId, pagesProjectId],
137
+ dependencies: [queueId, databaseId, formGuardKvId, ...turnstileWidgetId ? [turnstileWidgetId] : [], contentStoreId, pagesProjectId],
122
138
  spec: {
123
139
  workerName: legacyState.workerName
124
140
  },
@@ -9,6 +9,7 @@ const TRESEED_RECONCILE_UNIT_TYPES = [
9
9
  "queue",
10
10
  "database",
11
11
  "kv-form-guard",
12
+ "turnstile-widget",
12
13
  "pages-project",
13
14
  "custom-domain:web",
14
15
  "custom-domain:api",
@@ -19,7 +19,7 @@ export declare const PROJECT_WEB_DEPLOYMENT_ACTIONS: readonly ["deploy_web", "pu
19
19
  export declare const PROJECT_DEPLOYMENT_ENVIRONMENTS: readonly ["staging", "prod"];
20
20
  export declare const PROJECT_DEPLOYMENT_STATUSES: readonly ["pending", "queued", "claimed", "dispatching", "running", "monitoring", "succeeded", "failed", "cancelled", "timed_out"];
21
21
  export declare const PROJECT_INFRA_RESOURCE_PROVIDERS: readonly ["cloudflare", "railway", "github", "market"];
22
- export declare const PROJECT_INFRA_RESOURCE_KINDS: readonly ["pages", "worker", "r2", "d1", "queue", "dlq", "railway_project", "railway_service", "railway_schedule"];
22
+ export declare const PROJECT_INFRA_RESOURCE_KINDS: readonly ["pages", "worker", "kv", "turnstile-widget", "r2", "d1", "queue", "dlq", "railway_project", "railway_service", "railway_schedule"];
23
23
  export declare const AGENT_POOL_STATUSES: readonly ["pending", "active", "degraded", "offline"];
24
24
  export type SdkBuiltinModelName = (typeof SDK_MODEL_NAMES)[number];
25
25
  export type SdkModelName = SdkBuiltinModelName | (string & {});
package/dist/sdk-types.js CHANGED
@@ -48,6 +48,8 @@ const PROJECT_INFRA_RESOURCE_PROVIDERS = ["cloudflare", "railway", "github", "ma
48
48
  const PROJECT_INFRA_RESOURCE_KINDS = [
49
49
  "pages",
50
50
  "worker",
51
+ "kv",
52
+ "turnstile-widget",
51
53
  "r2",
52
54
  "d1",
53
55
  "queue",
@@ -1243,6 +1243,7 @@ export declare function workflowDestroy(helpers: WorkflowOperationHelpers, input
1243
1243
  siteUrl: any;
1244
1244
  accountId: any;
1245
1245
  pages: any;
1246
+ turnstileWidget: any;
1246
1247
  formGuardKv: any;
1247
1248
  sessionKv: any;
1248
1249
  siteDataDb: any;
@@ -1254,6 +1255,7 @@ export declare function workflowDestroy(helpers: WorkflowOperationHelpers, input
1254
1255
  queue: any;
1255
1256
  dlq: any;
1256
1257
  database: any;
1258
+ turnstileWidget: any;
1257
1259
  formGuardKv: any;
1258
1260
  railwayProject: any;
1259
1261
  webDomain: any;
@@ -986,8 +986,7 @@ CREATE TABLE IF NOT EXISTS "projects" (
986
986
  "description" text,
987
987
  "metadata_json" text,
988
988
  "created_at" text NOT NULL,
989
- "updated_at" text NOT NULL,
990
- CONSTRAINT "projects_slug_unique" UNIQUE("slug")
989
+ "updated_at" text NOT NULL
991
990
  );
992
991
 
993
992
  CREATE TABLE IF NOT EXISTS "provider_credential_sessions" (
@@ -2839,8 +2838,8 @@ CREATE INDEX IF NOT EXISTS "idx_capacity_routing_decisions_project_workday" ON "
2839
2838
  CREATE UNIQUE INDEX IF NOT EXISTS "idx_catalog_artifact_versions_item_version" ON "catalog_artifact_versions" USING btree ("item_id","version");
2840
2839
  CREATE INDEX IF NOT EXISTS "idx_catalog_artifact_versions_team_kind" ON "catalog_artifact_versions" USING btree ("team_id","kind","published_at");
2841
2840
  CREATE UNIQUE INDEX IF NOT EXISTS "idx_catalog_item_collaborators_subject_role" ON "catalog_item_collaborators" USING btree ("item_id","subject_type","subject_id","role");
2842
- CREATE UNIQUE INDEX IF NOT EXISTS "idx_catalog_items_kind_slug" ON "catalog_items" USING btree ("kind","slug");
2843
2841
  CREATE INDEX IF NOT EXISTS "idx_catalog_items_team_kind" ON "catalog_items" USING btree ("team_id","kind","updated_at");
2842
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_catalog_items_team_kind_slug" ON "catalog_items" USING btree ("team_id","kind","slug");
2844
2843
  CREATE INDEX IF NOT EXISTS "idx_catalog_items_visibility_listing" ON "catalog_items" USING btree ("visibility","listing_enabled","updated_at");
2845
2844
  CREATE UNIQUE INDEX IF NOT EXISTS "idx_credit_conversion_profiles_profile_key" ON "credit_conversion_profiles" USING btree ("task_signature","execution_profile_id","execution_provider_kind","native_unit");
2846
2845
  CREATE INDEX IF NOT EXISTS "idx_credit_conversion_profiles_kind_unit" ON "credit_conversion_profiles" USING btree ("execution_provider_kind","native_unit","updated_at");
@@ -2884,6 +2883,7 @@ CREATE INDEX IF NOT EXISTS "idx_project_summary_snapshots_team_generated" ON "pr
2884
2883
  CREATE INDEX IF NOT EXISTS "idx_project_update_plans_hub" ON "project_update_plans" USING btree ("hub_id","created_at");
2885
2884
  CREATE INDEX IF NOT EXISTS "idx_project_workday_summaries_project_environment_created" ON "project_workday_summaries" USING btree ("project_id","environment","created_at");
2886
2885
  CREATE INDEX IF NOT EXISTS "idx_projects_team_id" ON "projects" USING btree ("team_id");
2886
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_projects_team_slug" ON "projects" USING btree ("team_id","slug");
2887
2887
  CREATE INDEX IF NOT EXISTS "idx_provider_credential_sessions_team_host" ON "provider_credential_sessions" USING btree ("team_id","host_kind","host_id","status");
2888
2888
  CREATE INDEX IF NOT EXISTS "idx_provider_credential_sessions_job" ON "provider_credential_sessions" USING btree ("job_id","status");
2889
2889
  CREATE UNIQUE INDEX IF NOT EXISTS "idx_remote_job_events_job_seq" ON "remote_job_events" USING btree ("job_id","seq");
@@ -0,0 +1,4 @@
1
+ ALTER TABLE "projects" DROP CONSTRAINT IF EXISTS "projects_slug_unique";
2
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_projects_team_slug" ON "projects" USING btree ("team_id","slug");
3
+ DROP INDEX IF EXISTS "idx_catalog_items_kind_slug";
4
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_catalog_items_team_kind_slug" ON "catalog_items" USING btree ("team_id","kind","slug");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/sdk",
3
- "version": "0.10.22",
3
+ "version": "0.10.23",
4
4
  "description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {