@treeseed/sdk 0.13.0-rc.77 → 0.13.0-rc.80

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.
@@ -1,12 +1,45 @@
1
1
  import { z } from "zod";
2
2
  import { deploymentDigest } from "./canonical.js";
3
3
  const identifier = z.string().regex(/^[a-z][a-z0-9.-]{1,63}$/u);
4
+ const custodyIdentifier = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u);
4
5
  const digest = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
5
6
  const gitCommit = z.string().regex(/^[a-f0-9]{40}$/u);
6
7
  const timestamp = z.string().datetime();
8
+ const hostedEnvironmentSchema = z.enum(["staging", "production"]);
9
+ function hostedTopologyStateKey(input) {
10
+ const teamId = custodyIdentifier.parse(input.teamId), deploymentId = custodyIdentifier.parse(input.deploymentId), stackId = custodyIdentifier.parse(input.stackId);
11
+ const environment = hostedEnvironmentSchema.parse(input.environment);
12
+ return `teams/${teamId}/opentofu/v1/deployments/${deploymentId}/environments/${environment}/stacks/${stackId}/terraform.tfstate`;
13
+ }
14
+ const hostedStateBackendCoreSchema = z.object({
15
+ schemaVersion: z.literal("treeseed.hosted-state-backend/v1"),
16
+ type: z.literal("s3"),
17
+ teamId: custodyIdentifier,
18
+ deploymentId: custodyIdentifier,
19
+ environment: hostedEnvironmentSchema,
20
+ stackId: custodyIdentifier,
21
+ connectionRef: identifier,
22
+ bucket: z.string().trim().min(3).max(255),
23
+ key: z.string().trim().min(1).max(1024),
24
+ region: z.string().trim().min(1).max(128),
25
+ endpoint: z.string().url().optional(),
26
+ usePathStyle: z.boolean().optional(),
27
+ encryptionKeyRef: custodyIdentifier
28
+ }).strict();
29
+ const hostedStateBackendSchema = hostedStateBackendCoreSchema.extend({ bindingDigest: digest }).strict().superRefine((backend, context) => {
30
+ const { bindingDigest: _bindingDigest, ...core } = backend;
31
+ if (backend.key !== hostedTopologyStateKey(backend)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["key"], message: "Hosted state backend key must be the canonical team deployment stack key." });
32
+ if (deploymentDigest(core) !== backend.bindingDigest) context.addIssue({ code: z.ZodIssueCode.custom, path: ["bindingDigest"], message: "Hosted state backend digest must bind its complete custody and storage identity." });
33
+ });
34
+ function bindHostedStateBackend(input) {
35
+ const core = hostedStateBackendCoreSchema.parse(input);
36
+ if (core.key !== hostedTopologyStateKey(core)) throw new Error("Hosted state backend key must be the canonical team deployment stack key.");
37
+ return hostedStateBackendSchema.parse({ ...core, bindingDigest: deploymentDigest(core) });
38
+ }
7
39
  const hostedProviderSchema = z.enum(["cloudflare", "railway"]);
8
40
  const hostedResourceKindSchema = z.enum([
9
41
  "admin-application",
42
+ "pages-application",
10
43
  "dns-record",
11
44
  "tls-policy",
12
45
  "api-proxy",
@@ -15,14 +48,21 @@ const hostedResourceKindSchema = z.enum([
15
48
  "operations-runner",
16
49
  "treedx-service"
17
50
  ]);
51
+ const hostedArchiveArtifactSchema = z.object({ kind: z.literal("archive"), format: z.literal("tar+gzip"), digest, source: z.string().url() }).strict();
52
+ const hostedFileArtifactSchema = z.object({ kind: z.literal("file"), mediaType: z.string().trim().min(1).max(255), digest, source: z.string().url() }).strict();
53
+ const hostedOciArtifactSchema = z.object({ kind: z.literal("oci-image"), digest, identity: z.string().regex(/^[a-z0-9][a-z0-9._/-]*@sha256:[a-f0-9]{64}$/u) }).strict().superRefine((artifact, context) => {
54
+ if (!artifact.identity.endsWith(`@${artifact.digest}`)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["identity"], message: "Hosted OCI identity must bind the declared digest." });
55
+ });
56
+ const hostedArtifactSchema = z.union([hostedArchiveArtifactSchema, hostedFileArtifactSchema, hostedOciArtifactSchema]);
18
57
  const parameterSchema = z.union([
19
58
  z.object({ input: identifier }).strict(),
20
59
  z.object({ artifact: identifier }).strict(),
21
60
  z.object({ resourceOutput: z.object({ resourceId: identifier, output: identifier }).strict() }).strict(),
22
61
  z.object({ literal: z.union([z.string().max(4096), z.number().finite(), z.boolean()]) }).strict()
23
62
  ]);
63
+ const parameterName = z.union([identifier, z.string().regex(/^variable\.[A-Z][A-Z0-9_]{1,127}$/u)]);
24
64
  const resourceProviderKinds = {
25
- cloudflare: /* @__PURE__ */ new Set(["admin-application", "dns-record", "tls-policy", "api-proxy"]),
65
+ cloudflare: /* @__PURE__ */ new Set(["admin-application", "pages-application", "dns-record", "tls-policy", "api-proxy"]),
26
66
  railway: /* @__PURE__ */ new Set(["control-plane-api", "postgresql", "operations-runner", "treedx-service"])
27
67
  };
28
68
  const sensitiveKey = /(?:credential|password|private.?key|registration.?code|secret|token)/iu;
@@ -41,23 +81,31 @@ const hostedResourceDeclarationSchema = z.object({
41
81
  provider: hostedProviderSchema,
42
82
  kind: hostedResourceKindSchema,
43
83
  dependsOn: z.array(identifier).default([]),
44
- parameters: z.record(identifier, parameterSchema).default({}),
84
+ parameters: z.record(parameterName, parameterSchema).default({}),
45
85
  adoption: z.object({ mode: z.literal("adopt-or-create"), externalIdInput: identifier.optional(), replacement: z.literal("forbidden") }).strict()
46
86
  }).strict();
47
87
  const hostedTopologyDeclarationSchema = z.object({
48
88
  schemaVersion: z.literal("treeseed.hosted-topology/v1"),
49
89
  id: identifier,
50
- environment: z.enum(["staging", "production"]),
90
+ teamId: custodyIdentifier,
91
+ deploymentId: custodyIdentifier,
92
+ stackId: custodyIdentifier,
93
+ environment: hostedEnvironmentSchema,
51
94
  mutation: z.literal("approval-required"),
52
95
  platform: z.object({ repository: z.literal("treeseed-ai/platform"), commit: gitCommit }).strict(),
96
+ stateBackend: z.object({ connectionRef: identifier }).strict(),
53
97
  providerConnections: z.record(hostedProviderSchema, z.object({ connectionRef: identifier }).strict()),
54
- artifacts: z.record(identifier, z.object({ digest, source: z.string().url() }).strict()),
55
- resources: z.array(hostedResourceDeclarationSchema).min(1)
98
+ artifacts: z.record(identifier, hostedArtifactSchema),
99
+ resources: z.array(hostedResourceDeclarationSchema)
56
100
  }).strict().superRefine((declaration, context) => {
57
101
  const ids = declaration.resources.map(({ id }) => id);
58
102
  if (new Set(ids).size !== ids.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources"], message: "Hosted topology resource identities must be unique." });
59
103
  for (const [index, resource] of declaration.resources.entries()) {
60
104
  if (!resourceProviderKinds[resource.provider].has(resource.kind)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources", index, "kind"], message: `${resource.kind} is not owned by ${resource.provider}.` });
105
+ if (resource.kind === "pages-application") {
106
+ for (const key of ["artifact", "artifact-format", "name", "production-branch", "destination-dir"])
107
+ if (!resource.parameters[key]) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources", index, "parameters", key], message: `Cloudflare Pages applications require ${key}.` });
108
+ }
61
109
  for (const dependency of resource.dependsOn) if (!ids.includes(dependency)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources", index, "dependsOn"], message: `Unknown hosted resource dependency ${dependency}.` });
62
110
  for (const key of Object.keys(resource.parameters)) if (sensitiveKey.test(key)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources", index, "parameters", key], message: "Hosted topology parameters cannot carry credential-like values." });
63
111
  for (const [key, parameter] of Object.entries(resource.parameters)) if ("literal" in parameter && typeof parameter.literal === "string" && personalPath.test(parameter.literal)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["resources", index, "parameters", key], message: "Hosted topology parameters cannot contain personal filesystem paths." });
@@ -79,9 +127,13 @@ const hostedTopologyPlanShape = {
79
127
  planDigest: digest,
80
128
  declarationDigest: digest,
81
129
  topologyId: identifier,
82
- environment: z.enum(["staging", "production"]),
130
+ teamId: custodyIdentifier,
131
+ deploymentId: custodyIdentifier,
132
+ stackId: custodyIdentifier,
133
+ environment: hostedEnvironmentSchema,
83
134
  platformCommit: gitCommit,
84
- artifacts: z.record(identifier, z.object({ digest, source: z.string().url() }).strict()),
135
+ stateBackend: hostedStateBackendSchema.nullable(),
136
+ artifacts: z.record(identifier, hostedArtifactSchema),
85
137
  providerConnections: z.record(hostedProviderSchema, hostedConnectionSnapshotSchema),
86
138
  actions: z.array(z.object({
87
139
  resourceId: identifier,
@@ -93,7 +145,7 @@ const hostedTopologyPlanShape = {
93
145
  previousDigest: digest.nullable(),
94
146
  providerResourceId: z.string().min(1).max(512).nullable()
95
147
  }).strict()),
96
- blockers: z.array(z.object({ code: z.enum(["connection-unavailable", "dependency-cycle", "observation-unhealthy", "adoption-drift"]), resourceId: identifier.optional(), message: z.string().min(1) }).strict()),
148
+ blockers: z.array(z.object({ code: z.enum(["connection-unavailable", "state-backend-unavailable", "dependency-cycle", "observation-unhealthy", "adoption-drift"]), resourceId: identifier.optional(), message: z.string().min(1) }).strict()),
97
149
  approvalRequired: z.boolean(),
98
150
  executable: z.literal(false)
99
151
  };
@@ -107,10 +159,16 @@ function verifyPlanBinding(plan, context) {
107
159
  if (!plan.providerConnections[action.provider] && !plan.blockers.some((blocker) => blocker.code === "connection-unavailable"))
108
160
  context.addIssue({ code: z.ZodIssueCode.custom, path: ["providerConnections", action.provider], message: `Hosted plan is missing the selected ${action.provider} connection snapshot.` });
109
161
  }
162
+ if (plan.stateBackend && [plan.teamId !== plan.stateBackend.teamId, plan.deploymentId !== plan.stateBackend.deploymentId, plan.stackId !== plan.stateBackend.stackId, plan.environment !== plan.stateBackend.environment].some(Boolean))
163
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["stateBackend"], message: "Hosted topology plan and state backend custody identities must match." });
110
164
  const core = {
111
165
  declarationDigest: plan.declarationDigest,
112
166
  topologyId: plan.topologyId,
167
+ teamId: plan.teamId,
168
+ deploymentId: plan.deploymentId,
169
+ stackId: plan.stackId,
113
170
  environment: plan.environment,
171
+ stateBackend: plan.stateBackend,
114
172
  platformCommit: plan.platformCommit,
115
173
  artifacts: plan.artifacts,
116
174
  providerConnections: plan.providerConnections,
@@ -125,7 +183,11 @@ const hostedTopologyPlanSchema = z.object(hostedTopologyPlanShape).strict().supe
125
183
  const hostedTopologyApprovalSchema = z.object({
126
184
  schemaVersion: z.literal("treeseed.hosted-topology-approval/v1"),
127
185
  planDigest: digest,
128
- environment: z.enum(["staging", "production"]),
186
+ teamId: custodyIdentifier,
187
+ deploymentId: custodyIdentifier,
188
+ stackId: custodyIdentifier,
189
+ environment: hostedEnvironmentSchema,
190
+ backendBindingDigest: digest,
129
191
  decision: z.literal("approved"),
130
192
  approvedBy: z.string().min(1).max(256),
131
193
  approvedAt: timestamp
@@ -141,7 +203,11 @@ const hostedTopologyReceiptSchema = z.object({
141
203
  planDigest: digest,
142
204
  declarationDigest: digest,
143
205
  topologyId: identifier,
144
- environment: z.enum(["staging", "production"]),
206
+ teamId: custodyIdentifier,
207
+ deploymentId: custodyIdentifier,
208
+ stackId: custodyIdentifier,
209
+ environment: hostedEnvironmentSchema,
210
+ backendBindingDigest: digest,
145
211
  platformCommit: gitCommit,
146
212
  resources: z.array(hostedResourceObservationSchema),
147
213
  previousResources: z.array(hostedResourceObservationSchema),
@@ -152,7 +218,11 @@ const hostedTopologyRollbackSchema = z.object({
152
218
  schemaVersion: z.literal("treeseed.hosted-topology-rollback/v1"),
153
219
  rollbackId: z.string().regex(/^topology-rollback-[a-f0-9]{16}$/u),
154
220
  sourceReceiptId: z.string().regex(/^topology-receipt-[a-f0-9]{16}$/u),
155
- environment: z.enum(["staging", "production"]),
221
+ teamId: custodyIdentifier,
222
+ deploymentId: custodyIdentifier,
223
+ stackId: custodyIdentifier,
224
+ environment: hostedEnvironmentSchema,
225
+ backendBindingDigest: digest,
156
226
  operations: z.array(z.object({
157
227
  resourceId: identifier,
158
228
  action: z.enum(["restore", "delete-created", "noop"]),
@@ -164,7 +234,11 @@ const hostedTopologyRollbackSchema = z.object({
164
234
  const hostedTopologyRollbackApprovalSchema = z.object({
165
235
  schemaVersion: z.literal("treeseed.hosted-topology-rollback-approval/v1"),
166
236
  rollbackDigest: digest,
167
- environment: z.enum(["staging", "production"]),
237
+ teamId: custodyIdentifier,
238
+ deploymentId: custodyIdentifier,
239
+ stackId: custodyIdentifier,
240
+ environment: hostedEnvironmentSchema,
241
+ backendBindingDigest: digest,
168
242
  decision: z.literal("approved"),
169
243
  approvedBy: z.string().min(1).max(256),
170
244
  approvedAt: timestamp
@@ -174,7 +248,11 @@ const hostedTopologyRollbackExecutionSchema = z.object({
174
248
  rollback: hostedTopologyRollbackSchema,
175
249
  sourceReceiptId: z.string().regex(/^topology-receipt-[a-f0-9]{16}$/u),
176
250
  topologyId: identifier,
177
- environment: z.enum(["staging", "production"]),
251
+ teamId: custodyIdentifier,
252
+ deploymentId: custodyIdentifier,
253
+ stackId: custodyIdentifier,
254
+ environment: hostedEnvironmentSchema,
255
+ backendBindingDigest: digest,
178
256
  sourcePlanDigest: digest,
179
257
  targetPlanDigest: digest,
180
258
  executionDigest: digest
@@ -183,7 +261,11 @@ const hostedTopologyRollbackExecutionSchema = z.object({
183
261
  rollback: execution.rollback,
184
262
  sourceReceiptId: execution.sourceReceiptId,
185
263
  topologyId: execution.topologyId,
264
+ teamId: execution.teamId,
265
+ deploymentId: execution.deploymentId,
266
+ stackId: execution.stackId,
186
267
  environment: execution.environment,
268
+ backendBindingDigest: execution.backendBindingDigest,
187
269
  sourcePlanDigest: execution.sourcePlanDigest,
188
270
  targetPlanDigest: execution.targetPlanDigest
189
271
  };
@@ -192,7 +274,11 @@ const hostedTopologyRollbackExecutionSchema = z.object({
192
274
  const hostedTopologyRollbackExecutionApprovalSchema = z.object({
193
275
  schemaVersion: z.literal("treeseed.hosted-topology-rollback-execution-approval/v1"),
194
276
  executionDigest: digest,
195
- environment: z.enum(["staging", "production"]),
277
+ teamId: custodyIdentifier,
278
+ deploymentId: custodyIdentifier,
279
+ stackId: custodyIdentifier,
280
+ environment: hostedEnvironmentSchema,
281
+ backendBindingDigest: digest,
196
282
  decision: z.literal("approved"),
197
283
  approvedBy: z.string().min(1).max(256),
198
284
  approvedAt: timestamp
@@ -237,6 +323,14 @@ function planHostedTopology(input) {
237
323
  };
238
324
  const observations = observationMap(input.observations, normalizedDeclaration);
239
325
  const blockers = [];
326
+ let stateBackend = null;
327
+ if (!input.stateBackend) blockers.push({ code: "state-backend-unavailable", message: `State backend connection ${normalizedDeclaration.stateBackend.connectionRef} is unavailable.` });
328
+ else {
329
+ stateBackend = hostedStateBackendSchema.parse(input.stateBackend);
330
+ if (stateBackend.connectionRef !== normalizedDeclaration.stateBackend.connectionRef) blockers.push({ code: "state-backend-unavailable", message: `State backend connection ${normalizedDeclaration.stateBackend.connectionRef} is unavailable.` });
331
+ if (stateBackend.teamId !== normalizedDeclaration.teamId || stateBackend.deploymentId !== normalizedDeclaration.deploymentId || stateBackend.stackId !== normalizedDeclaration.stackId || stateBackend.environment !== normalizedDeclaration.environment)
332
+ throw new Error("Hosted state backend custody identity does not match the topology declaration.");
333
+ }
240
334
  const providerConnections = {};
241
335
  for (const [provider, binding] of Object.entries(normalizedDeclaration.providerConnections)) {
242
336
  const snapshot = input.connections[provider];
@@ -269,7 +363,11 @@ function planHostedTopology(input) {
269
363
  const core = {
270
364
  declarationDigest,
271
365
  topologyId: declaration.id,
366
+ teamId: declaration.teamId,
367
+ deploymentId: declaration.deploymentId,
368
+ stackId: declaration.stackId,
272
369
  environment: declaration.environment,
370
+ stateBackend,
273
371
  platformCommit: declaration.platform.commit,
274
372
  artifacts: normalizedDeclaration.artifacts,
275
373
  providerConnections,
@@ -282,9 +380,10 @@ function planHostedTopology(input) {
282
380
  function authorizeHostedTopologyPlan(planInput, approvalInput) {
283
381
  const plan = hostedTopologyPlanSchema.parse(planInput);
284
382
  if (plan.blockers.length) throw new Error("Hosted topology plan has unresolved blockers.");
383
+ if (!plan.stateBackend) throw new Error("Hosted topology plan has no state backend authority.");
285
384
  const approval = approvalInput ? hostedTopologyApprovalSchema.parse(approvalInput) : null;
286
385
  if (plan.approvalRequired && !approval) throw new Error("Hosted topology mutation requires environment approval.");
287
- if (approval && (approval.planDigest !== plan.planDigest || approval.environment !== plan.environment)) throw new Error("Hosted topology approval does not bind the exact plan and environment.");
386
+ if (approval && (approval.planDigest !== plan.planDigest || approval.teamId !== plan.teamId || approval.deploymentId !== plan.deploymentId || approval.stackId !== plan.stackId || approval.environment !== plan.environment || approval.backendBindingDigest !== plan.stateBackend.bindingDigest)) throw new Error("Hosted topology approval does not bind the exact plan custody and backend.");
288
387
  const { approvalRequired: _approvalRequired, ...approvedPlan } = plan;
289
388
  return authorizedHostedTopologyPlanSchema.parse({ ...approvedPlan, executable: true, approval });
290
389
  }
@@ -300,7 +399,23 @@ function verifyHostedTopologyReadback(input) {
300
399
  }
301
400
  const completedAt = timestamp.parse(input.completedAt);
302
401
  const receiptDigest = deploymentDigest({ planDigest: plan.planDigest, resources, completedAt });
303
- return hostedTopologyReceiptSchema.parse({ schemaVersion: "treeseed.hosted-topology-receipt/v1", receiptId: `topology-receipt-${receiptDigest.slice(7, 23)}`, planDigest: plan.planDigest, declarationDigest: plan.declarationDigest, topologyId: plan.topologyId, environment: plan.environment, platformCommit: plan.platformCommit, resources, previousResources: input.previousResources.map((item) => hostedResourceObservationSchema.parse(item)).sort((left, right) => left.resourceId.localeCompare(right.resourceId)), state: "known-good", completedAt });
402
+ return hostedTopologyReceiptSchema.parse({
403
+ schemaVersion: "treeseed.hosted-topology-receipt/v1",
404
+ receiptId: `topology-receipt-${receiptDigest.slice(7, 23)}`,
405
+ planDigest: plan.planDigest,
406
+ declarationDigest: plan.declarationDigest,
407
+ topologyId: plan.topologyId,
408
+ teamId: plan.teamId,
409
+ deploymentId: plan.deploymentId,
410
+ stackId: plan.stackId,
411
+ environment: plan.environment,
412
+ backendBindingDigest: plan.stateBackend.bindingDigest,
413
+ platformCommit: plan.platformCommit,
414
+ resources,
415
+ previousResources: input.previousResources.map((item) => hostedResourceObservationSchema.parse(item)).sort((left, right) => left.resourceId.localeCompare(right.resourceId)),
416
+ state: "known-good",
417
+ completedAt
418
+ });
304
419
  }
305
420
  function planHostedTopologyRollback(receiptInput) {
306
421
  const receipt = hostedTopologyReceiptSchema.parse(receiptInput);
@@ -309,12 +424,13 @@ function planHostedTopologyRollback(receiptInput) {
309
424
  const prior = previous.get(resource.resourceId);
310
425
  return { resourceId: resource.resourceId, action: !prior || prior.state === "missing" ? "delete-created" : prior.observedDigest === resource.observedDigest ? "noop" : "restore", providerResourceId: resource.providerResourceId, targetDigest: prior?.observedDigest ?? null };
311
426
  }).sort((left, right) => left.resourceId.localeCompare(right.resourceId));
312
- const rollbackDigest = deploymentDigest({ sourceReceiptId: receipt.receiptId, operations });
313
- return hostedTopologyRollbackSchema.parse({ schemaVersion: "treeseed.hosted-topology-rollback/v1", rollbackId: `topology-rollback-${rollbackDigest.slice(7, 23)}`, sourceReceiptId: receipt.receiptId, environment: receipt.environment, operations, rollbackDigest });
427
+ const custody = { teamId: receipt.teamId, deploymentId: receipt.deploymentId, stackId: receipt.stackId, environment: receipt.environment, backendBindingDigest: receipt.backendBindingDigest };
428
+ const rollbackDigest = deploymentDigest({ sourceReceiptId: receipt.receiptId, ...custody, operations });
429
+ return hostedTopologyRollbackSchema.parse({ schemaVersion: "treeseed.hosted-topology-rollback/v1", rollbackId: `topology-rollback-${rollbackDigest.slice(7, 23)}`, sourceReceiptId: receipt.receiptId, ...custody, operations, rollbackDigest });
314
430
  }
315
431
  function authorizeHostedTopologyRollback(rollbackInput, approvalInput) {
316
432
  const rollback = hostedTopologyRollbackSchema.parse(rollbackInput), approval = hostedTopologyRollbackApprovalSchema.parse(approvalInput);
317
- if (approval.rollbackDigest !== rollback.rollbackDigest || approval.environment !== rollback.environment) throw new Error("Hosted topology rollback approval does not bind the exact rollback and environment.");
433
+ if (approval.rollbackDigest !== rollback.rollbackDigest || approval.teamId !== rollback.teamId || approval.deploymentId !== rollback.deploymentId || approval.stackId !== rollback.stackId || approval.environment !== rollback.environment || approval.backendBindingDigest !== rollback.backendBindingDigest) throw new Error("Hosted topology rollback approval does not bind the exact rollback custody and backend.");
318
434
  return { rollback, approval };
319
435
  }
320
436
  function planHostedTopologyRollbackExecution(input) {
@@ -322,7 +438,12 @@ function planHostedTopologyRollbackExecution(input) {
322
438
  const sourcePlan = input.sourcePlan && typeof input.sourcePlan === "object" && "approval" in input.sourcePlan ? authorizedHostedTopologyPlanSchema.parse(input.sourcePlan) : hostedTopologyPlanSchema.parse(input.sourcePlan);
323
439
  const targetPlan = hostedTopologyPlanSchema.parse(input.targetPlan);
324
440
  if (sourceReceipt.receiptId !== rollback.sourceReceiptId || sourceReceipt.planDigest !== sourcePlan.planDigest) throw new Error("Hosted rollback execution source receipt is stale.");
325
- if ([sourceReceipt.environment, sourcePlan.environment, targetPlan.environment].some((environment) => environment !== rollback.environment)) throw new Error("Hosted rollback execution environment does not match its source and target plans.");
441
+ const custody = { teamId: rollback.teamId, deploymentId: rollback.deploymentId, stackId: rollback.stackId, environment: rollback.environment, backendBindingDigest: rollback.backendBindingDigest };
442
+ for (const candidate of [sourceReceipt, sourcePlan, targetPlan]) {
443
+ if (candidate.teamId !== custody.teamId || candidate.deploymentId !== custody.deploymentId || candidate.stackId !== custody.stackId || candidate.environment !== custody.environment) throw new Error("Hosted rollback execution custody identity does not match its source and target plans.");
444
+ const backendDigest = "backendBindingDigest" in candidate ? candidate.backendBindingDigest : candidate.stateBackend?.bindingDigest;
445
+ if (backendDigest !== custody.backendBindingDigest) throw new Error("Hosted rollback execution state backend changed.");
446
+ }
326
447
  if (sourceReceipt.topologyId !== sourcePlan.topologyId || sourcePlan.topologyId !== targetPlan.topologyId) throw new Error("Hosted rollback execution topology identity changed.");
327
448
  const sourceActions = new Map(sourcePlan.actions.map((action) => [action.resourceId, action]));
328
449
  const sourceResources = new Map(sourceReceipt.resources.map((resource) => [resource.resourceId, resource]));
@@ -342,7 +463,7 @@ function planHostedTopologyRollbackExecution(input) {
342
463
  rollback,
343
464
  sourceReceiptId: sourceReceipt.receiptId,
344
465
  topologyId: sourceReceipt.topologyId,
345
- environment: rollback.environment,
466
+ ...custody,
346
467
  sourcePlanDigest: sourcePlan.planDigest,
347
468
  targetPlanDigest: targetPlan.planDigest
348
469
  };
@@ -350,7 +471,7 @@ function planHostedTopologyRollbackExecution(input) {
350
471
  }
351
472
  function authorizeHostedTopologyRollbackExecution(executionInput, approvalInput) {
352
473
  const execution = hostedTopologyRollbackExecutionSchema.parse(executionInput), approval = hostedTopologyRollbackExecutionApprovalSchema.parse(approvalInput);
353
- if (approval.executionDigest !== execution.executionDigest || approval.environment !== execution.environment) throw new Error("Hosted rollback execution approval does not bind the exact source, target, and environment.");
474
+ if (approval.executionDigest !== execution.executionDigest || approval.teamId !== execution.teamId || approval.deploymentId !== execution.deploymentId || approval.stackId !== execution.stackId || approval.environment !== execution.environment || approval.backendBindingDigest !== execution.backendBindingDigest) throw new Error("Hosted rollback execution approval does not bind the exact source target custody and backend.");
354
475
  return { execution, approval };
355
476
  }
356
477
  export {
@@ -358,9 +479,13 @@ export {
358
479
  authorizeHostedTopologyRollback,
359
480
  authorizeHostedTopologyRollbackExecution,
360
481
  authorizedHostedTopologyPlanSchema,
482
+ bindHostedStateBackend,
483
+ hostedArtifactSchema,
361
484
  hostedProviderSchema,
485
+ hostedResourceDeclarationSchema,
362
486
  hostedResourceKindSchema,
363
487
  hostedResourceObservationSchema,
488
+ hostedStateBackendSchema,
364
489
  hostedTopologyApprovalSchema,
365
490
  hostedTopologyDeclarationSchema,
366
491
  hostedTopologyPlanSchema,
@@ -369,6 +494,7 @@ export {
369
494
  hostedTopologyRollbackExecutionApprovalSchema,
370
495
  hostedTopologyRollbackExecutionSchema,
371
496
  hostedTopologyRollbackSchema,
497
+ hostedTopologyStateKey,
372
498
  planHostedTopology,
373
499
  planHostedTopologyRollback,
374
500
  planHostedTopologyRollbackExecution,
@@ -4,3 +4,4 @@ export * from './catalog.js';
4
4
  export * from './topology.js';
5
5
  export * from './ai-mode.js';
6
6
  export * from './hosted-topology.js';
7
+ export * from './hosted-topology-template.js';
@@ -4,3 +4,4 @@ export * from "./catalog.js";
4
4
  export * from "./topology.js";
5
5
  export * from "./ai-mode.js";
6
6
  export * from "./hosted-topology.js";
7
+ export * from "./hosted-topology-template.js";
@@ -364,7 +364,7 @@ const commandTree = {
364
364
  { nodeType: "leaf", segment: "create", description: "Plan or reconcile a project, repository, template, library binding, and live inventory without writing application source into Platform Git.", kind: "mutation", arguments: [{ name: "slug", description: "Portable project slug.", required: true }], options: [planOption, { name: "--apply", description: "Apply the accepted creation plan.", type: "boolean" }, { name: "--yes", description: "Confirm authority-bearing project creation.", type: "boolean" }, { name: "--template", description: "Published template identity.", type: "string", required: true }, { name: "--json", description: "Emit the stable command-result envelope.", type: "boolean" }], authorization: { capability: "projects.create", confirmation: "authority" }, resultSchemaId: "treeseed.platform-project-create-result/v1", execution: local("local.platform.project.create") }
365
365
  ]),
366
366
  branch("topology", [
367
- { nodeType: "leaf", segment: "plan", description: "Plan Cloudflare and Railway reconciliation from a portable hosted-topology declaration.", kind: "read", arguments: [{ name: "file", description: "Hosted topology YAML or JSON declaration.", required: true }], resultSchemaId: "treeseed.hosted-topology-plan/v1", execution: local("local.platform.topology.plan") },
367
+ { nodeType: "leaf", segment: "plan", description: "Plan Cloudflare and Railway reconciliation from a portable hosted-topology declaration or template.", kind: "read", arguments: [{ name: "file", description: "Hosted topology YAML or JSON declaration or template.", required: true }], options: [{ name: "--artifacts", description: "Exact runtime artifact-input YAML or JSON required by a portable template.", type: "string" }], resultSchemaId: "treeseed.hosted-topology-plan/v1", execution: local("local.platform.topology.plan") },
368
368
  { nodeType: "leaf", segment: "apply", description: "Apply an exact reviewed hosted-topology plan through the operations runner.", kind: "mutation", arguments: [{ name: "plan", description: "Exact plan JSON file.", required: true }], options: [planOption, { name: "--approval", description: "Exact environment approval JSON file.", type: "string", required: true }, { name: "--yes", description: "Confirm the authority-bearing mutation.", type: "boolean" }], authorization: { capability: "infrastructure.write", confirmation: "authority" }, resultSchemaId: "treeseed.platform-operation/v1", execution: local("local.platform.topology.apply") },
369
369
  { nodeType: "leaf", segment: "status", description: "Read the latest hosted-topology operation and known-good receipt.", kind: "read", resultSchemaId: "treeseed.infrastructure.topology.status.output/v1", execution: local("local.platform.topology.status") },
370
370
  { nodeType: "leaf", segment: "rollback", description: "Restore exact prior hosted-topology state from a known-good receipt.", kind: "mutation", arguments: [{ name: "rollback", description: "Exact rollback JSON file.", required: true }], options: [planOption, { name: "--approval", description: "Exact rollback approval JSON file.", type: "string", required: true }, { name: "--yes", description: "Confirm the destructive rollback.", type: "boolean" }], authorization: { capability: "infrastructure.write", confirmation: "destructive" }, resultSchemaId: "treeseed.platform-operation/v1", execution: local("local.platform.topology.rollback") }
@@ -6,7 +6,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
6
6
  schemaVersion: "treeseed.hosted-topology/v1";
7
7
  resources: {
8
8
  id: string;
9
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
9
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
10
10
  provider: "cloudflare" | "railway";
11
11
  adoption: {
12
12
  mode: "adopt-or-create";
@@ -28,6 +28,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
28
28
  }> | undefined;
29
29
  }[];
30
30
  id: string;
31
+ teamId: string;
31
32
  platform: {
32
33
  repository: "treeseed-ai/platform";
33
34
  commit: string;
@@ -37,7 +38,23 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
37
38
  artifacts: Record<string, {
38
39
  digest: string;
39
40
  source: string;
41
+ kind: "archive";
42
+ format: "tar+gzip";
43
+ } | {
44
+ digest: string;
45
+ source: string;
46
+ kind: "file";
47
+ mediaType: string;
48
+ } | {
49
+ digest: string;
50
+ kind: "oci-image";
51
+ identity: string;
40
52
  }>;
53
+ deploymentId: string;
54
+ stackId: string;
55
+ stateBackend: {
56
+ connectionRef: string;
57
+ };
41
58
  providerConnections: Partial<Record<"cloudflare" | "railway", {
42
59
  connectionRef: string;
43
60
  }>>;
@@ -45,7 +62,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
45
62
  }, {
46
63
  schemaVersion: "treeseed.hosted-topology-plan/v1";
47
64
  blockers: {
48
- code: "connection-unavailable" | "dependency-cycle" | "observation-unhealthy" | "adoption-drift";
65
+ code: "connection-unavailable" | "state-backend-unavailable" | "dependency-cycle" | "observation-unhealthy" | "adoption-drift";
49
66
  message: string;
50
67
  resourceId?: string | undefined;
51
68
  }[];
@@ -53,12 +70,12 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
53
70
  actions: {
54
71
  action: "noop" | "create" | "adopt" | "update";
55
72
  resourceId: string;
56
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
73
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
57
74
  provider: "cloudflare" | "railway";
58
75
  providerResourceId: string | null;
59
76
  desiredResource: {
60
77
  id: string;
61
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
78
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
62
79
  provider: "cloudflare" | "railway";
63
80
  adoption: {
64
81
  mode: "adopt-or-create";
@@ -82,13 +99,43 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
82
99
  desiredDigest: string;
83
100
  previousDigest: string | null;
84
101
  }[];
102
+ teamId: string;
85
103
  environment: "production" | "staging";
86
104
  executable: false;
87
105
  artifacts: Record<string, {
88
106
  digest: string;
89
107
  source: string;
108
+ kind: "archive";
109
+ format: "tar+gzip";
110
+ } | {
111
+ digest: string;
112
+ source: string;
113
+ kind: "file";
114
+ mediaType: string;
115
+ } | {
116
+ digest: string;
117
+ kind: "oci-image";
118
+ identity: string;
90
119
  }>;
91
120
  planId: string;
121
+ deploymentId: string;
122
+ stackId: string;
123
+ stateBackend: {
124
+ key: string;
125
+ type: "s3";
126
+ schemaVersion: "treeseed.hosted-state-backend/v1";
127
+ teamId: string;
128
+ environment: "production" | "staging";
129
+ deploymentId: string;
130
+ stackId: string;
131
+ connectionRef: string;
132
+ bucket: string;
133
+ region: string;
134
+ encryptionKeyRef: string;
135
+ bindingDigest: string;
136
+ endpoint?: string | undefined;
137
+ usePathStyle?: boolean | undefined;
138
+ } | null;
92
139
  providerConnections: Partial<Record<"cloudflare" | "railway", {
93
140
  connectionRef: string;
94
141
  nonSecretConfig: Record<string, string | number | boolean>;
@@ -104,7 +151,11 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
104
151
  approval: {
105
152
  schemaVersion: "treeseed.hosted-topology-approval/v1";
106
153
  planDigest: string;
154
+ teamId: string;
107
155
  environment: "production" | "staging";
156
+ deploymentId: string;
157
+ stackId: string;
158
+ backendBindingDigest: string;
108
159
  decision: "approved";
109
160
  approvedBy: string;
110
161
  approvedAt: string;
@@ -112,7 +163,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
112
163
  plan: {
113
164
  schemaVersion: "treeseed.hosted-topology-plan/v1";
114
165
  blockers: {
115
- code: "connection-unavailable" | "dependency-cycle" | "observation-unhealthy" | "adoption-drift";
166
+ code: "connection-unavailable" | "state-backend-unavailable" | "dependency-cycle" | "observation-unhealthy" | "adoption-drift";
116
167
  message: string;
117
168
  resourceId?: string | undefined;
118
169
  }[];
@@ -120,12 +171,12 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
120
171
  actions: {
121
172
  action: "noop" | "create" | "adopt" | "update";
122
173
  resourceId: string;
123
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
174
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
124
175
  provider: "cloudflare" | "railway";
125
176
  providerResourceId: string | null;
126
177
  desiredResource: {
127
178
  id: string;
128
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
179
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
129
180
  provider: "cloudflare" | "railway";
130
181
  adoption: {
131
182
  mode: "adopt-or-create";
@@ -149,13 +200,43 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
149
200
  desiredDigest: string;
150
201
  previousDigest: string | null;
151
202
  }[];
203
+ teamId: string;
152
204
  environment: "production" | "staging";
153
205
  executable: false;
154
206
  artifacts: Record<string, {
155
207
  digest: string;
156
208
  source: string;
209
+ kind: "archive";
210
+ format: "tar+gzip";
211
+ } | {
212
+ digest: string;
213
+ source: string;
214
+ kind: "file";
215
+ mediaType: string;
216
+ } | {
217
+ digest: string;
218
+ kind: "oci-image";
219
+ identity: string;
157
220
  }>;
158
221
  planId: string;
222
+ deploymentId: string;
223
+ stackId: string;
224
+ stateBackend: {
225
+ key: string;
226
+ type: "s3";
227
+ schemaVersion: "treeseed.hosted-state-backend/v1";
228
+ teamId: string;
229
+ environment: "production" | "staging";
230
+ deploymentId: string;
231
+ stackId: string;
232
+ connectionRef: string;
233
+ bucket: string;
234
+ region: string;
235
+ encryptionKeyRef: string;
236
+ bindingDigest: string;
237
+ endpoint?: string | undefined;
238
+ usePathStyle?: boolean | undefined;
239
+ } | null;
159
240
  providerConnections: Partial<Record<"cloudflare" | "railway", {
160
241
  connectionRef: string;
161
242
  nonSecretConfig: Record<string, string | number | boolean>;
@@ -175,7 +256,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
175
256
  resources: {
176
257
  state: "missing" | "degraded" | "healthy";
177
258
  resourceId: string;
178
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
259
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
179
260
  provider: "cloudflare" | "railway";
180
261
  providerResourceId: string | null;
181
262
  managedBy: "external" | "treeseed" | null;
@@ -183,17 +264,21 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
183
264
  observedAt: string;
184
265
  }[];
185
266
  planDigest: string;
267
+ teamId: string;
186
268
  state: "known-good";
187
269
  environment: "production" | "staging";
188
270
  completedAt: string;
189
271
  receiptId: string;
272
+ deploymentId: string;
273
+ stackId: string;
190
274
  declarationDigest: string;
191
275
  topologyId: string;
192
276
  platformCommit: string;
277
+ backendBindingDigest: string;
193
278
  previousResources: {
194
279
  state: "missing" | "degraded" | "healthy";
195
280
  resourceId: string;
196
- kind: "admin-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
281
+ kind: "admin-application" | "pages-application" | "dns-record" | "tls-policy" | "api-proxy" | "control-plane-api" | "postgresql" | "operations-runner" | "treedx-service";
197
282
  provider: "cloudflare" | "railway";
198
283
  providerResourceId: string | null;
199
284
  managedBy: "external" | "treeseed" | null;
@@ -207,6 +292,7 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
207
292
  }, {}, {
208
293
  rollback: {
209
294
  schemaVersion: "treeseed.hosted-topology-rollback/v1";
295
+ teamId: string;
210
296
  environment: "production" | "staging";
211
297
  operations: {
212
298
  action: "noop" | "restore" | "delete-created";
@@ -214,13 +300,20 @@ export declare const HOSTED_TOPOLOGY_OPERATIONS: {
214
300
  providerResourceId: string;
215
301
  targetDigest: string | null;
216
302
  }[];
303
+ deploymentId: string;
304
+ stackId: string;
305
+ backendBindingDigest: string;
217
306
  rollbackId: string;
218
307
  sourceReceiptId: string;
219
308
  rollbackDigest: string;
220
309
  };
221
310
  approval: {
222
311
  schemaVersion: "treeseed.hosted-topology-rollback-approval/v1";
312
+ teamId: string;
223
313
  environment: "production" | "staging";
314
+ deploymentId: string;
315
+ stackId: string;
316
+ backendBindingDigest: string;
224
317
  decision: "approved";
225
318
  approvedBy: string;
226
319
  approvedAt: string;