@wildo-ai/wildo-module-client 1.1.1
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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/lockfile-repair.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-adopter.service.test.js.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts +0 -0
- package/dist/esm/__tests__/module-packager.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/module-packager.service.test.js.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-materialization-hook.test.js.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.d.ts.map +0 -0
- package/dist/esm/__tests__/provider-sync-runner.test.js.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-lockfile.service.test.js.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts +0 -0
- package/dist/esm/__tests__/registry-server-client.test.d.ts.map +0 -0
- package/dist/esm/__tests__/registry-server-client.test.js.map +0 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/services/_http-client.d.ts +44 -0
- package/dist/esm/services/_http-client.d.ts.map +1 -0
- package/dist/esm/services/_http-client.js +75 -0
- package/dist/esm/services/_http-client.js.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts +12 -0
- package/dist/esm/services/_zod-issue-formatting.d.ts.map +1 -0
- package/dist/esm/services/_zod-issue-formatting.js +47 -0
- package/dist/esm/services/_zod-issue-formatting.js.map +1 -0
- package/dist/esm/services/index.d.ts +10 -0
- package/dist/esm/services/index.d.ts.map +1 -0
- package/dist/esm/services/index.js +10 -0
- package/dist/esm/services/index.js.map +1 -0
- package/dist/esm/services/lockfile-repair.service.d.ts +68 -0
- package/dist/esm/services/lockfile-repair.service.d.ts.map +1 -0
- package/dist/esm/services/lockfile-repair.service.js +160 -0
- package/dist/esm/services/lockfile-repair.service.js.map +1 -0
- package/dist/esm/services/module-adopter.service.d.ts +59 -0
- package/dist/esm/services/module-adopter.service.d.ts.map +1 -0
- package/dist/esm/services/module-adopter.service.js +126 -0
- package/dist/esm/services/module-adopter.service.js.map +1 -0
- package/dist/esm/services/module-packager.service.d.ts +105 -0
- package/dist/esm/services/module-packager.service.d.ts.map +1 -0
- package/dist/esm/services/module-packager.service.js +212 -0
- package/dist/esm/services/module-packager.service.js.map +1 -0
- package/dist/esm/services/provider-materialization-hook.d.ts +32 -0
- package/dist/esm/services/provider-materialization-hook.d.ts.map +1 -0
- package/dist/esm/services/provider-materialization-hook.js +33 -0
- package/dist/esm/services/provider-materialization-hook.js.map +1 -0
- package/dist/esm/services/provider-sync-runner.d.ts +87 -0
- package/dist/esm/services/provider-sync-runner.d.ts.map +1 -0
- package/dist/esm/services/provider-sync-runner.js +207 -0
- package/dist/esm/services/provider-sync-runner.js.map +1 -0
- package/dist/esm/services/registry-companion-client.d.ts +39 -0
- package/dist/esm/services/registry-companion-client.d.ts.map +1 -0
- package/dist/esm/services/registry-companion-client.js +109 -0
- package/dist/esm/services/registry-companion-client.js.map +1 -0
- package/dist/esm/services/registry-lockfile.service.d.ts +56 -0
- package/dist/esm/services/registry-lockfile.service.d.ts.map +1 -0
- package/dist/esm/services/registry-lockfile.service.js +99 -0
- package/dist/esm/services/registry-lockfile.service.js.map +1 -0
- package/dist/esm/services/registry-server-client.d.ts +94 -0
- package/dist/esm/services/registry-server-client.d.ts.map +1 -0
- package/dist/esm/services/registry-server-client.js +225 -0
- package/dist/esm/services/registry-server-client.js.map +1 -0
- package/dist/esm/services/types.d.ts +22 -0
- package/dist/esm/services/types.d.ts.map +1 -0
- package/dist/esm/services/types.js +16 -0
- package/dist/esm/services/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +51 -0
- package/src/__tests__/lockfile-repair.service.test.ts +106 -0
- package/src/__tests__/module-adopter.service.test.ts +96 -0
- package/src/__tests__/module-packager.service.test.ts +258 -0
- package/src/__tests__/provider-materialization-hook.test.ts +47 -0
- package/src/__tests__/provider-sync-runner.test.ts +113 -0
- package/src/__tests__/registry-lockfile.service.test.ts +134 -0
- package/src/__tests__/registry-server-client.test.ts +312 -0
- package/src/index.ts +18 -0
- package/src/services/_http-client.ts +91 -0
- package/src/services/_zod-issue-formatting.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/lockfile-repair.service.ts +215 -0
- package/src/services/module-adopter.service.ts +155 -0
- package/src/services/module-packager.service.ts +251 -0
- package/src/services/provider-materialization-hook.ts +54 -0
- package/src/services/provider-sync-runner.ts +298 -0
- package/src/services/registry-companion-client.ts +163 -0
- package/src/services/registry-lockfile.service.ts +122 -0
- package/src/services/registry-server-client.ts +334 -0
- package/src/services/types.ts +22 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile-repair.service.js","sourceRoot":"","sources":["../../../../src/services/lockfile-repair.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `LockfileRepairService` — Phase 2 read-only lockfile diagnostics.\n *\n * Backs `wildo registry repair-lockfile` (CLI command lands in a\n * later step). The service does NOT mutate the lockfile by\n * default — its job is to surface a structured diagnostic the\n * operator can act on:\n * - validate the canonical envelope shape;\n * - validate `sections.modules` against\n * `RegistryModulesLockSectionSchema`;\n * - report which `installed[*].files` are missing on disk\n * (drift between the lockfile and the source tree);\n * - report which `installed[*].artifactDigest` strings are not\n * `Sha256DigestSchema`-shaped (residue from pre-tightening\n * publishes).\n *\n * Mutating repair is a Phase 7 deliverable — surfacing the gaps is\n * what unblocks the operator now.\n *\n * `i18n` validation is intentionally NOT performed here (KD-15 +\n * Review-pass-2 / MEDIUM-5): repair is the wrong place to\n * second-guess companion-owned section semantics. The shared\n * lockfile library STILL validates i18n on the companion-owned\n * read path; this service only owns module diagnostics.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { z } from 'zod';\nimport { formatZodIssuesAsSingleLine } from './_zod-issue-formatting';\nimport {\n RegistryModulesLockSectionSchema,\n Sha256DigestSchema,\n getWildoSaasLockPath,\n type RegistryModulesLockEntry,\n type RegistryModulesLockSection,\n} from '@wildo-ai/platform-config-lib';\n\n/**\n * Permissive envelope schema used by repair only — it accepts any\n * `sections.*` payload without delegating to per-section schemas\n * so the repair flow can isolate \"envelope is malformed\" from\n * \"envelope is fine but modules section is malformed\".\n */\nconst RelaxedEnvelopeSchema = z.strictObject({\n schemaVersion: z.literal(2),\n generatedAt: z.iso.datetime({ offset: true }),\n sections: z.record(z.string(), z.unknown()),\n});\n\nexport type LockfileRepairFinding =\n | {\n severity: 'error';\n kind: 'envelope-malformed';\n message: string;\n detail?: string;\n }\n | {\n severity: 'error';\n kind: 'modules-section-malformed';\n message: string;\n detail?: string;\n }\n | {\n severity: 'warn';\n kind: 'module-file-missing';\n moduleId: string;\n filePath: string;\n }\n | {\n severity: 'warn';\n kind: 'module-digest-not-sha256';\n moduleId: string;\n observedDigest: string;\n };\n\nexport interface LockfileRepairReport {\n /** Absolute path the report covers. */\n lockfilePath: string;\n /** True when no errors were produced (warnings allowed). */\n ok: boolean;\n findings: readonly LockfileRepairFinding[];\n}\n\nasync function fileExists(absPath: string): Promise<boolean> {\n try {\n await fs.access(absPath);\n return true;\n } catch {\n return false;\n }\n}\n\nexport const LockfileRepairService = {\n /** Compute the lockfile path for a saas root. */\n pathFor(saasRoot: string): string {\n return getWildoSaasLockPath(saasRoot);\n },\n\n /**\n * Read the envelope + run every diagnostic. Read-only.\n */\n async inspect(saasRoot: string): Promise<LockfileRepairReport> {\n const lockfilePath = getWildoSaasLockPath(saasRoot);\n const findings: LockfileRepairFinding[] = [];\n\n // Read raw bytes ourselves so we can produce per-section\n // diagnostics: a malformed `modules` section under an\n // otherwise-valid envelope should report `modules-section-malformed`,\n // NOT `envelope-malformed` (the latter would tell the operator\n // to re-author the whole file when only one section is bad).\n let raw: string;\n try {\n raw = await fs.readFile(lockfilePath, 'utf8');\n } catch (err: unknown) {\n const errno = (err as NodeJS.ErrnoException)?.code;\n if (errno === 'ENOENT') {\n // Missing lockfile is \"ok\" from a repair POV.\n return { lockfilePath, ok: true, findings: [] };\n }\n findings.push({\n severity: 'error',\n kind: 'envelope-malformed',\n message: 'Failed to read lockfile bytes.',\n detail: err instanceof Error ? err.message : String(err),\n });\n return { lockfilePath, ok: false, findings };\n }\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(raw);\n } catch (err) {\n findings.push({\n severity: 'error',\n kind: 'envelope-malformed',\n message: 'Lockfile is not valid JSON.',\n detail: err instanceof Error ? err.message : String(err),\n });\n return { lockfilePath, ok: false, findings };\n }\n\n const envelopeParsed = RelaxedEnvelopeSchema.safeParse(parsedJson);\n if (!envelopeParsed.success) {\n findings.push({\n severity: 'error',\n kind: 'envelope-malformed',\n message: 'Top-level envelope failed to parse.',\n detail: formatZodIssuesAsSingleLine(envelopeParsed.error),\n });\n return { lockfilePath, ok: false, findings };\n }\n const envelope = envelopeParsed.data;\n\n const modulesPayload = envelope.sections.modules;\n if (modulesPayload === undefined) {\n // No modules section present — nothing to inspect.\n return { lockfilePath, ok: true, findings };\n }\n\n const parsed = RegistryModulesLockSectionSchema.safeParse(modulesPayload);\n if (!parsed.success) {\n findings.push({\n severity: 'error',\n kind: 'modules-section-malformed',\n message: '`sections.modules` failed schema validation.',\n detail: formatZodIssuesAsSingleLine(parsed.error),\n });\n return { lockfilePath, ok: false, findings };\n }\n const modules: RegistryModulesLockSection = parsed.data;\n\n for (const [moduleId, entry] of Object.entries(modules.installed)) {\n await this.collectEntryFindings(saasRoot, moduleId, entry, findings);\n }\n\n const ok = findings.every((f) => f.severity !== 'error');\n return { lockfilePath, ok, findings };\n },\n\n /**\n * Per-entry diagnostic helper. Exposed so Phase 7 mutating-\n * repair flows can reuse the same predicates.\n */\n async collectEntryFindings(\n saasRoot: string,\n moduleId: string,\n entry: RegistryModulesLockEntry,\n findings: LockfileRepairFinding[],\n ): Promise<void> {\n if (!Sha256DigestSchema.safeParse(entry.artifactDigest).success) {\n findings.push({\n severity: 'warn',\n kind: 'module-digest-not-sha256',\n moduleId,\n observedDigest: entry.artifactDigest,\n });\n }\n for (const filePath of entry.files) {\n const abs = path.resolve(saasRoot, filePath);\n if (!(await fileExists(abs))) {\n findings.push({\n severity: 'warn',\n kind: 'module-file-missing',\n moduleId,\n filePath,\n });\n }\n }\n },\n} as const;\n\n// L6 (third-pass review): re-export removed — barrel-discipline rule\n// says \"Never re-export except in the root `index.ts`.\" Consumers\n// import the symbol directly from `@wildo-ai/platform-config-lib`.\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ModuleAdopterService` — Phase 2 CLI-direct adoption flow.
|
|
3
|
+
*
|
|
4
|
+
* `wildo registry adopt` takes existing app code that fits the
|
|
5
|
+
* Wildo module-artifact shape and prepares it for publication
|
|
6
|
+
* (KD-14). Adoption is intentionally CLI-DIRECT (Review-pass-3 /
|
|
7
|
+
* MEDIUM-G) — it only reads source slices and stages files. AST
|
|
8
|
+
* mutations on the host app's source are NOT part of adopt; if a
|
|
9
|
+
* future flow needs them, they go through the companion HTTP
|
|
10
|
+
* surface like every other source-mutating verb.
|
|
11
|
+
*
|
|
12
|
+
* Service surface:
|
|
13
|
+
* - `discoverModuleFiles(saasRoot, manifest)` — walks each
|
|
14
|
+
* declared `boundaries.*` allow-list and returns the set of
|
|
15
|
+
* files that exist on disk and are within the boundary.
|
|
16
|
+
* - `adoptToStaging(saasRoot, manifest)` — clears + populates
|
|
17
|
+
* `<saasRoot>/.wildo-saas/generated/modules-artifacts/<id>/`
|
|
18
|
+
* with the manifest + every discovered file, then computes
|
|
19
|
+
* the staging digest. Phase 6 plugs this into the publish
|
|
20
|
+
* flow.
|
|
21
|
+
*/
|
|
22
|
+
import type { ModuleManifest } from '@wildo-ai/platform-config-lib';
|
|
23
|
+
import { type StagingPaths } from './module-packager.service';
|
|
24
|
+
export interface AdoptionPlan {
|
|
25
|
+
/** Per-layer file list, relative to saas root. */
|
|
26
|
+
files: readonly string[];
|
|
27
|
+
/** Empty when every declared boundary contributed at least one file. */
|
|
28
|
+
emptyBoundaries: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface AdoptionResult {
|
|
31
|
+
manifest: ModuleManifest;
|
|
32
|
+
staging: StagingPaths;
|
|
33
|
+
/** Files that were copied into the staging root. */
|
|
34
|
+
staged: readonly string[];
|
|
35
|
+
/** sha256 digest of the staged tree (matches `Sha256DigestSchema`). */
|
|
36
|
+
digest: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const ModuleAdopterService: {
|
|
39
|
+
/**
|
|
40
|
+
* Walk every declared `boundaries.*` allow-list and return the
|
|
41
|
+
* files that actually exist on disk.
|
|
42
|
+
*/
|
|
43
|
+
readonly discoverModuleFiles: (saasRoot: string, manifest: ModuleManifest) => Promise<AdoptionPlan>;
|
|
44
|
+
/**
|
|
45
|
+
* Stage the module from existing app source. Idempotent — calling
|
|
46
|
+
* twice with the same source produces the same digest.
|
|
47
|
+
*/
|
|
48
|
+
readonly adoptToStaging: (saasRoot: string, manifest: ModuleManifest) => Promise<AdoptionResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Re-usable apply step: writes manifest + copies every file in
|
|
51
|
+
* the plan. Exposed so Phase 6 publish can reuse it after a
|
|
52
|
+
* separate staging-prep step.
|
|
53
|
+
*/
|
|
54
|
+
readonly runAdoptionPlan: (saasRoot: string, manifest: ModuleManifest, plan: AdoptionPlan) => Promise<{
|
|
55
|
+
staging: StagingPaths;
|
|
56
|
+
written: readonly string[];
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=module-adopter.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-adopter.service.d.ts","sourceRoot":"","sources":["../../../../src/services/module-adopter.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAErF,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,YAAY,CAAC;IACtB,oDAAoD;IACpD,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;CAChB;AAoCD,eAAO,MAAM,oBAAoB;IAC/B;;;OAGG;6CAES,MAAM,YACN,cAAc,KACvB,OAAO,CAAC,YAAY,CAAC;IA+BxB;;;OAGG;wCAES,MAAM,YACN,cAAc,KACvB,OAAO,CAAC,cAAc,CAAC;IAO1B;;;;OAIG;yCAES,MAAM,YACN,cAAc,QAClB,YAAY,KACjB,OAAO,CAAC;QAAE,OAAO,EAAE,YAAY,CAAC;QAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;CAezD,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ModuleAdopterService` — Phase 2 CLI-direct adoption flow.
|
|
3
|
+
*
|
|
4
|
+
* `wildo registry adopt` takes existing app code that fits the
|
|
5
|
+
* Wildo module-artifact shape and prepares it for publication
|
|
6
|
+
* (KD-14). Adoption is intentionally CLI-DIRECT (Review-pass-3 /
|
|
7
|
+
* MEDIUM-G) — it only reads source slices and stages files. AST
|
|
8
|
+
* mutations on the host app's source are NOT part of adopt; if a
|
|
9
|
+
* future flow needs them, they go through the companion HTTP
|
|
10
|
+
* surface like every other source-mutating verb.
|
|
11
|
+
*
|
|
12
|
+
* Service surface:
|
|
13
|
+
* - `discoverModuleFiles(saasRoot, manifest)` — walks each
|
|
14
|
+
* declared `boundaries.*` allow-list and returns the set of
|
|
15
|
+
* files that exist on disk and are within the boundary.
|
|
16
|
+
* - `adoptToStaging(saasRoot, manifest)` — clears + populates
|
|
17
|
+
* `<saasRoot>/.wildo-saas/generated/modules-artifacts/<id>/`
|
|
18
|
+
* with the manifest + every discovered file, then computes
|
|
19
|
+
* the staging digest. Phase 6 plugs this into the publish
|
|
20
|
+
* flow.
|
|
21
|
+
*/
|
|
22
|
+
import { promises as fs } from 'node:fs';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { ModulePackagerService } from './module-packager.service.js';
|
|
25
|
+
async function fileExists(absPath) {
|
|
26
|
+
try {
|
|
27
|
+
const stat = await fs.stat(absPath);
|
|
28
|
+
return stat.isFile() || stat.isDirectory();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function walkBoundary(saasRoot, appRelative) {
|
|
35
|
+
const abs = path.resolve(saasRoot, appRelative);
|
|
36
|
+
if (!(await fileExists(abs)))
|
|
37
|
+
return [];
|
|
38
|
+
const stat = await fs.stat(abs);
|
|
39
|
+
if (stat.isFile())
|
|
40
|
+
return [appRelative];
|
|
41
|
+
const out = [];
|
|
42
|
+
const queue = [appRelative];
|
|
43
|
+
while (queue.length > 0) {
|
|
44
|
+
const cur = queue.shift();
|
|
45
|
+
const entries = await fs.readdir(path.resolve(saasRoot, cur), { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const rel = path.join(cur, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
queue.push(rel);
|
|
50
|
+
}
|
|
51
|
+
else if (entry.isFile()) {
|
|
52
|
+
out.push(rel);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
export const ModuleAdopterService = {
|
|
59
|
+
/**
|
|
60
|
+
* Walk every declared `boundaries.*` allow-list and return the
|
|
61
|
+
* files that actually exist on disk.
|
|
62
|
+
*/
|
|
63
|
+
async discoverModuleFiles(saasRoot, manifest) {
|
|
64
|
+
const boundaries = manifest.boundaries;
|
|
65
|
+
if (!boundaries) {
|
|
66
|
+
return { files: [], emptyBoundaries: [] };
|
|
67
|
+
}
|
|
68
|
+
const layers = [
|
|
69
|
+
'specifications',
|
|
70
|
+
'shared',
|
|
71
|
+
'frontend',
|
|
72
|
+
'backend',
|
|
73
|
+
'minions',
|
|
74
|
+
'workers',
|
|
75
|
+
];
|
|
76
|
+
const aggregate = new Set();
|
|
77
|
+
const empty = [];
|
|
78
|
+
for (const layer of layers) {
|
|
79
|
+
const allows = boundaries[layer];
|
|
80
|
+
let layerCount = 0;
|
|
81
|
+
for (const rel of allows) {
|
|
82
|
+
const collected = await walkBoundary(saasRoot, rel);
|
|
83
|
+
for (const file of collected)
|
|
84
|
+
aggregate.add(file);
|
|
85
|
+
layerCount += collected.length;
|
|
86
|
+
}
|
|
87
|
+
if (allows.length > 0 && layerCount === 0)
|
|
88
|
+
empty.push(layer);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
files: [...aggregate].sort(),
|
|
92
|
+
emptyBoundaries: empty,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Stage the module from existing app source. Idempotent — calling
|
|
97
|
+
* twice with the same source produces the same digest.
|
|
98
|
+
*/
|
|
99
|
+
async adoptToStaging(saasRoot, manifest) {
|
|
100
|
+
const plan = await this.discoverModuleFiles(saasRoot, manifest);
|
|
101
|
+
const { staging, written } = await this.runAdoptionPlan(saasRoot, manifest, plan);
|
|
102
|
+
const digest = await ModulePackagerService.computeStagingDigest(saasRoot, manifest.id);
|
|
103
|
+
return { manifest, staging, staged: written, digest };
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Re-usable apply step: writes manifest + copies every file in
|
|
107
|
+
* the plan. Exposed so Phase 6 publish can reuse it after a
|
|
108
|
+
* separate staging-prep step.
|
|
109
|
+
*/
|
|
110
|
+
async runAdoptionPlan(saasRoot, manifest, plan) {
|
|
111
|
+
const staging = await ModulePackagerService.stageFresh(saasRoot, manifest.id);
|
|
112
|
+
await ModulePackagerService.writeManifest({ saasRoot, manifest });
|
|
113
|
+
const written = [];
|
|
114
|
+
for (const appRelativePath of plan.files) {
|
|
115
|
+
const result = await ModulePackagerService.copyFile({
|
|
116
|
+
saasRoot,
|
|
117
|
+
moduleId: manifest.id,
|
|
118
|
+
appRelativePath,
|
|
119
|
+
manifest,
|
|
120
|
+
});
|
|
121
|
+
written.push(result.appRelativePath);
|
|
122
|
+
}
|
|
123
|
+
return { staging, written };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=module-adopter.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-adopter.service.js","sourceRoot":"","sources":["../../../../src/services/module-adopter.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ModuleAdopterService` — Phase 2 CLI-direct adoption flow.\n *\n * `wildo registry adopt` takes existing app code that fits the\n * Wildo module-artifact shape and prepares it for publication\n * (KD-14). Adoption is intentionally CLI-DIRECT (Review-pass-3 /\n * MEDIUM-G) — it only reads source slices and stages files. AST\n * mutations on the host app's source are NOT part of adopt; if a\n * future flow needs them, they go through the companion HTTP\n * surface like every other source-mutating verb.\n *\n * Service surface:\n * - `discoverModuleFiles(saasRoot, manifest)` — walks each\n * declared `boundaries.*` allow-list and returns the set of\n * files that exist on disk and are within the boundary.\n * - `adoptToStaging(saasRoot, manifest)` — clears + populates\n * `<saasRoot>/.wildo-saas/generated/modules-artifacts/<id>/`\n * with the manifest + every discovered file, then computes\n * the staging digest. Phase 6 plugs this into the publish\n * flow.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport type { ModuleManifest } from '@wildo-ai/platform-config-lib';\nimport { ModulePackagerService, type StagingPaths } from './module-packager.service';\n\nexport interface AdoptionPlan {\n /** Per-layer file list, relative to saas root. */\n files: readonly string[];\n /** Empty when every declared boundary contributed at least one file. */\n emptyBoundaries: readonly string[];\n}\n\nexport interface AdoptionResult {\n manifest: ModuleManifest;\n staging: StagingPaths;\n /** Files that were copied into the staging root. */\n staged: readonly string[];\n /** sha256 digest of the staged tree (matches `Sha256DigestSchema`). */\n digest: string;\n}\n\nasync function fileExists(absPath: string): Promise<boolean> {\n try {\n const stat = await fs.stat(absPath);\n return stat.isFile() || stat.isDirectory();\n } catch {\n return false;\n }\n}\n\nasync function walkBoundary(\n saasRoot: string,\n appRelative: string,\n): Promise<string[]> {\n const abs = path.resolve(saasRoot, appRelative);\n if (!(await fileExists(abs))) return [];\n const stat = await fs.stat(abs);\n if (stat.isFile()) return [appRelative];\n const out: string[] = [];\n const queue: string[] = [appRelative];\n while (queue.length > 0) {\n const cur = queue.shift()!;\n const entries = await fs.readdir(path.resolve(saasRoot, cur), { withFileTypes: true });\n for (const entry of entries) {\n const rel = path.join(cur, entry.name);\n if (entry.isDirectory()) {\n queue.push(rel);\n } else if (entry.isFile()) {\n out.push(rel);\n }\n }\n }\n return out;\n}\n\nexport const ModuleAdopterService = {\n /**\n * Walk every declared `boundaries.*` allow-list and return the\n * files that actually exist on disk.\n */\n async discoverModuleFiles(\n saasRoot: string,\n manifest: ModuleManifest,\n ): Promise<AdoptionPlan> {\n const boundaries = manifest.boundaries;\n if (!boundaries) {\n return { files: [], emptyBoundaries: [] };\n }\n const layers: ReadonlyArray<keyof typeof boundaries> = [\n 'specifications',\n 'shared',\n 'frontend',\n 'backend',\n 'minions',\n 'workers',\n ];\n const aggregate = new Set<string>();\n const empty: string[] = [];\n for (const layer of layers) {\n const allows = boundaries[layer];\n let layerCount = 0;\n for (const rel of allows) {\n const collected = await walkBoundary(saasRoot, rel);\n for (const file of collected) aggregate.add(file);\n layerCount += collected.length;\n }\n if (allows.length > 0 && layerCount === 0) empty.push(layer);\n }\n return {\n files: [...aggregate].sort(),\n emptyBoundaries: empty,\n };\n },\n\n /**\n * Stage the module from existing app source. Idempotent — calling\n * twice with the same source produces the same digest.\n */\n async adoptToStaging(\n saasRoot: string,\n manifest: ModuleManifest,\n ): Promise<AdoptionResult> {\n const plan = await this.discoverModuleFiles(saasRoot, manifest);\n const { staging, written } = await this.runAdoptionPlan(saasRoot, manifest, plan);\n const digest = await ModulePackagerService.computeStagingDigest(saasRoot, manifest.id);\n return { manifest, staging, staged: written, digest };\n },\n\n /**\n * Re-usable apply step: writes manifest + copies every file in\n * the plan. Exposed so Phase 6 publish can reuse it after a\n * separate staging-prep step.\n */\n async runAdoptionPlan(\n saasRoot: string,\n manifest: ModuleManifest,\n plan: AdoptionPlan,\n ): Promise<{ staging: StagingPaths; written: readonly string[] }> {\n const staging = await ModulePackagerService.stageFresh(saasRoot, manifest.id);\n await ModulePackagerService.writeManifest({ saasRoot, manifest });\n const written: string[] = [];\n for (const appRelativePath of plan.files) {\n const result = await ModulePackagerService.copyFile({\n saasRoot,\n moduleId: manifest.id,\n appRelativePath,\n manifest,\n });\n written.push(result.appRelativePath);\n }\n return { staging, written };\n },\n} as const;\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ModulePackagerService` — Phase 2 staging logic for module
|
|
3
|
+
* publishing.
|
|
4
|
+
*
|
|
5
|
+
* Per KD-8 / `module-registry-analysis.md`:
|
|
6
|
+
* - Published artifacts are zip archives staged under
|
|
7
|
+
* `<saasRoot>/.wildo-saas/generated/modules-artifacts/<module.id>/`.
|
|
8
|
+
* - The staged folder is the source of the artifact. It must
|
|
9
|
+
* include the `wildo.module.json` manifest, copied source
|
|
10
|
+
* files, and dependency declarations.
|
|
11
|
+
*
|
|
12
|
+
* Phase 2 ships the staging-folder primitives (path resolution,
|
|
13
|
+
* stage-fresh, copy-source-tree, write-manifest, compute-digest).
|
|
14
|
+
* Phase 6 wires `wildo registry publish` to call these and the zip
|
|
15
|
+
* step. Phase 5 install consumes the same digest.
|
|
16
|
+
*
|
|
17
|
+
* Hard contracts:
|
|
18
|
+
* - Staging is always a fresh tree per call (`stageFresh`
|
|
19
|
+
* removes the destination first). The artifact's content
|
|
20
|
+
* digest is over-the-wire deterministic — leftover bytes
|
|
21
|
+
* from a previous failed stage MUST NOT leak into the digest.
|
|
22
|
+
* - The staged tree is a CHILD of `<saasRoot>/.wildo-saas/`,
|
|
23
|
+
* never reaches outside `saasRoot`.
|
|
24
|
+
* - Boundary enforcement (KD-12) lives in this service: the
|
|
25
|
+
* staging primitive rejects file paths that escape the
|
|
26
|
+
* declared `boundaries.<layer>` allow-list, so a publish
|
|
27
|
+
* can never claim app surface it doesn't own.
|
|
28
|
+
*/
|
|
29
|
+
import { type ModuleManifest, type ModuleManifestBoundaries } from '@wildo-ai/platform-config-lib';
|
|
30
|
+
export declare const MODULES_ARTIFACTS_RELATIVE_DIR = ".wildo-saas/generated/modules-artifacts";
|
|
31
|
+
export interface StagingPaths {
|
|
32
|
+
/** Absolute path to the per-module staging root. */
|
|
33
|
+
stagingRoot: string;
|
|
34
|
+
/** Absolute path to the manifest file inside the staging root. */
|
|
35
|
+
manifestPath: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the per-module staging paths. Pure path arithmetic — no
|
|
39
|
+
* fs side effects.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getModuleStagingPaths(saasRoot: string, moduleId: string): StagingPaths;
|
|
42
|
+
/**
|
|
43
|
+
* Verify a candidate file path is contained inside one of the
|
|
44
|
+
* manifest's declared `boundaries` (KD-12). Returns the resolved
|
|
45
|
+
* absolute path on success; throws `RegistryBoundaryViolationError`
|
|
46
|
+
* otherwise.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertWithinBoundaries(saasRoot: string, moduleId: string, appRelativePath: string, boundaries: ModuleManifestBoundaries | undefined): string;
|
|
49
|
+
export declare const ModulePackagerService: {
|
|
50
|
+
/**
|
|
51
|
+
* Compute the per-module staging paths.
|
|
52
|
+
*/
|
|
53
|
+
readonly paths: (saasRoot: string, moduleId: string) => StagingPaths;
|
|
54
|
+
/**
|
|
55
|
+
* Remove and recreate the staging root. The freshness guarantee
|
|
56
|
+
* is critical for digest determinism — `stageFresh` is the only
|
|
57
|
+
* way to enter the per-module staging root.
|
|
58
|
+
*/
|
|
59
|
+
readonly stageFresh: (saasRoot: string, moduleId: string) => Promise<StagingPaths>;
|
|
60
|
+
/**
|
|
61
|
+
* Copy a single file from the app source into the staging tree.
|
|
62
|
+
* Path is verified against the manifest's declared boundaries
|
|
63
|
+
* (KD-12) — files outside `boundaries.*` are rejected with
|
|
64
|
+
* `RegistryBoundaryViolationError`. The relative path INSIDE the
|
|
65
|
+
* staging root mirrors the relative path inside the saas root,
|
|
66
|
+
* so callers don't need to translate paths between the two
|
|
67
|
+
* trees.
|
|
68
|
+
*/
|
|
69
|
+
readonly copyFile: (args: {
|
|
70
|
+
saasRoot: string;
|
|
71
|
+
moduleId: string;
|
|
72
|
+
appRelativePath: string;
|
|
73
|
+
manifest: ModuleManifest;
|
|
74
|
+
}) => Promise<{
|
|
75
|
+
stagedAbsolute: string;
|
|
76
|
+
appRelativePath: string;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Validate the manifest, then write `wildo.module.json` into the
|
|
80
|
+
* staging root. Manifest authors who pass a typo-laden object
|
|
81
|
+
* get a Zod parse error here, before any zip work.
|
|
82
|
+
*/
|
|
83
|
+
readonly writeManifest: (args: {
|
|
84
|
+
saasRoot: string;
|
|
85
|
+
manifest: ModuleManifest;
|
|
86
|
+
}) => Promise<{
|
|
87
|
+
manifestPath: string;
|
|
88
|
+
manifest: ModuleManifest;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Compute the sha256 digest of every file in the staging tree, in
|
|
92
|
+
* stable lexicographic path order. Returns the canonical
|
|
93
|
+
* `sha256:<64-hex>` form.
|
|
94
|
+
*
|
|
95
|
+
* The digest is over-the-wire deterministic for the same set of
|
|
96
|
+
* staged files because:
|
|
97
|
+
* 1. File order is sorted lexicographically (no fs traversal
|
|
98
|
+
* randomness leaks in).
|
|
99
|
+
* 2. The path prefix is the relative-from-staging-root path
|
|
100
|
+
* (no absolute-path noise).
|
|
101
|
+
* 3. We append the file content bytes verbatim.
|
|
102
|
+
*/
|
|
103
|
+
readonly computeStagingDigest: (saasRoot: string, moduleId: string) => Promise<string>;
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=module-packager.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-packager.service.d.ts","sourceRoot":"","sources":["../../../../src/services/module-packager.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAKH,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC9B,MAAM,+BAA+B,CAAC;AAEvC,eAAO,MAAM,8BAA8B,4CAA4C,CAAC;AAGxF,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAmBtF;AA+BD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,wBAAwB,GAAG,SAAS,GAC/C,MAAM,CAsBR;AAED,eAAO,MAAM,qBAAqB;IAChC;;OAEG;+BACa,MAAM,YAAY,MAAM,KAAG,YAAY;IAIvD;;;;OAIG;oCACwB,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,YAAY,CAAC;IAO3E;;;;;;;;OAQG;8BACkB;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,eAAe,EAAE,MAAM,CAAC;QACxB,QAAQ,EAAE,cAAc,CAAC;KAC1B,KAAG,OAAO,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAehE;;;;OAIG;mCACuB;QACxB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,cAAc,CAAC;KAC1B,KAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,cAAc,CAAA;KAAE,CAAC;IAQ/D;;;;;;;;;;;;OAYG;8CACkC,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;CAmBvE,CAAC"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ModulePackagerService` — Phase 2 staging logic for module
|
|
3
|
+
* publishing.
|
|
4
|
+
*
|
|
5
|
+
* Per KD-8 / `module-registry-analysis.md`:
|
|
6
|
+
* - Published artifacts are zip archives staged under
|
|
7
|
+
* `<saasRoot>/.wildo-saas/generated/modules-artifacts/<module.id>/`.
|
|
8
|
+
* - The staged folder is the source of the artifact. It must
|
|
9
|
+
* include the `wildo.module.json` manifest, copied source
|
|
10
|
+
* files, and dependency declarations.
|
|
11
|
+
*
|
|
12
|
+
* Phase 2 ships the staging-folder primitives (path resolution,
|
|
13
|
+
* stage-fresh, copy-source-tree, write-manifest, compute-digest).
|
|
14
|
+
* Phase 6 wires `wildo registry publish` to call these and the zip
|
|
15
|
+
* step. Phase 5 install consumes the same digest.
|
|
16
|
+
*
|
|
17
|
+
* Hard contracts:
|
|
18
|
+
* - Staging is always a fresh tree per call (`stageFresh`
|
|
19
|
+
* removes the destination first). The artifact's content
|
|
20
|
+
* digest is over-the-wire deterministic — leftover bytes
|
|
21
|
+
* from a previous failed stage MUST NOT leak into the digest.
|
|
22
|
+
* - The staged tree is a CHILD of `<saasRoot>/.wildo-saas/`,
|
|
23
|
+
* never reaches outside `saasRoot`.
|
|
24
|
+
* - Boundary enforcement (KD-12) lives in this service: the
|
|
25
|
+
* staging primitive rejects file paths that escape the
|
|
26
|
+
* declared `boundaries.<layer>` allow-list, so a publish
|
|
27
|
+
* can never claim app surface it doesn't own.
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { promises as fs } from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { ModuleManifestSchema, RegistryBoundaryViolationError, } from '@wildo-ai/platform-config-lib';
|
|
33
|
+
export const MODULES_ARTIFACTS_RELATIVE_DIR = '.wildo-saas/generated/modules-artifacts';
|
|
34
|
+
const MANIFEST_FILE_NAME = 'wildo.module.json';
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the per-module staging paths. Pure path arithmetic — no
|
|
37
|
+
* fs side effects.
|
|
38
|
+
*/
|
|
39
|
+
export function getModuleStagingPaths(saasRoot, moduleId) {
|
|
40
|
+
if (!moduleId.includes('/')) {
|
|
41
|
+
// C-L3 (4th-pass review): `TypeError` is Node's standard for
|
|
42
|
+
// argument-shape violations. The typed `RegistryError` subclasses
|
|
43
|
+
// model registry-runtime conditions (auth, integrity, version
|
|
44
|
+
// not found); a malformed `moduleId` argument is a developer
|
|
45
|
+
// contract violation that downstream `isRegistryError(err)`
|
|
46
|
+
// handlers should NOT catch. `TypeError` keeps the error visible
|
|
47
|
+
// and stops the typed-error vocabulary from absorbing pure
|
|
48
|
+
// argument-validation concerns.
|
|
49
|
+
throw new TypeError(`Module id "${moduleId}" must be in <domain>/<name> form (e.g. "acme/billing").`);
|
|
50
|
+
}
|
|
51
|
+
const stagingRoot = path.resolve(saasRoot, MODULES_ARTIFACTS_RELATIVE_DIR, moduleId);
|
|
52
|
+
return {
|
|
53
|
+
stagingRoot,
|
|
54
|
+
manifestPath: path.join(stagingRoot, MANIFEST_FILE_NAME),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns `true` iff `child` is the same path as `parent` or
|
|
59
|
+
* a descendant of it. Both arguments are expected to be absolute,
|
|
60
|
+
* already resolved (no `..` segments). Tail-`/` arithmetic prevents
|
|
61
|
+
* the classic `/foo/barx` → `/foo/bar` false-positive.
|
|
62
|
+
*
|
|
63
|
+
* Extracted as a helper (5th-pass C-L2) so the boundary check below
|
|
64
|
+
* and the future zip-slip / symlink-escape guards in this package
|
|
65
|
+
* share one reading of the containment rule.
|
|
66
|
+
*
|
|
67
|
+
* 6th-pass C-L1 — root edge case: when `parent` is the filesystem
|
|
68
|
+
* root (`/` on POSIX, `C:\` on Windows), appending `path.sep` would
|
|
69
|
+
* produce `//` or `C:\\` and `startsWith` would reject every child.
|
|
70
|
+
* Treat the root as a special case: any absolute child path is
|
|
71
|
+
* "inside" the root by definition. Vanishingly unlikely in practice
|
|
72
|
+
* (containerized SaaS roots are not literally `/`) but the helper
|
|
73
|
+
* is also reusable for zip-slip guards where the assumption is
|
|
74
|
+
* weaker.
|
|
75
|
+
*/
|
|
76
|
+
function isPathInside(child, parent) {
|
|
77
|
+
// Special-case the filesystem root: any absolute child is inside it.
|
|
78
|
+
if (parent === path.sep || /^[a-zA-Z]:[\\/]?$/.test(parent)) {
|
|
79
|
+
return path.isAbsolute(child);
|
|
80
|
+
}
|
|
81
|
+
const childWithSep = child + path.sep;
|
|
82
|
+
const parentWithSep = parent + path.sep;
|
|
83
|
+
return childWithSep.startsWith(parentWithSep);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Verify a candidate file path is contained inside one of the
|
|
87
|
+
* manifest's declared `boundaries` (KD-12). Returns the resolved
|
|
88
|
+
* absolute path on success; throws `RegistryBoundaryViolationError`
|
|
89
|
+
* otherwise.
|
|
90
|
+
*/
|
|
91
|
+
export function assertWithinBoundaries(saasRoot, moduleId, appRelativePath, boundaries) {
|
|
92
|
+
const absolute = path.resolve(saasRoot, appRelativePath);
|
|
93
|
+
// Path must stay inside saasRoot (no `..` escapes).
|
|
94
|
+
if (!isPathInside(absolute, path.resolve(saasRoot))) {
|
|
95
|
+
throw new RegistryBoundaryViolationError(moduleId, appRelativePath);
|
|
96
|
+
}
|
|
97
|
+
if (!boundaries)
|
|
98
|
+
return absolute;
|
|
99
|
+
const allowed = [
|
|
100
|
+
...boundaries.specifications,
|
|
101
|
+
...boundaries.shared,
|
|
102
|
+
...boundaries.frontend,
|
|
103
|
+
...boundaries.backend,
|
|
104
|
+
...boundaries.minions,
|
|
105
|
+
...boundaries.workers,
|
|
106
|
+
];
|
|
107
|
+
if (allowed.length === 0)
|
|
108
|
+
return absolute;
|
|
109
|
+
// The candidate must be inside at least one declared boundary.
|
|
110
|
+
const isInside = allowed.some((rel) => isPathInside(absolute, path.resolve(saasRoot, rel)));
|
|
111
|
+
if (!isInside) {
|
|
112
|
+
throw new RegistryBoundaryViolationError(moduleId, appRelativePath);
|
|
113
|
+
}
|
|
114
|
+
return absolute;
|
|
115
|
+
}
|
|
116
|
+
export const ModulePackagerService = {
|
|
117
|
+
/**
|
|
118
|
+
* Compute the per-module staging paths.
|
|
119
|
+
*/
|
|
120
|
+
paths(saasRoot, moduleId) {
|
|
121
|
+
return getModuleStagingPaths(saasRoot, moduleId);
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Remove and recreate the staging root. The freshness guarantee
|
|
125
|
+
* is critical for digest determinism — `stageFresh` is the only
|
|
126
|
+
* way to enter the per-module staging root.
|
|
127
|
+
*/
|
|
128
|
+
async stageFresh(saasRoot, moduleId) {
|
|
129
|
+
const paths = getModuleStagingPaths(saasRoot, moduleId);
|
|
130
|
+
await fs.rm(paths.stagingRoot, { recursive: true, force: true });
|
|
131
|
+
await fs.mkdir(paths.stagingRoot, { recursive: true });
|
|
132
|
+
return paths;
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Copy a single file from the app source into the staging tree.
|
|
136
|
+
* Path is verified against the manifest's declared boundaries
|
|
137
|
+
* (KD-12) — files outside `boundaries.*` are rejected with
|
|
138
|
+
* `RegistryBoundaryViolationError`. The relative path INSIDE the
|
|
139
|
+
* staging root mirrors the relative path inside the saas root,
|
|
140
|
+
* so callers don't need to translate paths between the two
|
|
141
|
+
* trees.
|
|
142
|
+
*/
|
|
143
|
+
async copyFile(args) {
|
|
144
|
+
const { saasRoot, moduleId, appRelativePath, manifest } = args;
|
|
145
|
+
const sourceAbsolute = assertWithinBoundaries(saasRoot, moduleId, appRelativePath, manifest.boundaries);
|
|
146
|
+
const { stagingRoot } = getModuleStagingPaths(saasRoot, moduleId);
|
|
147
|
+
const destAbsolute = path.join(stagingRoot, appRelativePath);
|
|
148
|
+
await fs.mkdir(path.dirname(destAbsolute), { recursive: true });
|
|
149
|
+
await fs.copyFile(sourceAbsolute, destAbsolute);
|
|
150
|
+
return { stagedAbsolute: destAbsolute, appRelativePath };
|
|
151
|
+
},
|
|
152
|
+
/**
|
|
153
|
+
* Validate the manifest, then write `wildo.module.json` into the
|
|
154
|
+
* staging root. Manifest authors who pass a typo-laden object
|
|
155
|
+
* get a Zod parse error here, before any zip work.
|
|
156
|
+
*/
|
|
157
|
+
async writeManifest(args) {
|
|
158
|
+
const validated = ModuleManifestSchema.parse(args.manifest);
|
|
159
|
+
const { manifestPath, stagingRoot } = getModuleStagingPaths(args.saasRoot, validated.id);
|
|
160
|
+
await fs.mkdir(stagingRoot, { recursive: true });
|
|
161
|
+
await fs.writeFile(manifestPath, JSON.stringify(validated, null, 2) + '\n', 'utf8');
|
|
162
|
+
return { manifestPath, manifest: validated };
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Compute the sha256 digest of every file in the staging tree, in
|
|
166
|
+
* stable lexicographic path order. Returns the canonical
|
|
167
|
+
* `sha256:<64-hex>` form.
|
|
168
|
+
*
|
|
169
|
+
* The digest is over-the-wire deterministic for the same set of
|
|
170
|
+
* staged files because:
|
|
171
|
+
* 1. File order is sorted lexicographically (no fs traversal
|
|
172
|
+
* randomness leaks in).
|
|
173
|
+
* 2. The path prefix is the relative-from-staging-root path
|
|
174
|
+
* (no absolute-path noise).
|
|
175
|
+
* 3. We append the file content bytes verbatim.
|
|
176
|
+
*/
|
|
177
|
+
async computeStagingDigest(saasRoot, moduleId) {
|
|
178
|
+
const { stagingRoot } = getModuleStagingPaths(saasRoot, moduleId);
|
|
179
|
+
const files = await collectFilesRelative(stagingRoot, '');
|
|
180
|
+
// Normalize separators to POSIX before sorting + hashing — the
|
|
181
|
+
// digest must reproduce across platforms for the same staged
|
|
182
|
+
// tree, so the bytes fed into the hash cannot depend on the
|
|
183
|
+
// publisher's OS path-separator convention.
|
|
184
|
+
const normalized = files.map((f) => f.split(path.sep).join(path.posix.sep));
|
|
185
|
+
normalized.sort();
|
|
186
|
+
const hash = createHash('sha256');
|
|
187
|
+
for (const rel of normalized) {
|
|
188
|
+
const abs = path.join(stagingRoot, rel.split(path.posix.sep).join(path.sep));
|
|
189
|
+
hash.update(rel);
|
|
190
|
+
hash.update('\0');
|
|
191
|
+
hash.update(await fs.readFile(abs));
|
|
192
|
+
hash.update('\0');
|
|
193
|
+
}
|
|
194
|
+
return `sha256:${hash.digest('hex')}`;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
async function collectFilesRelative(rootAbs, prefix) {
|
|
198
|
+
const here = path.join(rootAbs, prefix);
|
|
199
|
+
const entries = await fs.readdir(here, { withFileTypes: true });
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const rel = path.join(prefix, entry.name);
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
out.push(...(await collectFilesRelative(rootAbs, rel)));
|
|
205
|
+
}
|
|
206
|
+
else if (entry.isFile()) {
|
|
207
|
+
out.push(rel);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=module-packager.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-packager.service.js","sourceRoot":"","sources":["../../../../src/services/module-packager.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ModulePackagerService` — Phase 2 staging logic for module\n * publishing.\n *\n * Per KD-8 / `module-registry-analysis.md`:\n * - Published artifacts are zip archives staged under\n * `<saasRoot>/.wildo-saas/generated/modules-artifacts/<module.id>/`.\n * - The staged folder is the source of the artifact. It must\n * include the `wildo.module.json` manifest, copied source\n * files, and dependency declarations.\n *\n * Phase 2 ships the staging-folder primitives (path resolution,\n * stage-fresh, copy-source-tree, write-manifest, compute-digest).\n * Phase 6 wires `wildo registry publish` to call these and the zip\n * step. Phase 5 install consumes the same digest.\n *\n * Hard contracts:\n * - Staging is always a fresh tree per call (`stageFresh`\n * removes the destination first). The artifact's content\n * digest is over-the-wire deterministic — leftover bytes\n * from a previous failed stage MUST NOT leak into the digest.\n * - The staged tree is a CHILD of `<saasRoot>/.wildo-saas/`,\n * never reaches outside `saasRoot`.\n * - Boundary enforcement (KD-12) lives in this service: the\n * staging primitive rejects file paths that escape the\n * declared `boundaries.<layer>` allow-list, so a publish\n * can never claim app surface it doesn't own.\n */\n\nimport { createHash } from 'node:crypto';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport {\n ModuleManifestSchema,\n RegistryBoundaryViolationError,\n type ModuleManifest,\n type ModuleManifestBoundaries,\n} from '@wildo-ai/platform-config-lib';\n\nexport const MODULES_ARTIFACTS_RELATIVE_DIR = '.wildo-saas/generated/modules-artifacts';\nconst MANIFEST_FILE_NAME = 'wildo.module.json';\n\nexport interface StagingPaths {\n /** Absolute path to the per-module staging root. */\n stagingRoot: string;\n /** Absolute path to the manifest file inside the staging root. */\n manifestPath: string;\n}\n\n/**\n * Resolve the per-module staging paths. Pure path arithmetic — no\n * fs side effects.\n */\nexport function getModuleStagingPaths(saasRoot: string, moduleId: string): StagingPaths {\n if (!moduleId.includes('/')) {\n // C-L3 (4th-pass review): `TypeError` is Node's standard for\n // argument-shape violations. The typed `RegistryError` subclasses\n // model registry-runtime conditions (auth, integrity, version\n // not found); a malformed `moduleId` argument is a developer\n // contract violation that downstream `isRegistryError(err)`\n // handlers should NOT catch. `TypeError` keeps the error visible\n // and stops the typed-error vocabulary from absorbing pure\n // argument-validation concerns.\n throw new TypeError(\n `Module id \"${moduleId}\" must be in <domain>/<name> form (e.g. \"acme/billing\").`,\n );\n }\n const stagingRoot = path.resolve(saasRoot, MODULES_ARTIFACTS_RELATIVE_DIR, moduleId);\n return {\n stagingRoot,\n manifestPath: path.join(stagingRoot, MANIFEST_FILE_NAME),\n };\n}\n\n/**\n * Returns `true` iff `child` is the same path as `parent` or\n * a descendant of it. Both arguments are expected to be absolute,\n * already resolved (no `..` segments). Tail-`/` arithmetic prevents\n * the classic `/foo/barx` → `/foo/bar` false-positive.\n *\n * Extracted as a helper (5th-pass C-L2) so the boundary check below\n * and the future zip-slip / symlink-escape guards in this package\n * share one reading of the containment rule.\n *\n * 6th-pass C-L1 — root edge case: when `parent` is the filesystem\n * root (`/` on POSIX, `C:\\` on Windows), appending `path.sep` would\n * produce `//` or `C:\\\\` and `startsWith` would reject every child.\n * Treat the root as a special case: any absolute child path is\n * \"inside\" the root by definition. Vanishingly unlikely in practice\n * (containerized SaaS roots are not literally `/`) but the helper\n * is also reusable for zip-slip guards where the assumption is\n * weaker.\n */\nfunction isPathInside(child: string, parent: string): boolean {\n // Special-case the filesystem root: any absolute child is inside it.\n if (parent === path.sep || /^[a-zA-Z]:[\\\\/]?$/.test(parent)) {\n return path.isAbsolute(child);\n }\n const childWithSep = child + path.sep;\n const parentWithSep = parent + path.sep;\n return childWithSep.startsWith(parentWithSep);\n}\n\n/**\n * Verify a candidate file path is contained inside one of the\n * manifest's declared `boundaries` (KD-12). Returns the resolved\n * absolute path on success; throws `RegistryBoundaryViolationError`\n * otherwise.\n */\nexport function assertWithinBoundaries(\n saasRoot: string,\n moduleId: string,\n appRelativePath: string,\n boundaries: ModuleManifestBoundaries | undefined,\n): string {\n const absolute = path.resolve(saasRoot, appRelativePath);\n // Path must stay inside saasRoot (no `..` escapes).\n if (!isPathInside(absolute, path.resolve(saasRoot))) {\n throw new RegistryBoundaryViolationError(moduleId, appRelativePath);\n }\n if (!boundaries) return absolute;\n const allowed: readonly string[] = [\n ...boundaries.specifications,\n ...boundaries.shared,\n ...boundaries.frontend,\n ...boundaries.backend,\n ...boundaries.minions,\n ...boundaries.workers,\n ];\n if (allowed.length === 0) return absolute;\n // The candidate must be inside at least one declared boundary.\n const isInside = allowed.some((rel) => isPathInside(absolute, path.resolve(saasRoot, rel)));\n if (!isInside) {\n throw new RegistryBoundaryViolationError(moduleId, appRelativePath);\n }\n return absolute;\n}\n\nexport const ModulePackagerService = {\n /**\n * Compute the per-module staging paths.\n */\n paths(saasRoot: string, moduleId: string): StagingPaths {\n return getModuleStagingPaths(saasRoot, moduleId);\n },\n\n /**\n * Remove and recreate the staging root. The freshness guarantee\n * is critical for digest determinism — `stageFresh` is the only\n * way to enter the per-module staging root.\n */\n async stageFresh(saasRoot: string, moduleId: string): Promise<StagingPaths> {\n const paths = getModuleStagingPaths(saasRoot, moduleId);\n await fs.rm(paths.stagingRoot, { recursive: true, force: true });\n await fs.mkdir(paths.stagingRoot, { recursive: true });\n return paths;\n },\n\n /**\n * Copy a single file from the app source into the staging tree.\n * Path is verified against the manifest's declared boundaries\n * (KD-12) — files outside `boundaries.*` are rejected with\n * `RegistryBoundaryViolationError`. The relative path INSIDE the\n * staging root mirrors the relative path inside the saas root,\n * so callers don't need to translate paths between the two\n * trees.\n */\n async copyFile(args: {\n saasRoot: string;\n moduleId: string;\n appRelativePath: string;\n manifest: ModuleManifest;\n }): Promise<{ stagedAbsolute: string; appRelativePath: string }> {\n const { saasRoot, moduleId, appRelativePath, manifest } = args;\n const sourceAbsolute = assertWithinBoundaries(\n saasRoot,\n moduleId,\n appRelativePath,\n manifest.boundaries,\n );\n const { stagingRoot } = getModuleStagingPaths(saasRoot, moduleId);\n const destAbsolute = path.join(stagingRoot, appRelativePath);\n await fs.mkdir(path.dirname(destAbsolute), { recursive: true });\n await fs.copyFile(sourceAbsolute, destAbsolute);\n return { stagedAbsolute: destAbsolute, appRelativePath };\n },\n\n /**\n * Validate the manifest, then write `wildo.module.json` into the\n * staging root. Manifest authors who pass a typo-laden object\n * get a Zod parse error here, before any zip work.\n */\n async writeManifest(args: {\n saasRoot: string;\n manifest: ModuleManifest;\n }): Promise<{ manifestPath: string; manifest: ModuleManifest }> {\n const validated = ModuleManifestSchema.parse(args.manifest);\n const { manifestPath, stagingRoot } = getModuleStagingPaths(args.saasRoot, validated.id);\n await fs.mkdir(stagingRoot, { recursive: true });\n await fs.writeFile(manifestPath, JSON.stringify(validated, null, 2) + '\\n', 'utf8');\n return { manifestPath, manifest: validated };\n },\n\n /**\n * Compute the sha256 digest of every file in the staging tree, in\n * stable lexicographic path order. Returns the canonical\n * `sha256:<64-hex>` form.\n *\n * The digest is over-the-wire deterministic for the same set of\n * staged files because:\n * 1. File order is sorted lexicographically (no fs traversal\n * randomness leaks in).\n * 2. The path prefix is the relative-from-staging-root path\n * (no absolute-path noise).\n * 3. We append the file content bytes verbatim.\n */\n async computeStagingDigest(saasRoot: string, moduleId: string): Promise<string> {\n const { stagingRoot } = getModuleStagingPaths(saasRoot, moduleId);\n const files = await collectFilesRelative(stagingRoot, '');\n // Normalize separators to POSIX before sorting + hashing — the\n // digest must reproduce across platforms for the same staged\n // tree, so the bytes fed into the hash cannot depend on the\n // publisher's OS path-separator convention.\n const normalized = files.map((f) => f.split(path.sep).join(path.posix.sep));\n normalized.sort();\n const hash = createHash('sha256');\n for (const rel of normalized) {\n const abs = path.join(stagingRoot, rel.split(path.posix.sep).join(path.sep));\n hash.update(rel);\n hash.update('\\0');\n hash.update(await fs.readFile(abs));\n hash.update('\\0');\n }\n return `sha256:${hash.digest('hex')}`;\n },\n} as const;\n\nasync function collectFilesRelative(rootAbs: string, prefix: string): Promise<string[]> {\n const here = path.join(rootAbs, prefix);\n const entries = await fs.readdir(here, { withFileTypes: true });\n const out: string[] = [];\n for (const entry of entries) {\n const rel = path.join(prefix, entry.name);\n if (entry.isDirectory()) {\n out.push(...(await collectFilesRelative(rootAbs, rel)));\n } else if (entry.isFile()) {\n out.push(rel);\n }\n }\n return out;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runProviderMaterializationHook` — Phase 0.5 dry-run hook.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 superseded this by `runProviderSync` (the real
|
|
5
|
+
* `wildo config sync --domain config` shell-out with classified
|
|
6
|
+
* failure surfaces). This file is kept as a thin
|
|
7
|
+
* `dryRun: true` forwarder so the Phase 0.5 lockfile-smoke
|
|
8
|
+
* command and any external caller continue to work unchanged.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Phase 2+ flows MUST call `runProviderSync` directly.
|
|
11
|
+
* This forwarder will be removed once Wonder Todos's
|
|
12
|
+
* `lockfile-smoke` command is rewritten in the Phase 4 CLI
|
|
13
|
+
* read-flow series.
|
|
14
|
+
*/
|
|
15
|
+
import { PROVIDER_LOCK_SECTION_PATH } from '@wildo-ai/platform-config-lib';
|
|
16
|
+
import type { RegistryClientLogger } from './types';
|
|
17
|
+
export interface ProviderMaterializationHookInput {
|
|
18
|
+
saasRoot: string;
|
|
19
|
+
reason: 'install' | 'update' | 'remove' | 'adopt' | 'smoke';
|
|
20
|
+
log?: RegistryClientLogger;
|
|
21
|
+
}
|
|
22
|
+
export interface ProviderMaterializationHookResult {
|
|
23
|
+
invoked: true;
|
|
24
|
+
mode: 'dry-run' | 'shell-out';
|
|
25
|
+
performedFsWrite: false;
|
|
26
|
+
providerJsonRelativePaths: readonly string[];
|
|
27
|
+
providerSourceRelativePath: string;
|
|
28
|
+
providerLockSectionPath: typeof PROVIDER_LOCK_SECTION_PATH;
|
|
29
|
+
reason: ProviderMaterializationHookInput['reason'];
|
|
30
|
+
}
|
|
31
|
+
export declare function runProviderMaterializationHook(input: ProviderMaterializationHookInput): Promise<ProviderMaterializationHookResult>;
|
|
32
|
+
//# sourceMappingURL=provider-materialization-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-materialization-hook.d.ts","sourceRoot":"","sources":["../../../../src/services/provider-materialization-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAE3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IAC5D,GAAG,CAAC,EAAE,oBAAoB,CAAC;CAC5B;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,EAAE,IAAI,CAAC;IACd,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;IAC9B,gBAAgB,EAAE,KAAK,CAAC;IACxB,yBAAyB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,0BAA0B,EAAE,MAAM,CAAC;IACnC,uBAAuB,EAAE,OAAO,0BAA0B,CAAC;IAC3D,MAAM,EAAE,gCAAgC,CAAC,QAAQ,CAAC,CAAC;CACpD;AAED,wBAAsB,8BAA8B,CAClD,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,iCAAiC,CAAC,CAgB5C"}
|