@treeseed/sdk 0.10.24 → 0.10.26

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 (39) hide show
  1. package/dist/index.d.ts +12 -2
  2. package/dist/index.js +42 -1
  3. package/dist/market-client.d.ts +23 -0
  4. package/dist/market-client.js +30 -0
  5. package/dist/operations/providers/default.js +103 -10
  6. package/dist/operations/repository-operations.d.ts +6 -1
  7. package/dist/operations/repository-operations.js +44 -0
  8. package/dist/operations/services/config-runtime.d.ts +24 -9
  9. package/dist/operations/services/config-runtime.js +60 -12
  10. package/dist/operations/services/deploy.js +6 -1
  11. package/dist/operations/services/hub-launch.js +1 -0
  12. package/dist/operations/services/hub-provider-launch.d.ts +11 -1
  13. package/dist/operations/services/hub-provider-launch.js +81 -8
  14. package/dist/operations/services/project-host-operations.d.ts +153 -0
  15. package/dist/operations/services/project-host-operations.js +365 -0
  16. package/dist/operations/services/project-platform.d.ts +198 -193
  17. package/dist/operations/services/project-platform.js +29 -14
  18. package/dist/operations/services/railway-deploy.d.ts +3 -0
  19. package/dist/operations/services/railway-deploy.js +74 -35
  20. package/dist/operations/services/release-candidate.js +8 -2
  21. package/dist/operations/services/template-host-bindings.d.ts +68 -0
  22. package/dist/operations/services/template-host-bindings.js +400 -0
  23. package/dist/operations/services/template-registry.d.ts +22 -2
  24. package/dist/operations/services/template-registry.js +60 -3
  25. package/dist/operations/services/template-secret-sync.d.ts +97 -0
  26. package/dist/operations/services/template-secret-sync.js +292 -0
  27. package/dist/platform/environment.d.ts +3 -0
  28. package/dist/project-workflow.d.ts +7 -1
  29. package/dist/scripts/scaffold-site.js +3 -2
  30. package/dist/scripts/test-scaffold.js +2 -1
  31. package/dist/sdk-types.d.ts +87 -0
  32. package/dist/sdk-types.js +29 -0
  33. package/dist/template-catalog.js +3 -1
  34. package/dist/template-launch-requirements.d.ts +118 -0
  35. package/dist/template-launch-requirements.js +759 -0
  36. package/dist/template-launch-ui.d.ts +85 -0
  37. package/dist/template-launch-ui.js +189 -0
  38. package/dist/treeseed/template-catalog/catalog.fixture.json +330 -3
  39. package/package.json +13 -1
@@ -0,0 +1,759 @@
1
+ import {
2
+ PROJECT_ENVIRONMENT_NAMES,
3
+ PROJECT_LAUNCH_REQUIREMENT_KINDS,
4
+ TEMPLATE_CONFIG_MERGE_STRATEGIES,
5
+ TEMPLATE_CONFIG_WRITE_TARGETS,
6
+ TEMPLATE_CONFIG_WRITE_WHEN,
7
+ TEMPLATE_HOST_REQUIREMENT_TYPES,
8
+ TEMPLATE_RESOURCE_REQUIREMENT_TYPES,
9
+ TEMPLATE_SECRET_SENSITIVITIES,
10
+ TEMPLATE_SECRET_SOURCES,
11
+ TEMPLATE_SECRET_TARGETS
12
+ } from "./sdk-types.js";
13
+ function expectRecord(value, label) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
15
+ throw new Error(`${label} must be an object.`);
16
+ }
17
+ return value;
18
+ }
19
+ function optionalRecord(value, label) {
20
+ if (value === void 0 || value === null) return void 0;
21
+ return expectRecord(value, label);
22
+ }
23
+ function expectString(value, label) {
24
+ if (typeof value !== "string" || value.trim().length === 0) {
25
+ throw new Error(`${label} must be a non-empty string.`);
26
+ }
27
+ return value.trim();
28
+ }
29
+ function optionalString(value) {
30
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
31
+ }
32
+ function expectBoolean(value, label) {
33
+ if (typeof value !== "boolean") {
34
+ throw new Error(`${label} must be a boolean.`);
35
+ }
36
+ return value;
37
+ }
38
+ function optionalStringArray(value, label) {
39
+ if (value === void 0 || value === null) return void 0;
40
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
41
+ return value.map((entry, index) => expectString(entry, `${label}[${index}]`));
42
+ }
43
+ function optionalRecordOfStrings(value, label) {
44
+ if (value === void 0 || value === null) return void 0;
45
+ const record = expectRecord(value, label);
46
+ const normalized = {};
47
+ for (const [key, entry] of Object.entries(record)) {
48
+ normalized[key] = expectString(entry, `${label}.${key}`);
49
+ }
50
+ return normalized;
51
+ }
52
+ function expectEnum(value, allowed, label) {
53
+ const text = expectString(value, label);
54
+ if (!allowed.includes(text)) {
55
+ throw new Error(`${label} uses unsupported value "${text}".`);
56
+ }
57
+ return text;
58
+ }
59
+ function optionalEnum(value, allowed, label) {
60
+ if (value === void 0 || value === null || value === "") return void 0;
61
+ return expectEnum(value, allowed, label);
62
+ }
63
+ function expectArray(value, label) {
64
+ if (value === void 0 || value === null) return [];
65
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
66
+ return value;
67
+ }
68
+ function normalizeBoolean(value) {
69
+ return value === void 0 || value === null ? false : expectBoolean(value, "required");
70
+ }
71
+ function validateRequirementKey(key, label) {
72
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(key)) {
73
+ throw new Error(`${label} must start with a letter and contain only letters, numbers, underscores, or hyphens.`);
74
+ }
75
+ return key;
76
+ }
77
+ function validateConfigWritePath(path, label) {
78
+ const segments = path.split(".");
79
+ if (segments.some((segment) => !segment || segment === "..")) {
80
+ throw new Error(`${label} must be a safe dot path.`);
81
+ }
82
+ for (const segment of segments) {
83
+ if (!/^[A-Za-z0-9_-]+$/u.test(segment)) {
84
+ throw new Error(`${label} contains unsafe segment "${segment}".`);
85
+ }
86
+ if (segment === "__proto__" || segment === "prototype" || segment === "constructor") {
87
+ throw new Error(`${label} contains forbidden segment "${segment}".`);
88
+ }
89
+ }
90
+ return path;
91
+ }
92
+ function normalizeConfigWrite(value, label) {
93
+ const record = expectRecord(value, label);
94
+ return {
95
+ target: expectEnum(record.target, TEMPLATE_CONFIG_WRITE_TARGETS, `${label}.target`),
96
+ path: validateConfigWritePath(expectString(record.path, `${label}.path`), `${label}.path`),
97
+ valueFrom: expectString(record.valueFrom, `${label}.valueFrom`),
98
+ writeWhen: optionalEnum(record.writeWhen, TEMPLATE_CONFIG_WRITE_WHEN, `${label}.writeWhen`),
99
+ mergeStrategy: optionalEnum(record.mergeStrategy, TEMPLATE_CONFIG_MERGE_STRATEGIES, `${label}.mergeStrategy`)
100
+ };
101
+ }
102
+ function normalizeEnvironmentWrite(value, label) {
103
+ const record = expectRecord(value, label);
104
+ return {
105
+ env: expectString(record.env, `${label}.env`),
106
+ valueFrom: expectString(record.valueFrom, `${label}.valueFrom`),
107
+ targets: optionalStringArray(record.targets, `${label}.targets`)?.map((target, index) => expectEnum(target, TEMPLATE_SECRET_TARGETS, `${label}.targets[${index}]`)),
108
+ scopes: optionalStringArray(record.scopes, `${label}.scopes`)?.map((scope, index) => expectEnum(scope, PROJECT_ENVIRONMENT_NAMES, `${label}.scopes[${index}]`)),
109
+ sensitivity: optionalEnum(record.sensitivity, TEMPLATE_SECRET_SENSITIVITIES, `${label}.sensitivity`)
110
+ };
111
+ }
112
+ function normalizeHostRequirement(value, index) {
113
+ const label = `launchRequirements.hosts[${index}]`;
114
+ const record = expectRecord(value, label);
115
+ const kind = record.kind === void 0 ? "host" : expectString(record.kind, `${label}.kind`);
116
+ if (kind !== "host") throw new Error(`${label}.kind must be "host".`);
117
+ return {
118
+ kind: "host",
119
+ key: validateRequirementKey(expectString(record.key, `${label}.key`), `${label}.key`),
120
+ type: expectEnum(record.type, TEMPLATE_HOST_REQUIREMENT_TYPES, `${label}.type`),
121
+ required: normalizeBoolean(record.required),
122
+ compatibleProviders: optionalStringArray(record.compatibleProviders, `${label}.compatibleProviders`),
123
+ displayName: expectString(record.displayName, `${label}.displayName`),
124
+ purpose: expectString(record.purpose, `${label}.purpose`),
125
+ defaultSelection: optionalEnum(record.defaultSelection, ["team-default", "managed", "none"], `${label}.defaultSelection`),
126
+ configWrites: expectArray(record.configWrites, `${label}.configWrites`).map((entry, writeIndex) => normalizeConfigWrite(entry, `${label}.configWrites[${writeIndex}]`)),
127
+ environmentWrites: expectArray(record.environmentWrites, `${label}.environmentWrites`).map((entry, writeIndex) => normalizeEnvironmentWrite(entry, `${label}.environmentWrites[${writeIndex}]`))
128
+ };
129
+ }
130
+ function normalizeResourceRequirement(value, index) {
131
+ const label = `launchRequirements.resources[${index}]`;
132
+ const record = expectRecord(value, label);
133
+ const kind = record.kind === void 0 ? "resource" : expectString(record.kind, `${label}.kind`);
134
+ if (kind !== "resource") throw new Error(`${label}.kind must be "resource".`);
135
+ return {
136
+ kind: "resource",
137
+ key: validateRequirementKey(expectString(record.key, `${label}.key`), `${label}.key`),
138
+ type: expectEnum(record.type, TEMPLATE_RESOURCE_REQUIREMENT_TYPES, `${label}.type`),
139
+ required: normalizeBoolean(record.required),
140
+ compatibleProviders: optionalStringArray(record.compatibleProviders, `${label}.compatibleProviders`),
141
+ displayName: expectString(record.displayName, `${label}.displayName`),
142
+ purpose: expectString(record.purpose, `${label}.purpose`),
143
+ configWrites: expectArray(record.configWrites, `${label}.configWrites`).map((entry, writeIndex) => normalizeConfigWrite(entry, `${label}.configWrites[${writeIndex}]`)),
144
+ environmentWrites: expectArray(record.environmentWrites, `${label}.environmentWrites`).map((entry, writeIndex) => normalizeEnvironmentWrite(entry, `${label}.environmentWrites[${writeIndex}]`))
145
+ };
146
+ }
147
+ function normalizeSecretRequirement(value, index) {
148
+ const label = `launchRequirements.secrets[${index}]`;
149
+ const record = expectRecord(value, label);
150
+ const kind = record.kind === void 0 ? "secret" : expectString(record.kind, `${label}.kind`);
151
+ if (kind !== "secret") throw new Error(`${label}.kind must be "secret".`);
152
+ return {
153
+ kind: "secret",
154
+ key: validateRequirementKey(expectString(record.key, `${label}.key`), `${label}.key`),
155
+ env: expectString(record.env, `${label}.env`),
156
+ required: normalizeBoolean(record.required),
157
+ sensitivity: expectEnum(record.sensitivity, TEMPLATE_SECRET_SENSITIVITIES, `${label}.sensitivity`),
158
+ targets: expectArray(record.targets, `${label}.targets`).map((target, targetIndex) => expectEnum(target, TEMPLATE_SECRET_TARGETS, `${label}.targets[${targetIndex}]`)),
159
+ source: expectEnum(record.source, TEMPLATE_SECRET_SOURCES, `${label}.source`)
160
+ };
161
+ }
162
+ function normalizeTemplateLaunchRequirements(value, label = "launchRequirements") {
163
+ if (value === void 0 || value === null) return void 0;
164
+ const record = expectRecord(value, label);
165
+ const version = record.version === void 0 || record.version === null ? void 0 : Number(record.version);
166
+ if (version !== void 0 && (!Number.isInteger(version) || version < 1)) {
167
+ throw new Error(`${label}.version must be a positive integer.`);
168
+ }
169
+ return {
170
+ version,
171
+ hosts: expectArray(record.hosts, `${label}.hosts`).map(normalizeHostRequirement),
172
+ resources: expectArray(record.resources, `${label}.resources`).map(normalizeResourceRequirement),
173
+ secrets: expectArray(record.secrets, `${label}.secrets`).map(normalizeSecretRequirement)
174
+ };
175
+ }
176
+ function validateTemplateLaunchRequirements(value, label = "launchRequirements") {
177
+ normalizeTemplateLaunchRequirements(value, label);
178
+ }
179
+ function normalizeEnvironmentScopes(value, label) {
180
+ return optionalStringArray(value, label)?.map((scope, index) => expectEnum(scope, PROJECT_ENVIRONMENT_NAMES, `${label}[${index}]`));
181
+ }
182
+ function normalizeBinding(value, key) {
183
+ const label = `hostBindings.${key}`;
184
+ const record = expectRecord(value, label);
185
+ const requirementKind = optionalEnum(record.requirementKind ?? record.kind, PROJECT_LAUNCH_REQUIREMENT_KINDS, `${label}.requirementKind`);
186
+ const normalized = {
187
+ requirementKey: validateRequirementKey(optionalString(record.requirementKey) ?? key, `${label}.requirementKey`),
188
+ requirementKind: requirementKind ?? "host",
189
+ type: expectString(record.type, `${label}.type`),
190
+ provider: expectString(record.provider, `${label}.provider`),
191
+ hostId: optionalString(record.hostId) ?? null,
192
+ managedHostKey: optionalString(record.managedHostKey) ?? null,
193
+ mode: optionalString(record.mode) ?? null,
194
+ displayName: optionalString(record.displayName),
195
+ environmentScopes: normalizeEnvironmentScopes(record.environmentScopes, `${label}.environmentScopes`),
196
+ configValues: optionalRecord(record.configValues, `${label}.configValues`),
197
+ environmentValues: optionalRecordOfStrings(record.environmentValues, `${label}.environmentValues`),
198
+ secretRefs: optionalRecordOfStrings(record.secretRefs, `${label}.secretRefs`),
199
+ selectedBy: optionalEnum(record.selectedBy, ["user", "team-default", "managed-default", "template-default"], `${label}.selectedBy`)
200
+ };
201
+ return normalized;
202
+ }
203
+ function defaultScopes(input) {
204
+ return normalizeEnvironmentScopes(input.targetEnvironments, "targetEnvironments") ?? ["staging", "prod"];
205
+ }
206
+ function deriveLegacyBindings(input) {
207
+ const intent = optionalRecord(input.intent, "intent");
208
+ const repository = optionalRecord(intent?.repository, "intent.repository");
209
+ const bindings = {};
210
+ const scopes = defaultScopes(input);
211
+ const repositoryHostId = optionalString(repository?.hostId) ?? optionalString(input.repositoryHostId) ?? "platform:github:hosted-hubs";
212
+ bindings.sourceRepository = {
213
+ requirementKey: "sourceRepository",
214
+ requirementKind: "host",
215
+ type: "repository",
216
+ provider: "github",
217
+ hostId: repositoryHostId,
218
+ mode: repositoryHostId.startsWith("platform:") ? "treeseed_managed" : void 0,
219
+ environmentScopes: scopes,
220
+ selectedBy: repositoryHostId.startsWith("platform:") ? "managed-default" : "user"
221
+ };
222
+ const cloudflareHostMode = optionalString(input.cloudflareHostMode);
223
+ const cloudflareHostId = optionalString(input.cloudflareHostId);
224
+ if (cloudflareHostMode || cloudflareHostId) {
225
+ bindings.publicWeb = {
226
+ requirementKey: "publicWeb",
227
+ requirementKind: "host",
228
+ type: "web",
229
+ provider: "cloudflare",
230
+ hostId: cloudflareHostMode === "team_owned" ? cloudflareHostId ?? null : null,
231
+ managedHostKey: cloudflareHostMode === "treeseed_managed" ? "treeseed-managed-cloudflare" : null,
232
+ mode: cloudflareHostMode ?? null,
233
+ environmentScopes: scopes,
234
+ selectedBy: cloudflareHostMode === "treeseed_managed" ? "managed-default" : "user"
235
+ };
236
+ }
237
+ const emailHostMode = optionalString(input.emailHostMode);
238
+ const emailHostId = optionalString(input.emailHostId);
239
+ if (emailHostMode || emailHostId) {
240
+ bindings.transactionalEmail = {
241
+ requirementKey: "transactionalEmail",
242
+ requirementKind: "host",
243
+ type: "email",
244
+ provider: "smtp",
245
+ hostId: emailHostMode === "team_owned" ? emailHostId ?? null : null,
246
+ managedHostKey: emailHostMode === "treeseed_managed" ? "treeseed-managed-email" : null,
247
+ mode: emailHostMode ?? null,
248
+ environmentScopes: scopes,
249
+ selectedBy: emailHostMode === "treeseed_managed" ? "managed-default" : "user"
250
+ };
251
+ }
252
+ return bindings;
253
+ }
254
+ function normalizeProjectLaunchHostBindings(input) {
255
+ const record = expectRecord(input, "project launch request");
256
+ const normalized = {};
257
+ for (const [key, binding] of Object.entries(optionalRecord(record.hostBindings, "hostBindings") ?? {})) {
258
+ normalized[validateRequirementKey(key, `hostBindings key "${key}"`)] = normalizeBinding(binding, key);
259
+ }
260
+ for (const [key, binding] of Object.entries(deriveLegacyBindings(record))) {
261
+ if (!normalized[key]) normalized[key] = binding;
262
+ }
263
+ return normalized;
264
+ }
265
+ function hostMetadataType(host) {
266
+ const metadataType = typeof host.metadata?.hostType === "string" ? host.metadata.hostType : host.type;
267
+ if (metadataType === "repository_host" || metadataType === "repository") return "repository";
268
+ if (metadataType === "web_host" || metadataType === "cloudflare" || metadataType === "web") return "web";
269
+ if (metadataType === "email_host" || metadataType === "smtp" || metadataType === "email") return "email";
270
+ if (metadataType === "ai_host" || metadataType === "ai") return "ai";
271
+ if (host.provider === "smtp") return "email";
272
+ if (host.provider === "cloudflare") return "web";
273
+ if (host.provider === "github") return "repository";
274
+ return metadataType ?? "";
275
+ }
276
+ function allRequirements(launchRequirements) {
277
+ return [
278
+ ...launchRequirements?.hosts ?? [],
279
+ ...launchRequirements?.resources ?? [],
280
+ ...launchRequirements?.secrets ?? []
281
+ ];
282
+ }
283
+ function normalizeSpecList(specs) {
284
+ if (!specs) return [];
285
+ return (Array.isArray(specs) ? specs : [specs]).flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
286
+ }
287
+ function safeLocalIdSegment(value) {
288
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "local";
289
+ }
290
+ function portableHostConfigValues(requirement, provider, alias) {
291
+ const values = {
292
+ localAlias: alias,
293
+ provider
294
+ };
295
+ if (requirement.type === "repository" && provider === "github") {
296
+ values.owner = alias;
297
+ values.github = { owner: alias };
298
+ return values;
299
+ }
300
+ if (requirement.type === "web" && provider === "cloudflare") {
301
+ values.cloudflare = { account: alias };
302
+ return values;
303
+ }
304
+ if (requirement.type === "email" && provider === "smtp") {
305
+ values.smtp = { profile: alias };
306
+ return values;
307
+ }
308
+ return values;
309
+ }
310
+ function localHostMetadata(requirement, managed) {
311
+ return {
312
+ hostType: requirement.type,
313
+ managed,
314
+ configured: true,
315
+ local: true
316
+ };
317
+ }
318
+ function parseProjectLaunchHostBindingSpecs(options) {
319
+ const specs = normalizeSpecList(options.specs);
320
+ const hostRequirements = new Map((options.launchRequirements?.hosts ?? []).map((requirement) => [requirement.key, requirement]));
321
+ const hostBindings = {};
322
+ const repositoryHosts = [];
323
+ const teamHosts = [];
324
+ const managedHosts = [];
325
+ const summaries = [];
326
+ const omitted = [];
327
+ for (const spec of specs) {
328
+ const separatorIndex = spec.indexOf("=");
329
+ if (separatorIndex < 1 || separatorIndex === spec.length - 1) {
330
+ throw new Error(`Invalid host binding spec "${spec}". Expected <requirement>=<provider>:<alias> or <requirement>=none.`);
331
+ }
332
+ const key = validateRequirementKey(spec.slice(0, separatorIndex).trim(), `host binding spec "${spec}" requirement`);
333
+ const value = spec.slice(separatorIndex + 1).trim();
334
+ const requirement = hostRequirements.get(key);
335
+ if (!requirement) {
336
+ throw new Error(`Unknown host binding requirement "${key}".`);
337
+ }
338
+ if (value === "none") {
339
+ if (requirement.required) {
340
+ throw new Error(`${key} is required and cannot be set to none.`);
341
+ }
342
+ omitted.push({
343
+ requirementKey: key,
344
+ requirementKind: "host",
345
+ type: requirement.type,
346
+ provider: null,
347
+ alias: null,
348
+ mode: "none",
349
+ displayName: requirement.displayName
350
+ });
351
+ delete hostBindings[key];
352
+ continue;
353
+ }
354
+ const [rawProvider, ...aliasParts] = value.split(":");
355
+ const provider = expectString(rawProvider, `${key} provider`);
356
+ const alias = expectString(aliasParts.join(":"), `${key} alias`);
357
+ if (requirement.compatibleProviders?.length && !requirement.compatibleProviders.includes(provider)) {
358
+ throw new Error(`${key} requires provider ${requirement.compatibleProviders.join(", ")}, but received "${provider}".`);
359
+ }
360
+ if (String(requirement.type) === "capacity-provider" || provider === "capacity-provider") {
361
+ throw new Error(`${key} capacity-provider host bindings are not accepted for standard project launch.`);
362
+ }
363
+ const managed = alias === "managed";
364
+ const localSegment = `${safeLocalIdSegment(key)}-${safeLocalIdSegment(provider)}-${safeLocalIdSegment(alias)}`;
365
+ const hostId = managed ? `local-managed:${localSegment}` : `local:${localSegment}`;
366
+ const displayName = managed ? `Managed ${requirement.displayName}` : `${requirement.displayName} (${alias})`;
367
+ const host = {
368
+ id: hostId,
369
+ type: requirement.type,
370
+ provider,
371
+ ownership: managed ? "treeseed_managed" : "team_owned",
372
+ name: displayName,
373
+ accountLabel: alias,
374
+ organizationOrOwner: requirement.type === "repository" ? alias : void 0,
375
+ allowedEnvironments: ["local", "staging", "prod"],
376
+ status: "active",
377
+ metadata: localHostMetadata(requirement, managed)
378
+ };
379
+ if (managed) {
380
+ managedHosts.push(host);
381
+ } else if (requirement.type === "repository") {
382
+ repositoryHosts.push(host);
383
+ } else {
384
+ teamHosts.push(host);
385
+ }
386
+ hostBindings[key] = {
387
+ requirementKey: key,
388
+ requirementKind: "host",
389
+ type: requirement.type,
390
+ provider,
391
+ hostId: managed ? null : hostId,
392
+ managedHostKey: managed ? hostId : null,
393
+ mode: managed ? "treeseed_managed" : "team_owned",
394
+ displayName,
395
+ environmentScopes: ["local", "staging", "prod"],
396
+ configValues: portableHostConfigValues(requirement, provider, alias),
397
+ environmentValues: {},
398
+ secretRefs: {},
399
+ selectedBy: managed ? "managed-default" : "user"
400
+ };
401
+ summaries.push({
402
+ requirementKey: key,
403
+ requirementKind: "host",
404
+ type: requirement.type,
405
+ provider,
406
+ alias,
407
+ mode: managed ? "treeseed_managed" : "team_owned",
408
+ displayName
409
+ });
410
+ }
411
+ return {
412
+ hostBindings,
413
+ repositoryHosts,
414
+ teamHosts,
415
+ managedHosts,
416
+ summaries,
417
+ omitted
418
+ };
419
+ }
420
+ function defaultHostIds(defaultHosts, requirementKey, type) {
421
+ const keys = [
422
+ requirementKey,
423
+ type,
424
+ requirementKey === "sourceRepository" ? "repository" : null,
425
+ requirementKey === "publicWeb" ? "web" : null,
426
+ requirementKey === "transactionalEmail" ? "email" : null
427
+ ].filter(Boolean);
428
+ for (const key of keys) {
429
+ const value = defaultHosts?.[key];
430
+ if (typeof value === "string" && value.trim()) return value.trim();
431
+ }
432
+ return null;
433
+ }
434
+ function hostMatchesRequirement(host, requirement, environmentScopes) {
435
+ if (hostMetadataType(host) !== requirement.type) return false;
436
+ if (requirement.compatibleProviders?.length && !requirement.compatibleProviders.includes(host.provider)) return false;
437
+ if (host.allowedEnvironments?.length) {
438
+ return environmentScopes.every((scope) => host.allowedEnvironments?.includes(scope));
439
+ }
440
+ return true;
441
+ }
442
+ function hostUsabilityError(host, requirement, required) {
443
+ if (!required) return null;
444
+ const status = typeof host.status === "string" ? host.status : "active";
445
+ if (status === "active" || status === "ready") return null;
446
+ return `${requirement.key} selected host "${host.name ?? host.id}" is not active (${status}).`;
447
+ }
448
+ function resolveManagedHost(requirement, managedHosts, environmentScopes) {
449
+ return managedHosts.find((host) => hostMatchesRequirement(host, requirement, environmentScopes)) ?? null;
450
+ }
451
+ function resolveTeamHost(requirement, hosts, environmentScopes, id) {
452
+ return hosts.find((host) => (!id || host.id === id) && hostMatchesRequirement(host, requirement, environmentScopes)) ?? null;
453
+ }
454
+ function normalizeHostSnapshot(host) {
455
+ if (!host) return null;
456
+ return {
457
+ id: host.id,
458
+ name: host.name ?? null,
459
+ ownership: host.ownership ?? null,
460
+ status: host.status ?? null,
461
+ accountLabel: host.accountLabel ?? null,
462
+ organizationOrOwner: host.organizationOrOwner ?? null,
463
+ metadata: host.metadata ? {
464
+ hostType: host.metadata.hostType,
465
+ dns: host.metadata.dns,
466
+ managed: host.metadata.managed,
467
+ configured: host.metadata.configured,
468
+ missingConfigKeys: host.metadata.missingConfigKeys
469
+ } : void 0
470
+ };
471
+ }
472
+ function normalizeResourceSnapshot(binding) {
473
+ const id = binding.hostId ?? binding.managedHostKey ?? null;
474
+ if (!id) return null;
475
+ return {
476
+ id,
477
+ name: binding.displayName ?? id,
478
+ ownership: binding.managedHostKey ? "treeseed_managed" : "team_owned",
479
+ status: "active",
480
+ accountLabel: null,
481
+ organizationOrOwner: null,
482
+ metadata: {
483
+ hostType: binding.type,
484
+ resource: true
485
+ }
486
+ };
487
+ }
488
+ function bindingSelectedBy(binding, host, fallback) {
489
+ if (binding?.selectedBy) return binding.selectedBy;
490
+ if (host?.ownership === "treeseed_managed") return "managed-default";
491
+ return fallback;
492
+ }
493
+ function resolveHostRequirementBinding(requirement, input, options, selectedAt) {
494
+ const environmentScopes = input?.environmentScopes ?? ["staging", "prod"];
495
+ if (options.standardProjectLaunch !== false && (input?.type === "capacity-provider" || input?.provider === "capacity-provider")) {
496
+ throw new Error(`${requirement.key} capacity-provider host bindings are not accepted for standard project launch.`);
497
+ }
498
+ if (input && input.requirementKind !== "host") {
499
+ throw new Error(`${requirement.key} must bind a host requirement.`);
500
+ }
501
+ if (input && input.type !== requirement.type) {
502
+ throw new Error(`${requirement.key} requires host type "${requirement.type}", but received "${input.type}".`);
503
+ }
504
+ if (input && requirement.compatibleProviders?.length && !requirement.compatibleProviders.includes(input.provider)) {
505
+ throw new Error(`${requirement.key} requires provider ${requirement.compatibleProviders.join(", ")}, but received "${input.provider}".`);
506
+ }
507
+ const inventory = requirement.type === "repository" ? options.repositoryHosts ?? [] : [...options.teamHosts ?? [], ...options.managedHosts ?? []];
508
+ const teamInventory = requirement.type === "repository" ? options.repositoryHosts ?? [] : options.teamHosts ?? [];
509
+ let host = null;
510
+ let managedHostKey = input?.managedHostKey ?? null;
511
+ let hostId = input?.hostId ?? null;
512
+ let provider = input?.provider ?? requirement.compatibleProviders?.[0] ?? "";
513
+ let selectedBy = input?.selectedBy ?? "user";
514
+ if (input) {
515
+ if (input.mode === "treeseed_managed" || input.managedHostKey) {
516
+ host = requirement.type === "repository" ? resolveTeamHost(requirement, inventory, environmentScopes, input.hostId ?? null) : resolveManagedHost(requirement, options.managedHosts ?? [], environmentScopes);
517
+ if (!host) throw new Error(`${requirement.key} requires a compatible managed ${requirement.type} host.`);
518
+ provider = host.provider;
519
+ managedHostKey = input.managedHostKey ?? host.id;
520
+ hostId = requirement.type === "repository" ? input.hostId ?? host.id : null;
521
+ } else if (input.hostId) {
522
+ host = resolveTeamHost(requirement, inventory, environmentScopes, input.hostId);
523
+ if (!host) throw new Error(`${requirement.key} selected host "${input.hostId}" is not available or compatible.`);
524
+ provider = host.provider;
525
+ hostId = host.id;
526
+ } else if (requirement.required) {
527
+ throw new Error(`${requirement.key} is required and must select a compatible host.`);
528
+ }
529
+ } else if (requirement.defaultSelection === "team-default") {
530
+ const defaultHostId = defaultHostIds(options.defaultHosts, requirement.key, requirement.type);
531
+ host = defaultHostId ? resolveTeamHost(requirement, teamInventory, environmentScopes, defaultHostId) : null;
532
+ if (!host && !defaultHostId) host = resolveTeamHost(requirement, teamInventory, environmentScopes);
533
+ if (!host) host = resolveManagedHost(requirement, options.managedHosts ?? [], environmentScopes);
534
+ if (host) {
535
+ provider = host.provider;
536
+ hostId = host.id;
537
+ managedHostKey = host.ownership === "treeseed_managed" ? host.id : null;
538
+ selectedBy = defaultHostId ? "team-default" : host.ownership === "treeseed_managed" ? "managed-default" : "team-default";
539
+ }
540
+ } else if (requirement.defaultSelection === "managed") {
541
+ host = resolveManagedHost(requirement, options.managedHosts ?? [], environmentScopes);
542
+ if (host) {
543
+ provider = host.provider;
544
+ managedHostKey = host.id;
545
+ hostId = requirement.type === "repository" ? host.id : null;
546
+ selectedBy = "managed-default";
547
+ }
548
+ }
549
+ if (!host && requirement.required) {
550
+ throw new Error(`${requirement.key} is required and no compatible ${requirement.type} host is available.`);
551
+ }
552
+ if (host) {
553
+ const usabilityError = hostUsabilityError(host, requirement, requirement.required);
554
+ if (usabilityError) throw new Error(usabilityError);
555
+ }
556
+ return {
557
+ requirementKey: requirement.key,
558
+ requirementKind: "host",
559
+ type: requirement.type,
560
+ provider,
561
+ hostId,
562
+ managedHostKey,
563
+ displayName: input?.displayName ?? requirement.displayName,
564
+ environmentScopes,
565
+ configValues: input?.configValues ?? {},
566
+ environmentValues: input?.environmentValues ?? {},
567
+ secretRefs: input?.secretRefs ?? {},
568
+ provenance: {
569
+ selectedBy: bindingSelectedBy(input, host, selectedBy),
570
+ selectedAt
571
+ },
572
+ host: normalizeHostSnapshot(host)
573
+ };
574
+ }
575
+ function resolveResourceRequirementBinding(requirement, input, selectedAt) {
576
+ if (!input) {
577
+ if (requirement.required) {
578
+ throw new Error(`${requirement.key} is required and must select a compatible resource.`);
579
+ }
580
+ return null;
581
+ }
582
+ const environmentScopes = input.environmentScopes ?? ["staging", "prod"];
583
+ if (input.requirementKind !== "resource") {
584
+ throw new Error(`${requirement.key} must bind a resource requirement.`);
585
+ }
586
+ if (input.type !== requirement.type) {
587
+ throw new Error(`${requirement.key} requires resource type "${requirement.type}", but received "${input.type}".`);
588
+ }
589
+ if (requirement.compatibleProviders?.length && !requirement.compatibleProviders.includes(input.provider)) {
590
+ throw new Error(`${requirement.key} requires provider ${requirement.compatibleProviders.join(", ")}, but received "${input.provider}".`);
591
+ }
592
+ return {
593
+ requirementKey: requirement.key,
594
+ requirementKind: "resource",
595
+ type: requirement.type,
596
+ provider: input.provider,
597
+ hostId: input.hostId ?? null,
598
+ managedHostKey: input.managedHostKey ?? null,
599
+ displayName: input.displayName ?? requirement.displayName,
600
+ environmentScopes,
601
+ configValues: input.configValues ?? {},
602
+ environmentValues: input.environmentValues ?? {},
603
+ secretRefs: input.secretRefs ?? {},
604
+ provenance: {
605
+ selectedBy: input.selectedBy ?? "user",
606
+ selectedAt
607
+ },
608
+ host: normalizeResourceSnapshot(input)
609
+ };
610
+ }
611
+ function resolveAdHocBinding(key, input, options, selectedAt) {
612
+ if (options.standardProjectLaunch !== false && (input.type === "capacity-provider" || input.provider === "capacity-provider")) {
613
+ throw new Error(`${key} capacity-provider host bindings are not accepted for standard project launch.`);
614
+ }
615
+ const environmentScopes = input.environmentScopes ?? ["staging", "prod"];
616
+ const inventory = input.type === "repository" ? options.repositoryHosts ?? [] : [...options.teamHosts ?? [], ...options.managedHosts ?? []];
617
+ let host = null;
618
+ if (input.mode === "treeseed_managed" || input.managedHostKey) {
619
+ host = inventory.find((entry) => hostMetadataType(entry) === input.type && entry.provider === input.provider && entry.ownership === "treeseed_managed") ?? null;
620
+ } else if (input.hostId) {
621
+ host = inventory.find((entry) => entry.id === input.hostId && hostMetadataType(entry) === input.type && entry.provider === input.provider) ?? null;
622
+ }
623
+ return {
624
+ requirementKey: input.requirementKey ?? key,
625
+ requirementKind: input.requirementKind,
626
+ type: input.type,
627
+ provider: input.provider,
628
+ hostId: input.hostId ?? host?.id ?? null,
629
+ managedHostKey: input.managedHostKey ?? (host?.ownership === "treeseed_managed" ? host.id : null),
630
+ displayName: input.displayName ?? host?.name ?? key,
631
+ environmentScopes,
632
+ configValues: input.configValues ?? {},
633
+ environmentValues: input.environmentValues ?? {},
634
+ secretRefs: input.secretRefs ?? {},
635
+ provenance: {
636
+ selectedBy: bindingSelectedBy(input, host, input.selectedBy ?? "user"),
637
+ selectedAt
638
+ },
639
+ host: normalizeHostSnapshot(host)
640
+ };
641
+ }
642
+ function compatibilityFromBindings(bindings) {
643
+ const sourceRepository = bindings.sourceRepository;
644
+ const publicWeb = bindings.publicWeb;
645
+ const transactionalEmail = bindings.transactionalEmail;
646
+ const publicWebSelected = Boolean(publicWeb?.host || publicWeb?.hostId || publicWeb?.managedHostKey);
647
+ const transactionalEmailSelected = Boolean(transactionalEmail?.host || transactionalEmail?.hostId || transactionalEmail?.managedHostKey);
648
+ return {
649
+ repositoryHostId: sourceRepository?.hostId ?? null,
650
+ cloudflareHostMode: publicWebSelected && publicWeb ? publicWeb.provenance.selectedBy === "managed-default" || publicWeb.host?.ownership === "treeseed_managed" || publicWeb.managedHostKey ? "treeseed_managed" : "team_owned" : null,
651
+ cloudflareHostId: publicWebSelected && publicWeb?.host?.ownership === "team_owned" ? publicWeb.host.id : publicWebSelected && publicWeb?.hostId && !publicWeb.managedHostKey ? publicWeb.hostId : null,
652
+ emailHostMode: transactionalEmailSelected && transactionalEmail ? transactionalEmail.provenance.selectedBy === "managed-default" || transactionalEmail.host?.ownership === "treeseed_managed" || transactionalEmail.managedHostKey ? "treeseed_managed" : "team_owned" : null,
653
+ emailHostId: transactionalEmailSelected && transactionalEmail?.host?.ownership === "team_owned" ? transactionalEmail.host.id : transactionalEmailSelected && transactionalEmail?.hostId && !transactionalEmail.managedHostKey ? transactionalEmail.hostId : null
654
+ };
655
+ }
656
+ function resolveProjectLaunchHostBindings(options) {
657
+ const selectedAt = options.selectedAt ?? (/* @__PURE__ */ new Date()).toISOString();
658
+ const inputs = options.hostBindings ?? {};
659
+ const hostBindings = {};
660
+ const configWritePlan = [];
661
+ const secretItems = [];
662
+ const requirementKeys = /* @__PURE__ */ new Set();
663
+ for (const requirement of allRequirements(options.launchRequirements)) {
664
+ requirementKeys.add(requirement.key);
665
+ if (requirement.kind === "host") {
666
+ const binding = resolveHostRequirementBinding(requirement, inputs[requirement.key], options, selectedAt);
667
+ if (binding.host || requirement.required || inputs[requirement.key]) {
668
+ hostBindings[requirement.key] = binding;
669
+ for (const write of requirement.configWrites ?? []) {
670
+ configWritePlan.push({
671
+ ...write,
672
+ requirementKey: requirement.key,
673
+ requirementKind: "host",
674
+ requirementType: requirement.type,
675
+ provider: binding.provider
676
+ });
677
+ }
678
+ for (const write of requirement.environmentWrites ?? []) {
679
+ secretItems.push({
680
+ requirementKey: requirement.key,
681
+ requirementKind: "host",
682
+ env: write.env,
683
+ sensitivity: write.sensitivity ?? "plain",
684
+ source: write.valueFrom,
685
+ targets: write.targets ?? [],
686
+ scopes: write.scopes ?? binding.environmentScopes,
687
+ sourceHostId: binding.hostId ?? binding.host?.id ?? null
688
+ });
689
+ }
690
+ }
691
+ continue;
692
+ }
693
+ if (options.standardProjectLaunch !== false && requirement.kind === "resource") {
694
+ throw new Error(`${requirement.key} resource requirements are not accepted for standard project launch yet.`);
695
+ }
696
+ if (requirement.kind === "resource") {
697
+ const binding = resolveResourceRequirementBinding(requirement, inputs[requirement.key], selectedAt);
698
+ if (binding) {
699
+ hostBindings[requirement.key] = binding;
700
+ for (const write of requirement.configWrites ?? []) {
701
+ configWritePlan.push({
702
+ ...write,
703
+ requirementKey: requirement.key,
704
+ requirementKind: "resource",
705
+ requirementType: requirement.type,
706
+ provider: binding.provider
707
+ });
708
+ }
709
+ for (const write of requirement.environmentWrites ?? []) {
710
+ secretItems.push({
711
+ requirementKey: requirement.key,
712
+ requirementKind: "resource",
713
+ env: write.env,
714
+ sensitivity: write.sensitivity ?? "plain",
715
+ source: write.valueFrom,
716
+ targets: write.targets ?? [],
717
+ scopes: write.scopes ?? binding.environmentScopes,
718
+ sourceHostId: binding.hostId ?? binding.managedHostKey ?? binding.host?.id ?? null
719
+ });
720
+ }
721
+ }
722
+ continue;
723
+ }
724
+ if (requirement.kind === "secret") {
725
+ if (requirement.required && requirement.source === "selected-host" && !inputs[requirement.key]) {
726
+ throw new Error(`${requirement.key} is required and must resolve from a selected host.`);
727
+ }
728
+ secretItems.push({
729
+ requirementKey: requirement.key,
730
+ requirementKind: "secret",
731
+ env: requirement.env,
732
+ sensitivity: requirement.sensitivity,
733
+ source: requirement.source,
734
+ targets: requirement.targets,
735
+ scopes: ["staging", "prod"],
736
+ sourceHostId: inputs[requirement.key]?.hostId ?? null
737
+ });
738
+ }
739
+ }
740
+ for (const [key, input] of Object.entries(inputs)) {
741
+ if (!requirementKeys.has(key)) {
742
+ hostBindings[key] = resolveAdHocBinding(key, input, options, selectedAt);
743
+ }
744
+ }
745
+ return {
746
+ hostBindings,
747
+ compatibility: compatibilityFromBindings(hostBindings),
748
+ configWritePlan,
749
+ secretDeploymentPlan: { items: secretItems },
750
+ diagnostics: []
751
+ };
752
+ }
753
+ export {
754
+ normalizeProjectLaunchHostBindings,
755
+ normalizeTemplateLaunchRequirements,
756
+ parseProjectLaunchHostBindingSpecs,
757
+ resolveProjectLaunchHostBindings,
758
+ validateTemplateLaunchRequirements
759
+ };