@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.
- package/dist/db/market-schema.js +3 -2
- package/dist/market-client.d.ts +4 -0
- package/dist/market-client.js +6 -0
- package/dist/operations/providers/default.js +26 -4
- package/dist/operations/repository-operations.js +6 -2
- package/dist/operations/services/config-runtime.d.ts +1 -1
- package/dist/operations/services/deploy.d.ts +18 -1
- package/dist/operations/services/deploy.js +176 -24
- package/dist/operations/services/github-automation.d.ts +10 -1
- package/dist/operations/services/github-automation.js +18 -4
- package/dist/operations/services/hosting-audit.d.ts +2 -1
- package/dist/operations/services/hosting-audit.js +12 -1
- package/dist/operations/services/hub-launch.d.ts +1 -0
- package/dist/operations/services/hub-launch.js +1 -0
- package/dist/operations/services/hub-provider-launch.d.ts +9 -0
- package/dist/operations/services/hub-provider-launch.js +140 -40
- package/dist/operations/services/managed-host-security.d.ts +1 -1
- package/dist/operations/services/managed-host-security.js +4 -1
- package/dist/operations/services/project-platform.js +16 -0
- package/dist/operations/services/railway-api.js +2 -1
- package/dist/operations/services/railway-deploy.d.ts +2 -1
- package/dist/operations/services/railway-deploy.js +15 -18
- package/dist/platform/environment.d.ts +1 -1
- package/dist/platform/environment.js +1 -1
- package/dist/reconcile/builtin-adapters.js +155 -25
- package/dist/reconcile/contracts.d.ts +1 -1
- package/dist/reconcile/desired-state.js +17 -1
- package/dist/reconcile/units.js +1 -0
- package/dist/sdk-types.d.ts +1 -1
- package/dist/sdk-types.js +2 -0
- package/dist/workflow/operations.d.ts +2 -0
- package/drizzle/market/0000_market_control_plane.sql +3 -3
- package/drizzle/market/0003_project_team_slug_unique.sql +4 -0
- 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
|
|
1882
|
-
(entry) => entry.
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
},
|
package/dist/reconcile/units.js
CHANGED
package/dist/sdk-types.d.ts
CHANGED
|
@@ -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
|
@@ -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");
|