@treeseed/sdk 0.8.1 → 0.8.2
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/operations/services/config-runtime.d.ts +2 -1
- package/dist/operations/services/config-runtime.js +21 -1
- package/dist/operations/services/github-automation.d.ts +10 -3
- package/dist/operations/services/github-automation.js +20 -8
- package/dist/operations/services/hosting-audit.d.ts +67 -0
- package/dist/operations/services/hosting-audit.js +642 -0
- package/dist/operations/services/hub-launch.js +2 -2
- package/dist/operations/services/hub-provider-launch.js +4 -4
- package/dist/operations/services/managed-host-security.d.ts +13 -0
- package/dist/operations/services/managed-host-security.js +53 -0
- package/dist/platform/env.yaml +49 -0
- package/dist/reconcile/builtin-adapters.js +5 -4
- package/dist/workflow/operations.d.ts +6 -0
- package/dist/workflow/operations.js +46 -0
- package/dist/workflow-support.d.ts +1 -0
- package/dist/workflow-support.js +8 -0
- package/package.json +1 -1
- package/templates/github/deploy.managed.workflow.yml +208 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
import {
|
|
4
|
+
getTreeseedEnvironmentSuggestedValues,
|
|
5
|
+
validateTreeseedEnvironmentValues
|
|
6
|
+
} from "../../platform/environment.js";
|
|
7
|
+
import {
|
|
8
|
+
collectTreeseedConfigSeedValues,
|
|
9
|
+
collectTreeseedEnvironmentContext,
|
|
10
|
+
checkTreeseedProviderConnections
|
|
11
|
+
} from "./config-runtime.js";
|
|
12
|
+
import {
|
|
13
|
+
buildProvisioningSummary,
|
|
14
|
+
createBranchPreviewDeployTarget,
|
|
15
|
+
createPersistentDeployTarget,
|
|
16
|
+
loadDeployState
|
|
17
|
+
} from "./deploy.js";
|
|
18
|
+
import {
|
|
19
|
+
currentManagedBranch,
|
|
20
|
+
PRODUCTION_BRANCH,
|
|
21
|
+
STAGING_BRANCH
|
|
22
|
+
} from "./git-workflow.js";
|
|
23
|
+
import { loadCliDeployConfig } from "./runtime-tools.js";
|
|
24
|
+
import {
|
|
25
|
+
collectTreeseedReconcileStatus,
|
|
26
|
+
reconcileTreeseedTarget
|
|
27
|
+
} from "../../reconcile/index.js";
|
|
28
|
+
const HOST_KINDS = ["repository", "web", "processing", "email"];
|
|
29
|
+
const HOST_GROUPS = {
|
|
30
|
+
repository: /* @__PURE__ */ new Set(["auth", "github"]),
|
|
31
|
+
web: /* @__PURE__ */ new Set(["auth", "cloudflare", "hosting"]),
|
|
32
|
+
processing: /* @__PURE__ */ new Set(["auth", "railway", "hosting"]),
|
|
33
|
+
email: /* @__PURE__ */ new Set(["smtp"])
|
|
34
|
+
};
|
|
35
|
+
function hasValue(value) {
|
|
36
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
function firstValue(values, keys) {
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const value = values[key];
|
|
41
|
+
if (hasValue(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
function nonEmptyEnvironmentValues(env = process.env) {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
Object.entries(env).filter(([, value]) => typeof value === "string" && value.trim().length > 0).map(([key, value]) => [key, String(value)])
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function normalizeHostKinds(hostKinds) {
|
|
53
|
+
const selected = Array.isArray(hostKinds) && hostKinds.length > 0 ? hostKinds : HOST_KINDS;
|
|
54
|
+
const normalized = selected.map((kind) => String(kind).trim()).filter((kind) => HOST_KINDS.includes(kind));
|
|
55
|
+
return normalized.length > 0 ? [...new Set(normalized)] : HOST_KINDS;
|
|
56
|
+
}
|
|
57
|
+
function targetLabel(target) {
|
|
58
|
+
return target.kind === "branch" ? `preview:${target.branchName}` : target.scope;
|
|
59
|
+
}
|
|
60
|
+
function serializeTarget(target) {
|
|
61
|
+
return {
|
|
62
|
+
kind: target.kind,
|
|
63
|
+
...target.kind === "branch" ? { branchName: target.branchName } : { scope: target.scope },
|
|
64
|
+
label: targetLabel(target)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveTreeseedHostingAuditTarget({
|
|
68
|
+
tenantRoot,
|
|
69
|
+
environment = "current"
|
|
70
|
+
}) {
|
|
71
|
+
if (environment === "local") {
|
|
72
|
+
return {
|
|
73
|
+
environment: "local",
|
|
74
|
+
scope: "local",
|
|
75
|
+
target: createPersistentDeployTarget("staging"),
|
|
76
|
+
branchName: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (environment === "staging") {
|
|
80
|
+
return {
|
|
81
|
+
environment: "staging",
|
|
82
|
+
scope: "staging",
|
|
83
|
+
target: createPersistentDeployTarget("staging"),
|
|
84
|
+
branchName: null
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (environment === "prod") {
|
|
88
|
+
return {
|
|
89
|
+
environment: "prod",
|
|
90
|
+
scope: "prod",
|
|
91
|
+
target: createPersistentDeployTarget("prod"),
|
|
92
|
+
branchName: null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const branchName = currentManagedBranch(tenantRoot);
|
|
96
|
+
if (branchName === PRODUCTION_BRANCH) {
|
|
97
|
+
return {
|
|
98
|
+
environment: "prod",
|
|
99
|
+
scope: "prod",
|
|
100
|
+
target: createPersistentDeployTarget("prod"),
|
|
101
|
+
branchName
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (branchName === STAGING_BRANCH) {
|
|
105
|
+
return {
|
|
106
|
+
environment: "staging",
|
|
107
|
+
scope: "staging",
|
|
108
|
+
target: createPersistentDeployTarget("staging"),
|
|
109
|
+
branchName
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (branchName) {
|
|
113
|
+
try {
|
|
114
|
+
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
115
|
+
const previewTarget = createBranchPreviewDeployTarget(branchName);
|
|
116
|
+
const previewState = loadDeployState(tenantRoot, deployConfig, { target: previewTarget });
|
|
117
|
+
if (previewState?.previewEnabled === true || previewState?.readiness?.initialized === true || hasValue(previewState?.lastDeployedUrl) || hasValue(previewState?.workerName)) {
|
|
118
|
+
return {
|
|
119
|
+
environment: "preview",
|
|
120
|
+
scope: "staging",
|
|
121
|
+
target: previewTarget,
|
|
122
|
+
branchName
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
environment: "staging",
|
|
130
|
+
scope: "staging",
|
|
131
|
+
target: createPersistentDeployTarget("staging"),
|
|
132
|
+
branchName
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function normalizeAuditValues(values) {
|
|
136
|
+
const normalized = { ...values };
|
|
137
|
+
const githubToken = normalized.TREESEED_HOSTED_HUBS_GITHUB_TOKEN;
|
|
138
|
+
if (githubToken) {
|
|
139
|
+
normalized.GH_TOKEN = githubToken;
|
|
140
|
+
normalized.GITHUB_TOKEN = githubToken;
|
|
141
|
+
}
|
|
142
|
+
const cloudflareToken = normalized.CLOUDFLARE_API_TOKEN;
|
|
143
|
+
if (cloudflareToken) {
|
|
144
|
+
normalized.CLOUDFLARE_API_TOKEN = cloudflareToken;
|
|
145
|
+
}
|
|
146
|
+
const cloudflareAccount = normalized.CLOUDFLARE_ACCOUNT_ID;
|
|
147
|
+
if (cloudflareAccount) {
|
|
148
|
+
normalized.CLOUDFLARE_ACCOUNT_ID = cloudflareAccount;
|
|
149
|
+
}
|
|
150
|
+
const railwayToken = normalized.RAILWAY_API_TOKEN;
|
|
151
|
+
if (railwayToken) {
|
|
152
|
+
normalized.RAILWAY_API_TOKEN = railwayToken;
|
|
153
|
+
}
|
|
154
|
+
const railwayWorkspace = normalized.TREESEED_RAILWAY_WORKSPACE;
|
|
155
|
+
if (railwayWorkspace) {
|
|
156
|
+
normalized.TREESEED_RAILWAY_WORKSPACE = railwayWorkspace;
|
|
157
|
+
}
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
function configCheck({
|
|
161
|
+
id,
|
|
162
|
+
hostType,
|
|
163
|
+
provider,
|
|
164
|
+
status,
|
|
165
|
+
severity,
|
|
166
|
+
summary,
|
|
167
|
+
detail,
|
|
168
|
+
remediation
|
|
169
|
+
}) {
|
|
170
|
+
return {
|
|
171
|
+
id,
|
|
172
|
+
hostType,
|
|
173
|
+
provider,
|
|
174
|
+
category: "config",
|
|
175
|
+
status,
|
|
176
|
+
severity,
|
|
177
|
+
summary,
|
|
178
|
+
...detail ? { detail } : {},
|
|
179
|
+
...remediation ? { remediation } : {}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function requiredKeyCheck(checks, values, {
|
|
183
|
+
id,
|
|
184
|
+
hostType,
|
|
185
|
+
provider,
|
|
186
|
+
keys,
|
|
187
|
+
label,
|
|
188
|
+
remediation
|
|
189
|
+
}) {
|
|
190
|
+
const configured = firstValue(values, keys);
|
|
191
|
+
checks.push(configCheck({
|
|
192
|
+
id,
|
|
193
|
+
hostType,
|
|
194
|
+
provider,
|
|
195
|
+
status: configured ? "passed" : "failed",
|
|
196
|
+
severity: configured ? "info" : "critical",
|
|
197
|
+
summary: configured ? `${label} is configured.` : `${label} is missing.`,
|
|
198
|
+
detail: configured ? void 0 : `Expected one of: ${keys.join(", ")}.`,
|
|
199
|
+
remediation
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
function appendManualConfigChecks(checks, values, hostKinds) {
|
|
203
|
+
if (hostKinds.includes("repository")) {
|
|
204
|
+
requiredKeyCheck(checks, values, {
|
|
205
|
+
id: "repository.github.owner",
|
|
206
|
+
hostType: "repository",
|
|
207
|
+
provider: "github",
|
|
208
|
+
keys: ["TREESEED_HOSTED_HUBS_GITHUB_OWNER"],
|
|
209
|
+
label: "Repository owner or organization",
|
|
210
|
+
remediation: "Set TREESEED_HOSTED_HUBS_GITHUB_OWNER for TreeSeed-managed hosted repositories."
|
|
211
|
+
});
|
|
212
|
+
requiredKeyCheck(checks, values, {
|
|
213
|
+
id: "repository.github.token",
|
|
214
|
+
hostType: "repository",
|
|
215
|
+
provider: "github",
|
|
216
|
+
keys: ["TREESEED_HOSTED_HUBS_GITHUB_TOKEN"],
|
|
217
|
+
label: "Repository provider token",
|
|
218
|
+
remediation: "Set TREESEED_HOSTED_HUBS_GITHUB_TOKEN for TreeSeed-managed hosted repositories, or provide a team-owned Repository Host session."
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (hostKinds.includes("web")) {
|
|
222
|
+
requiredKeyCheck(checks, values, {
|
|
223
|
+
id: "web.cloudflare.token",
|
|
224
|
+
hostType: "web",
|
|
225
|
+
provider: "cloudflare",
|
|
226
|
+
keys: ["CLOUDFLARE_API_TOKEN"],
|
|
227
|
+
label: "Web provider token",
|
|
228
|
+
remediation: "Set CLOUDFLARE_API_TOKEN for TreeSeed-managed Web hosting."
|
|
229
|
+
});
|
|
230
|
+
requiredKeyCheck(checks, values, {
|
|
231
|
+
id: "web.cloudflare.account",
|
|
232
|
+
hostType: "web",
|
|
233
|
+
provider: "cloudflare",
|
|
234
|
+
keys: ["CLOUDFLARE_ACCOUNT_ID"],
|
|
235
|
+
label: "Web provider account",
|
|
236
|
+
remediation: "Set CLOUDFLARE_ACCOUNT_ID for TreeSeed-managed Web hosting."
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (hostKinds.includes("processing")) {
|
|
240
|
+
requiredKeyCheck(checks, values, {
|
|
241
|
+
id: "processing.railway.token",
|
|
242
|
+
hostType: "processing",
|
|
243
|
+
provider: "railway",
|
|
244
|
+
keys: ["RAILWAY_API_TOKEN"],
|
|
245
|
+
label: "Processing provider token",
|
|
246
|
+
remediation: "Set RAILWAY_API_TOKEN for TreeSeed-managed Processing hosting."
|
|
247
|
+
});
|
|
248
|
+
requiredKeyCheck(checks, values, {
|
|
249
|
+
id: "processing.railway.workspace",
|
|
250
|
+
hostType: "processing",
|
|
251
|
+
provider: "railway",
|
|
252
|
+
keys: ["TREESEED_RAILWAY_WORKSPACE"],
|
|
253
|
+
label: "Processing provider workspace",
|
|
254
|
+
remediation: "Set TREESEED_RAILWAY_WORKSPACE for TreeSeed-managed Processing hosting."
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (hostKinds.includes("email")) {
|
|
258
|
+
requiredKeyCheck(checks, values, {
|
|
259
|
+
id: "email.smtp.host",
|
|
260
|
+
hostType: "email",
|
|
261
|
+
provider: "smtp",
|
|
262
|
+
keys: ["TREESEED_SMTP_HOST"],
|
|
263
|
+
label: "Email provider host",
|
|
264
|
+
remediation: "Set TREESEED_SMTP_HOST or configure a team-owned Email Host session."
|
|
265
|
+
});
|
|
266
|
+
requiredKeyCheck(checks, values, {
|
|
267
|
+
id: "email.smtp.port",
|
|
268
|
+
hostType: "email",
|
|
269
|
+
provider: "smtp",
|
|
270
|
+
keys: ["TREESEED_SMTP_PORT"],
|
|
271
|
+
label: "Email provider port",
|
|
272
|
+
remediation: "Set TREESEED_SMTP_PORT or configure a team-owned Email Host session."
|
|
273
|
+
});
|
|
274
|
+
requiredKeyCheck(checks, values, {
|
|
275
|
+
id: "email.smtp.from",
|
|
276
|
+
hostType: "email",
|
|
277
|
+
provider: "smtp",
|
|
278
|
+
keys: ["TREESEED_SMTP_FROM"],
|
|
279
|
+
label: "Email sender address",
|
|
280
|
+
remediation: "Set TREESEED_SMTP_FROM to a verified sender address."
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function appendRegistryConfigChecks({
|
|
285
|
+
checks,
|
|
286
|
+
tenantRoot,
|
|
287
|
+
scope,
|
|
288
|
+
values,
|
|
289
|
+
hostKinds
|
|
290
|
+
}) {
|
|
291
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
292
|
+
const selectedGroups = new Set(hostKinds.flatMap((kind) => [...HOST_GROUPS[kind]]));
|
|
293
|
+
const validation = validateTreeseedEnvironmentValues({
|
|
294
|
+
values,
|
|
295
|
+
scope,
|
|
296
|
+
purpose: "config",
|
|
297
|
+
deployConfig: registry.context.deployConfig,
|
|
298
|
+
tenantConfig: registry.context.tenantConfig,
|
|
299
|
+
plugins: registry.context.plugins
|
|
300
|
+
});
|
|
301
|
+
const selectedProblems = [...validation.missing, ...validation.invalid].filter((problem) => selectedGroups.has(problem.entry.group));
|
|
302
|
+
for (const problem of selectedProblems) {
|
|
303
|
+
const hostType = hostKinds.find((kind) => HOST_GROUPS[kind].has(problem.entry.group)) ?? "platform";
|
|
304
|
+
checks.push(configCheck({
|
|
305
|
+
id: `config.${problem.id}`,
|
|
306
|
+
hostType,
|
|
307
|
+
provider: problem.entry.group,
|
|
308
|
+
status: "failed",
|
|
309
|
+
severity: problem.entry.requirement === "optional" ? "warning" : "critical",
|
|
310
|
+
summary: `${problem.label} is ${problem.reason}.`,
|
|
311
|
+
detail: problem.message,
|
|
312
|
+
remediation: problem.entry.howToGet
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
if (selectedProblems.length === 0) {
|
|
316
|
+
checks.push(configCheck({
|
|
317
|
+
id: "config.registry",
|
|
318
|
+
hostType: "platform",
|
|
319
|
+
provider: "treeseed",
|
|
320
|
+
status: "passed",
|
|
321
|
+
severity: "info",
|
|
322
|
+
summary: "Registered hosting environment values are complete."
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function providerConnectionChecks(report, hostKinds) {
|
|
327
|
+
const allowedProviders = /* @__PURE__ */ new Set();
|
|
328
|
+
if (hostKinds.includes("repository")) allowedProviders.add("github");
|
|
329
|
+
if (hostKinds.includes("web")) allowedProviders.add("cloudflare");
|
|
330
|
+
if (hostKinds.includes("processing")) allowedProviders.add("railway");
|
|
331
|
+
return report.checks.filter((check) => allowedProviders.has(check.provider)).map((check) => {
|
|
332
|
+
const hostType = check.provider === "github" ? "repository" : check.provider === "railway" ? "processing" : "web";
|
|
333
|
+
return {
|
|
334
|
+
id: `identity.${check.provider}`,
|
|
335
|
+
hostType,
|
|
336
|
+
provider: check.provider,
|
|
337
|
+
category: "identity",
|
|
338
|
+
status: check.ready ? "passed" : check.skipped ? "skipped" : "failed",
|
|
339
|
+
severity: check.ready || check.skipped ? "info" : "critical",
|
|
340
|
+
summary: check.ready ? `${check.provider} identity check passed.` : check.skipped ? `${check.provider} identity check skipped.` : `${check.provider} identity check failed.`,
|
|
341
|
+
detail: check.detail,
|
|
342
|
+
repairAvailable: false,
|
|
343
|
+
remediation: check.ready ? void 0 : `Confirm ${check.provider} credentials and provider CLI access for this environment.`
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function reconcileSystemsForHostKinds(hostKinds) {
|
|
348
|
+
const systems = /* @__PURE__ */ new Set();
|
|
349
|
+
if (hostKinds.includes("repository")) systems.add("github");
|
|
350
|
+
if (hostKinds.includes("web")) {
|
|
351
|
+
systems.add("data");
|
|
352
|
+
systems.add("web");
|
|
353
|
+
}
|
|
354
|
+
if (hostKinds.includes("processing")) {
|
|
355
|
+
systems.add("api");
|
|
356
|
+
systems.add("agents");
|
|
357
|
+
}
|
|
358
|
+
return [...systems];
|
|
359
|
+
}
|
|
360
|
+
function reconcileStatusChecks(status, repairedIds = /* @__PURE__ */ new Set()) {
|
|
361
|
+
const checks = [];
|
|
362
|
+
for (const unit of status.units) {
|
|
363
|
+
const hostType = unit.provider === "github" ? "repository" : unit.provider === "railway" ? "processing" : "web";
|
|
364
|
+
const verified = unit.verification?.verified === true;
|
|
365
|
+
const id = `resource.${unit.provider}.${unit.unitType}.${unit.unitId}`;
|
|
366
|
+
const repaired = repairedIds.has(id);
|
|
367
|
+
checks.push({
|
|
368
|
+
id,
|
|
369
|
+
hostType,
|
|
370
|
+
provider: unit.provider,
|
|
371
|
+
category: "resource",
|
|
372
|
+
status: repaired ? "repaired" : verified ? "passed" : "failed",
|
|
373
|
+
severity: verified || repaired ? "info" : "critical",
|
|
374
|
+
summary: `${unit.provider}:${unit.unitType} ${verified || repaired ? "is ready" : "is not ready"}.`,
|
|
375
|
+
detail: [
|
|
376
|
+
unit.exists === false ? "Resource is missing." : null,
|
|
377
|
+
unit.verification?.missing?.length ? `missing: ${unit.verification.missing.join(", ")}` : null,
|
|
378
|
+
unit.verification?.drifted?.length ? `drifted: ${unit.verification.drifted.join(", ")}` : null,
|
|
379
|
+
unit.warnings?.length ? `warnings: ${unit.warnings.join("; ")}` : null
|
|
380
|
+
].filter(Boolean).join(" ") || void 0,
|
|
381
|
+
resourceRef: unit.locators?.length ? unit.locators.join(", ") : unit.unitId,
|
|
382
|
+
repairAvailable: true,
|
|
383
|
+
repaired,
|
|
384
|
+
remediation: verified || repaired ? void 0 : "Run trsd audit hosting --repair to reconcile platform resources."
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return checks;
|
|
388
|
+
}
|
|
389
|
+
async function checkSmtpReachability(values) {
|
|
390
|
+
const host = values.TREESEED_SMTP_HOST;
|
|
391
|
+
const port = Number(values.TREESEED_SMTP_PORT ?? 0);
|
|
392
|
+
const secure = /^(true|1|tls|ssl|465)$/iu.test(String(values.TREESEED_SMTP_SECURE ?? ""));
|
|
393
|
+
if (!host || !Number.isFinite(port) || port <= 0) {
|
|
394
|
+
return {
|
|
395
|
+
id: "connectivity.smtp",
|
|
396
|
+
hostType: "email",
|
|
397
|
+
provider: "smtp",
|
|
398
|
+
category: "connectivity",
|
|
399
|
+
status: "skipped",
|
|
400
|
+
severity: "warning",
|
|
401
|
+
summary: "Email connectivity check skipped.",
|
|
402
|
+
detail: "SMTP host and port are required before TreeSeed can test connectivity.",
|
|
403
|
+
remediation: "Configure SMTP host and port, then rerun the hosting audit."
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return new Promise((resolve) => {
|
|
407
|
+
const socket = secure ? tls.connect({ host, port, servername: host, timeout: 5e3 }) : net.connect({ host, port, timeout: 5e3 });
|
|
408
|
+
let settled = false;
|
|
409
|
+
const finish = (check) => {
|
|
410
|
+
if (settled) return;
|
|
411
|
+
settled = true;
|
|
412
|
+
socket.destroy();
|
|
413
|
+
resolve(check);
|
|
414
|
+
};
|
|
415
|
+
socket.once("connect", () => finish({
|
|
416
|
+
id: "connectivity.smtp",
|
|
417
|
+
hostType: "email",
|
|
418
|
+
provider: "smtp",
|
|
419
|
+
category: "connectivity",
|
|
420
|
+
status: "passed",
|
|
421
|
+
severity: "info",
|
|
422
|
+
summary: "Email provider accepts a TCP connection.",
|
|
423
|
+
resourceRef: `${host}:${port}`
|
|
424
|
+
}));
|
|
425
|
+
socket.once("secureConnect", () => finish({
|
|
426
|
+
id: "connectivity.smtp",
|
|
427
|
+
hostType: "email",
|
|
428
|
+
provider: "smtp",
|
|
429
|
+
category: "connectivity",
|
|
430
|
+
status: "passed",
|
|
431
|
+
severity: "info",
|
|
432
|
+
summary: "Email provider accepts a TLS connection.",
|
|
433
|
+
resourceRef: `${host}:${port}`
|
|
434
|
+
}));
|
|
435
|
+
socket.once("timeout", () => finish({
|
|
436
|
+
id: "connectivity.smtp",
|
|
437
|
+
hostType: "email",
|
|
438
|
+
provider: "smtp",
|
|
439
|
+
category: "connectivity",
|
|
440
|
+
status: "failed",
|
|
441
|
+
severity: "critical",
|
|
442
|
+
summary: "Email provider connection timed out.",
|
|
443
|
+
resourceRef: `${host}:${port}`,
|
|
444
|
+
remediation: "Confirm SMTP host, port, firewall, and TLS mode."
|
|
445
|
+
}));
|
|
446
|
+
socket.once("error", (error) => finish({
|
|
447
|
+
id: "connectivity.smtp",
|
|
448
|
+
hostType: "email",
|
|
449
|
+
provider: "smtp",
|
|
450
|
+
category: "connectivity",
|
|
451
|
+
status: "failed",
|
|
452
|
+
severity: "critical",
|
|
453
|
+
summary: "Email provider connection failed.",
|
|
454
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
455
|
+
resourceRef: `${host}:${port}`,
|
|
456
|
+
remediation: "Confirm SMTP host, port, firewall, and TLS mode."
|
|
457
|
+
}));
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function summarizeReport(checks) {
|
|
461
|
+
const blockers = checks.filter((check) => check.status === "failed" && check.severity === "critical").map((check) => check.detail ? `${check.summary} ${check.detail}` : check.summary);
|
|
462
|
+
const warnings = checks.filter((check) => check.status === "warning" || check.status === "failed" && check.severity === "warning").map((check) => check.detail ? `${check.summary} ${check.detail}` : check.summary);
|
|
463
|
+
const missingConfig = checks.filter((check) => check.category === "config" && check.status === "failed").flatMap((check) => {
|
|
464
|
+
const detail = check.detail ?? "";
|
|
465
|
+
const match = detail.match(/Expected one of: ([^.]+)\./u);
|
|
466
|
+
const keys = match?.[1]?.split(",").map((key) => key.trim()).filter(Boolean) ?? [];
|
|
467
|
+
return keys.length > 0 ? keys.map((key) => ({
|
|
468
|
+
key,
|
|
469
|
+
hostType: check.hostType,
|
|
470
|
+
severity: check.severity,
|
|
471
|
+
summary: check.summary
|
|
472
|
+
})) : [{
|
|
473
|
+
key: check.id.replace(/^config\./u, ""),
|
|
474
|
+
hostType: check.hostType,
|
|
475
|
+
severity: check.severity,
|
|
476
|
+
summary: check.summary
|
|
477
|
+
}];
|
|
478
|
+
});
|
|
479
|
+
const nextActions = blockers.length > 0 ? [...new Set(checks.filter((check) => check.status === "failed").map((check) => check.remediation).filter((value) => typeof value === "string" && value.trim().length > 0))] : ["Hosting setup is ready for host saving and project launch."];
|
|
480
|
+
return { blockers, warnings, missingConfig, nextActions };
|
|
481
|
+
}
|
|
482
|
+
async function runTreeseedHostingAudit({
|
|
483
|
+
tenantRoot,
|
|
484
|
+
environment = "current",
|
|
485
|
+
repair = false,
|
|
486
|
+
env = process.env,
|
|
487
|
+
valuesOverlay = {},
|
|
488
|
+
hostKinds: requestedHostKinds,
|
|
489
|
+
write
|
|
490
|
+
}) {
|
|
491
|
+
const resolved = resolveTreeseedHostingAuditTarget({ tenantRoot, environment });
|
|
492
|
+
const hostKinds = normalizeHostKinds(requestedHostKinds);
|
|
493
|
+
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
494
|
+
const seedValues = collectTreeseedConfigSeedValues(tenantRoot, resolved.scope, env, valuesOverlay);
|
|
495
|
+
const suggestedValues = getTreeseedEnvironmentSuggestedValues({
|
|
496
|
+
scope: resolved.scope,
|
|
497
|
+
purpose: "config",
|
|
498
|
+
deployConfig,
|
|
499
|
+
values: {
|
|
500
|
+
...seedValues,
|
|
501
|
+
...nonEmptyEnvironmentValues(env),
|
|
502
|
+
...nonEmptyEnvironmentValues(valuesOverlay)
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const values = normalizeAuditValues({
|
|
506
|
+
...suggestedValues,
|
|
507
|
+
...seedValues,
|
|
508
|
+
...nonEmptyEnvironmentValues(env),
|
|
509
|
+
...nonEmptyEnvironmentValues(valuesOverlay)
|
|
510
|
+
});
|
|
511
|
+
const checks = [];
|
|
512
|
+
appendManualConfigChecks(checks, values, hostKinds);
|
|
513
|
+
appendRegistryConfigChecks({ checks, tenantRoot, scope: resolved.scope, values, hostKinds });
|
|
514
|
+
const connectionReport = await checkTreeseedProviderConnections({
|
|
515
|
+
tenantRoot,
|
|
516
|
+
scope: resolved.scope,
|
|
517
|
+
env: values,
|
|
518
|
+
valuesOverlay: values
|
|
519
|
+
});
|
|
520
|
+
checks.push(...providerConnectionChecks(connectionReport, hostKinds));
|
|
521
|
+
if (hostKinds.includes("email")) {
|
|
522
|
+
checks.push(await checkSmtpReachability(values));
|
|
523
|
+
}
|
|
524
|
+
const systems = reconcileSystemsForHostKinds(hostKinds);
|
|
525
|
+
let resources = {};
|
|
526
|
+
let repaired = false;
|
|
527
|
+
if (resolved.environment !== "local" && systems.length > 0) {
|
|
528
|
+
try {
|
|
529
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target: resolved.target });
|
|
530
|
+
resources = buildProvisioningSummary(deployConfig, state, resolved.target);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
checks.push({
|
|
533
|
+
id: "resources.summary",
|
|
534
|
+
hostType: "platform",
|
|
535
|
+
provider: "treeseed",
|
|
536
|
+
category: "resource",
|
|
537
|
+
status: "warning",
|
|
538
|
+
severity: "warning",
|
|
539
|
+
summary: "Unable to load provisioning summary.",
|
|
540
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
const beforeStatus = await collectTreeseedReconcileStatus({
|
|
545
|
+
tenantRoot,
|
|
546
|
+
target: resolved.target,
|
|
547
|
+
env: values,
|
|
548
|
+
systems
|
|
549
|
+
});
|
|
550
|
+
let repairedIds = /* @__PURE__ */ new Set();
|
|
551
|
+
if (repair && beforeStatus.ready !== true) {
|
|
552
|
+
write?.("[audit][hosting] Repair mode enabled; reconciling platform provider resources.");
|
|
553
|
+
const repairResult = await reconcileTreeseedTarget({
|
|
554
|
+
tenantRoot,
|
|
555
|
+
target: resolved.target,
|
|
556
|
+
env: values,
|
|
557
|
+
systems,
|
|
558
|
+
write
|
|
559
|
+
});
|
|
560
|
+
repaired = repairResult.results.some((result) => result.action !== "none");
|
|
561
|
+
const afterStatus = await collectTreeseedReconcileStatus({
|
|
562
|
+
tenantRoot,
|
|
563
|
+
target: resolved.target,
|
|
564
|
+
env: values,
|
|
565
|
+
systems
|
|
566
|
+
});
|
|
567
|
+
repairedIds = new Set(afterStatus.units.filter((unit) => unit.verification?.verified === true).map((unit) => `resource.${unit.provider}.${unit.unitType}.${unit.unitId}`));
|
|
568
|
+
checks.push(...reconcileStatusChecks(afterStatus, repairedIds));
|
|
569
|
+
} else {
|
|
570
|
+
checks.push(...reconcileStatusChecks(beforeStatus));
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
checks.push({
|
|
574
|
+
id: "resources.reconcile-status",
|
|
575
|
+
hostType: "platform",
|
|
576
|
+
provider: "treeseed",
|
|
577
|
+
category: "resource",
|
|
578
|
+
status: "failed",
|
|
579
|
+
severity: "critical",
|
|
580
|
+
summary: repair ? "Hosting resource repair failed." : "Hosting resource readiness check failed.",
|
|
581
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
582
|
+
repairAvailable: !repair,
|
|
583
|
+
remediation: repair ? "Inspect provider credentials and rerun the audit." : "Run trsd audit hosting --repair after fixing provider credentials."
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} else if (resolved.environment === "local") {
|
|
587
|
+
checks.push({
|
|
588
|
+
id: "resources.local",
|
|
589
|
+
hostType: "platform",
|
|
590
|
+
provider: "treeseed",
|
|
591
|
+
category: "resource",
|
|
592
|
+
status: "skipped",
|
|
593
|
+
severity: "info",
|
|
594
|
+
summary: "Hosted provider resource checks are skipped for local audits."
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const summary = summarizeReport(checks);
|
|
598
|
+
return {
|
|
599
|
+
ok: summary.blockers.length === 0,
|
|
600
|
+
environment: resolved.environment,
|
|
601
|
+
requestedEnvironment: environment,
|
|
602
|
+
repairMode: repair,
|
|
603
|
+
repaired,
|
|
604
|
+
target: serializeTarget(resolved.target),
|
|
605
|
+
hostKinds,
|
|
606
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
607
|
+
checks,
|
|
608
|
+
missingConfig: summary.missingConfig,
|
|
609
|
+
resources,
|
|
610
|
+
warnings: summary.warnings,
|
|
611
|
+
blockers: summary.blockers,
|
|
612
|
+
nextActions: summary.nextActions
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function formatTreeseedHostingAuditReport(report) {
|
|
616
|
+
const lines = [
|
|
617
|
+
`Treeseed hosting audit (${report.environment}, ${report.repairMode ? "repair" : "read-only"})`,
|
|
618
|
+
`Status: ${report.ok ? "ready" : "blocked"}`,
|
|
619
|
+
`Target: ${report.target.label}`,
|
|
620
|
+
"",
|
|
621
|
+
"Checks:",
|
|
622
|
+
...report.checks.map((check) => {
|
|
623
|
+
const status = check.status.toUpperCase();
|
|
624
|
+
const resource = check.resourceRef ? ` [${check.resourceRef}]` : "";
|
|
625
|
+
const detail = check.detail ? ` ${check.detail}` : "";
|
|
626
|
+
return ` - ${status} ${check.hostType}/${check.provider}/${check.category}: ${check.summary}${resource}${detail}`;
|
|
627
|
+
})
|
|
628
|
+
];
|
|
629
|
+
if (report.blockers.length > 0) {
|
|
630
|
+
lines.push("", "Blockers:", ...report.blockers.map((blocker) => ` - ${blocker}`));
|
|
631
|
+
}
|
|
632
|
+
if (report.warnings.length > 0) {
|
|
633
|
+
lines.push("", "Warnings:", ...report.warnings.map((warning) => ` - ${warning}`));
|
|
634
|
+
}
|
|
635
|
+
lines.push("", "Next actions:", ...report.nextActions.map((action) => ` - ${action}`));
|
|
636
|
+
return lines.join("\n");
|
|
637
|
+
}
|
|
638
|
+
export {
|
|
639
|
+
formatTreeseedHostingAuditReport,
|
|
640
|
+
resolveTreeseedHostingAuditTarget,
|
|
641
|
+
runTreeseedHostingAudit
|
|
642
|
+
};
|
|
@@ -63,7 +63,7 @@ function defaultHubContentResolutionPolicy() {
|
|
|
63
63
|
function planKnowledgeHubRepositories(intent, host) {
|
|
64
64
|
const normalized = normalizeKnowledgeHubLaunchIntent(intent);
|
|
65
65
|
const hubSlug = normalized.hub.slug;
|
|
66
|
-
const owner = normalized.repository?.owner ?? host?.organizationOrOwner ?? process.env.TREESEED_HOSTED_HUBS_GITHUB_OWNER ??
|
|
66
|
+
const owner = normalized.repository?.owner ?? host?.organizationOrOwner ?? process.env.TREESEED_HOSTED_HUBS_GITHUB_OWNER ?? "treeseed-sites";
|
|
67
67
|
const topology = normalized.repository?.topology ?? "split_software_content";
|
|
68
68
|
const visibility = normalized.repository?.visibility ?? host?.defaultVisibility ?? "private";
|
|
69
69
|
if (topology === "combined_compatibility") {
|
|
@@ -154,7 +154,7 @@ function validateRepositoryHost(host) {
|
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
async function createKnowledgeHubRepositories(input) {
|
|
157
|
-
const githubToken = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN ||
|
|
157
|
+
const githubToken = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || "";
|
|
158
158
|
const githubEnv = githubToken ? { ...process.env, GH_TOKEN: githubToken, GITHUB_TOKEN: githubToken } : process.env;
|
|
159
159
|
const created = [];
|
|
160
160
|
for (const repository of input.plan.repositories) {
|
|
@@ -239,7 +239,7 @@ function applyManagedProjectDefaults(projectRoot, input) {
|
|
|
239
239
|
const marketBaseUrl = normalizeBaseUrl(input.marketBaseUrl ?? envOrNull("TREESEED_MARKET_API_BASE_URL") ?? "https://knowledge.coop");
|
|
240
240
|
const siteUrl = resolveManagedWebUrl(slug);
|
|
241
241
|
const projectApiBaseUrl = normalizeBaseUrl(input.projectApiBaseUrl ?? resolveManagedApiUrl(slug));
|
|
242
|
-
const cloudflareAccountId = envOrNull("
|
|
242
|
+
const cloudflareAccountId = envOrNull("CLOUDFLARE_ACCOUNT_ID") ?? "replace-with-cloudflare-account-id";
|
|
243
243
|
const runtimeMode = input.hostingMode === "managed" ? "treeseed_managed" : input.hostingMode === "hybrid" ? "byo_attached" : "none";
|
|
244
244
|
const runtimeRegistration = input.hostingMode === "managed" ? "required" : input.hostingMode === "hybrid" ? "optional" : "none";
|
|
245
245
|
const hubMode = input.hostingMode === "self_hosted" ? "customer_hosted" : "treeseed_hosted";
|
|
@@ -545,7 +545,7 @@ function buildCloudflareHostEnvironmentOverlay(input, scope) {
|
|
|
545
545
|
for (const [key, value] of Object.entries(environmentConfig)) {
|
|
546
546
|
overlayValue(overlay, key, value);
|
|
547
547
|
}
|
|
548
|
-
overlay.CLOUDFLARE_ACCOUNT_ID = overlay.CLOUDFLARE_ACCOUNT_ID ||
|
|
548
|
+
overlay.CLOUDFLARE_ACCOUNT_ID = overlay.CLOUDFLARE_ACCOUNT_ID || "";
|
|
549
549
|
overlayValue(overlay, "TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME", overlay.TREESEED_CLOUDFLARE_PAGES_PROJECT_NAME || projectSlug);
|
|
550
550
|
overlayValue(overlay, "TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME", overlay.TREESEED_CLOUDFLARE_PAGES_PREVIEW_PROJECT_NAME || `${projectSlug}-staging`);
|
|
551
551
|
overlayValue(overlay, "TREESEED_CONTENT_BUCKET_NAME", overlay.TREESEED_CONTENT_BUCKET_NAME || `${projectSlug}-content`);
|
|
@@ -600,7 +600,7 @@ function scaffoldKnowledgeCoopSource(projectRoot, input) {
|
|
|
600
600
|
});
|
|
601
601
|
}
|
|
602
602
|
function repositoryHostGitHubEnvOverlay() {
|
|
603
|
-
const token = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN ||
|
|
603
|
+
const token = process.env.TREESEED_HOSTED_HUBS_GITHUB_TOKEN || "";
|
|
604
604
|
return token ? { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token } : process.env;
|
|
605
605
|
}
|
|
606
606
|
function prepareKnowledgeHubContentRepositoryRoot(sourceRoot, contentRoot, input) {
|
|
@@ -656,7 +656,7 @@ async function validateKnowledgeHubProviderLaunchPrerequisites(tenantRoot = proc
|
|
|
656
656
|
["TREESEED_API_WEB_SERVICE_ID"],
|
|
657
657
|
["TREESEED_API_WEB_SERVICE_SECRET"],
|
|
658
658
|
["TREESEED_API_WEB_ASSERTION_SECRET"],
|
|
659
|
-
["CLOUDFLARE_ACCOUNT_ID"
|
|
659
|
+
["CLOUDFLARE_ACCOUNT_ID"]
|
|
660
660
|
];
|
|
661
661
|
const missingConfig = requiredConfig.filter((group) => !group.some((name) => {
|
|
662
662
|
const value = values[name];
|