@nexural/factory 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @nexural/factory
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release — Phase 1 scope.
6
+
7
+ - `loadRecipe(rawManifest, revocationList, options?)` — parse + revocation check (ADRs 0002, 0009)
8
+ - `checkRevocation(name, version, list)` — direct revocation check
9
+ - `buildLockfile(input)` — validated `ForgedLockfile` constructor (ADR-0006)
10
+ - `runLicenseGate(sbom, commercialRestrictedOk?)` — fail forge on GPL/AGPL/BUSL/unknown (ADR-0006 §4)
11
+ - `ALLOWED_BY_DEFAULT`, `STRONG_COPYLEFT`, `COMMERCIAL_RESTRICTED` sets
12
+ - `detectTyposquats(candidates, options?)` — Levenshtein-based typosquat detection (ADR-0009 §1.7)
13
+ - `HIGH_PRIORITY_PACKAGES` curated target list
14
+ - `levenshtein(a, b)` exported for direct use
15
+
16
+ Deferred to Phase 5 (when recipes ship): cosign signature shell-out, template emission engine, op:// secret resolution, pre/post-emit hooks.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @nexural/factory
2
+
3
+ Codegen engine for `nx forge`. Phase-1 scope: validation primitives + lockfile + safety gates.
4
+
5
+ ## What ships at v0.1.0
6
+
7
+ - **`loadRecipe(raw, revocationList, options?)`** — parse a `recipe.yaml` against `RecipeManifest`, fail on revoked.
8
+ - **`buildLockfile(input)`** — produces a schema-valid `.nexural/forged.lock.yaml` for emitted apps (ADR-0006 §1).
9
+ - **`runLicenseGate(sbom, commercialRestrictedOk?)`** — fail forge on GPL/AGPL/BUSL/unknown license. Allowed by default: MIT, Apache-2.0, ISC, BSD, MPL-2.0, CC0, Unlicense, 0BSD.
10
+ - **`detectTyposquats(candidates, options?)`** — Levenshtein distance check against `HIGH_PRIORITY_PACKAGES` (per ADR-0009 §1.7).
11
+ - **`checkRevocation(name, version, list)`** — direct lookup against the revocation list (per ADR-0009 §1.6).
12
+
13
+ ## Deferred to Phase 5
14
+
15
+ Template emission, cosign shell-out, op:// secret resolution, pre/post-emit hooks land when recipes themselves ship.
16
+
17
+ ## Usage outline (Phase 5 — illustrative)
18
+
19
+ ```ts
20
+ import { loadRecipe, runLicenseGate, detectTyposquats, buildLockfile } from "@nexural/factory";
21
+
22
+ // 1. Load + revocation check
23
+ const { recipe } = loadRecipe(parsedYaml, revocationList);
24
+
25
+ // 2. (cosign verify-attestation — outside this lib)
26
+ // 3. (Generate SBOM via cyclonedx-npm — outside this lib)
27
+
28
+ // 4. License gate
29
+ const gate = runLicenseGate(sbom, recipe.commercial_restricted_ok);
30
+ if (!gate.passed) throw new Error(`License gate failed: ${JSON.stringify(gate.failures)}`);
31
+
32
+ // 5. Typosquat check
33
+ const squats = detectTyposquats(sbom.map((s) => s.name));
34
+ if (squats.length) throw new Error(`Typosquat suspects: ${JSON.stringify(squats)}`);
35
+
36
+ // 6. Emit lockfile
37
+ const lockfile = buildLockfile({
38
+ /* ... */
39
+ });
40
+ await writeFile(".nexural/forged.lock.yaml", yaml.dump(lockfile), "utf8");
41
+ ```
@@ -0,0 +1,145 @@
1
+ import { RevokedRecipesList, ForgedLockfile, RecipeManifest } from '@nexural/schema';
2
+
3
+ /**
4
+ * License composition gate per ADR-0006 §4.
5
+ *
6
+ * Forge fails if any direct or transitive dep is:
7
+ * - Strong copyleft (GPL, AGPL, LGPL)
8
+ * - Source-available commercial-restricted (BUSL, SSPL, Elastic-2.0)
9
+ * UNLESS recipe explicitly opts in via `commercial_restricted_ok: true`
10
+ * - Unknown license
11
+ */
12
+ declare const ALLOWED_BY_DEFAULT: ReadonlySet<string>;
13
+ declare const STRONG_COPYLEFT: ReadonlySet<string>;
14
+ declare const COMMERCIAL_RESTRICTED: ReadonlySet<string>;
15
+ interface SbomEntry {
16
+ readonly name: string;
17
+ readonly version: string;
18
+ readonly license: string | null;
19
+ }
20
+ type GateFailureCode = "strong_copyleft" | "commercial_restricted" | "unknown_license";
21
+ interface GateFailure {
22
+ readonly package: string;
23
+ readonly version: string;
24
+ readonly license: string | null;
25
+ readonly code: GateFailureCode;
26
+ }
27
+ interface GateResult {
28
+ readonly passed: boolean;
29
+ readonly failures: ReadonlyArray<GateFailure>;
30
+ }
31
+ /**
32
+ * Run the license gate on a list of SBOM entries.
33
+ *
34
+ * @param sbom dep tree from cyclonedx-npm or equivalent
35
+ * @param commercialRestrictedOk if true, BUSL/SSPL/Elastic are accepted (per recipe declaration)
36
+ */
37
+ declare function runLicenseGate(sbom: ReadonlyArray<SbomEntry>, commercialRestrictedOk?: boolean): GateResult;
38
+
39
+ /**
40
+ * Typosquat detection per ADR-0009 §1.7.
41
+ *
42
+ * For each package in the to-be-emitted dep tree, compare against a known
43
+ * high-priority package list. If Levenshtein distance is small (1-2 chars),
44
+ * flag as suspicious.
45
+ *
46
+ * Recipes that legitimately depend on a similarly-named package opt in via
47
+ * explicit allowlist in recipe.yaml (out of scope here; consumed by `nx forge`).
48
+ */
49
+ /**
50
+ * High-priority package names — common typosquat targets.
51
+ * Update as the ecosystem evolves; sourced from npm download statistics.
52
+ */
53
+ declare const HIGH_PRIORITY_PACKAGES: ReadonlyArray<string>;
54
+ /**
55
+ * Levenshtein distance (edit distance).
56
+ */
57
+ declare function levenshtein(a: string, b: string): number;
58
+ interface TyposquatHit {
59
+ readonly suspectName: string;
60
+ readonly knownTarget: string;
61
+ readonly distance: number;
62
+ }
63
+ /**
64
+ * Check each candidate name against the high-priority list.
65
+ * Returns hits where distance ≤ `maxDistance` AND names differ (exact matches excluded).
66
+ *
67
+ * Default maxDistance = 2 per ADR-0009 §1.7.
68
+ */
69
+ declare function detectTyposquats(candidates: ReadonlyArray<string>, options?: {
70
+ readonly knownTargets?: ReadonlyArray<string>;
71
+ readonly maxDistance?: number;
72
+ }): ReadonlyArray<TyposquatHit>;
73
+
74
+ /**
75
+ * Recipe revocation check per ADR-0009 §1.6.
76
+ *
77
+ * `nx forge` consults `nexural-meta/security/revoked-recipes.yaml` before
78
+ * emitting. Revoked recipe + version → forge fails immediately.
79
+ */
80
+
81
+ interface RevocationCheckResult {
82
+ readonly revoked: boolean;
83
+ readonly reason?: string;
84
+ readonly revokedAt?: string;
85
+ readonly ticket?: string;
86
+ }
87
+ /**
88
+ * Check whether a (name, version) pair is on the revocation list.
89
+ *
90
+ * Exact name AND exact version must match (per ADR-0009 — revocations are precise).
91
+ */
92
+ declare function checkRevocation(recipeName: string, recipeVersion: string, list: RevokedRecipesList): RevocationCheckResult;
93
+
94
+ /**
95
+ * Forged-app lockfile writer per ADR-0006 §1.
96
+ *
97
+ * Every `nx forge` writes `.nexural/forged.lock.yaml` in the emitted app.
98
+ * The lockfile pins recipe + warehouse SHAs, inputs, and SBOM hash.
99
+ * `nx upgrade` reads this file to compute clean diffs.
100
+ */
101
+
102
+ /**
103
+ * Build (but don't write to disk) a forged lockfile.
104
+ * Validates the result against the schema.
105
+ */
106
+ declare function buildLockfile(input: {
107
+ forgedByNxVersion: string;
108
+ recipe: ForgedLockfile["recipe"];
109
+ warehousesConsumed: ForgedLockfile["warehouses_consumed"];
110
+ inputs: Record<string, unknown>;
111
+ modelFamiliesUsed: ReadonlyArray<string>;
112
+ sbomHash: string;
113
+ forgedAtMs?: number;
114
+ }): ForgedLockfile;
115
+
116
+ /**
117
+ * Recipe manifest loader per ADRs 0002, 0006.
118
+ *
119
+ * Parses `recipe.yaml` against RecipeManifest schema.
120
+ * Verifies the recipe is NOT in the revocation list.
121
+ * (Signature verification happens via `cosign` shell-out — that is the
122
+ * responsibility of the `nx forge` command, not this library.)
123
+ */
124
+
125
+ interface LoadResult {
126
+ readonly recipe: RecipeManifest;
127
+ readonly revocation: RevocationCheckResult;
128
+ }
129
+ type LoadError = {
130
+ readonly code: "parse_error";
131
+ readonly message: string;
132
+ } | {
133
+ readonly code: "revoked";
134
+ readonly revocation: RevocationCheckResult;
135
+ };
136
+ /**
137
+ * Parse + check revocation. Throws on parse error or revocation.
138
+ *
139
+ * Pass `{ allowRevoked: true }` to bypass revocation check (audit-only consumers).
140
+ */
141
+ declare function loadRecipe(rawManifest: unknown, revocationList: RevokedRecipesList, options?: {
142
+ allowRevoked?: boolean;
143
+ }): LoadResult;
144
+
145
+ export { ALLOWED_BY_DEFAULT, COMMERCIAL_RESTRICTED, type GateFailure, type GateFailureCode, type GateResult, HIGH_PRIORITY_PACKAGES, type LoadError, type LoadResult, type RevocationCheckResult, STRONG_COPYLEFT, type SbomEntry, type TyposquatHit, buildLockfile, checkRevocation, detectTyposquats, levenshtein, loadRecipe, runLicenseGate };
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ // src/license-gate.ts
2
+ var ALLOWED_BY_DEFAULT = /* @__PURE__ */ new Set([
3
+ "MIT",
4
+ "Apache-2.0",
5
+ "ISC",
6
+ "BSD-2-Clause",
7
+ "BSD-3-Clause",
8
+ "MPL-2.0",
9
+ "CC0-1.0",
10
+ "Unlicense",
11
+ "0BSD"
12
+ ]);
13
+ var STRONG_COPYLEFT = /* @__PURE__ */ new Set([
14
+ "GPL-2.0",
15
+ "GPL-2.0-only",
16
+ "GPL-2.0-or-later",
17
+ "GPL-3.0",
18
+ "GPL-3.0-only",
19
+ "GPL-3.0-or-later",
20
+ "AGPL-3.0",
21
+ "AGPL-3.0-only",
22
+ "AGPL-3.0-or-later",
23
+ "LGPL-2.1",
24
+ "LGPL-2.1-only",
25
+ "LGPL-2.1-or-later",
26
+ "LGPL-3.0",
27
+ "LGPL-3.0-only",
28
+ "LGPL-3.0-or-later"
29
+ ]);
30
+ var COMMERCIAL_RESTRICTED = /* @__PURE__ */ new Set([
31
+ "BUSL-1.1",
32
+ "SSPL-1.0",
33
+ "Elastic-2.0"
34
+ ]);
35
+ function runLicenseGate(sbom, commercialRestrictedOk = false) {
36
+ const failures = [];
37
+ for (const entry of sbom) {
38
+ if (entry.license === null) {
39
+ failures.push({
40
+ package: entry.name,
41
+ version: entry.version,
42
+ license: null,
43
+ code: "unknown_license"
44
+ });
45
+ continue;
46
+ }
47
+ if (STRONG_COPYLEFT.has(entry.license)) {
48
+ failures.push({
49
+ package: entry.name,
50
+ version: entry.version,
51
+ license: entry.license,
52
+ code: "strong_copyleft"
53
+ });
54
+ continue;
55
+ }
56
+ if (COMMERCIAL_RESTRICTED.has(entry.license) && !commercialRestrictedOk) {
57
+ failures.push({
58
+ package: entry.name,
59
+ version: entry.version,
60
+ license: entry.license,
61
+ code: "commercial_restricted"
62
+ });
63
+ continue;
64
+ }
65
+ if (!ALLOWED_BY_DEFAULT.has(entry.license) && !COMMERCIAL_RESTRICTED.has(entry.license)) {
66
+ failures.push({
67
+ package: entry.name,
68
+ version: entry.version,
69
+ license: entry.license,
70
+ code: "unknown_license"
71
+ });
72
+ }
73
+ }
74
+ return { passed: failures.length === 0, failures };
75
+ }
76
+
77
+ // src/typosquat.ts
78
+ var HIGH_PRIORITY_PACKAGES = [
79
+ // React ecosystem
80
+ "react",
81
+ "react-dom",
82
+ "react-router",
83
+ "react-router-dom",
84
+ // Next
85
+ "next",
86
+ // Vue / Svelte
87
+ "vue",
88
+ "svelte",
89
+ // Tooling
90
+ "vite",
91
+ "vitest",
92
+ "webpack",
93
+ "rollup",
94
+ "esbuild",
95
+ "typescript",
96
+ "tsup",
97
+ "turbo",
98
+ // Utils
99
+ "lodash",
100
+ "lodash-es",
101
+ "axios",
102
+ "node-fetch",
103
+ "zod",
104
+ "yup",
105
+ // Auth / data
106
+ "next-auth",
107
+ "stripe",
108
+ "@supabase/supabase-js",
109
+ // Email
110
+ "resend",
111
+ "nodemailer",
112
+ // LLM
113
+ "@anthropic-ai/sdk",
114
+ "openai",
115
+ // AWS / cloud
116
+ "aws-sdk",
117
+ "@aws-sdk/client-s3",
118
+ // Misc top packages
119
+ "express",
120
+ "cors",
121
+ "dotenv",
122
+ "uuid"
123
+ ];
124
+ function levenshtein(a, b) {
125
+ if (a === b) return 0;
126
+ if (a.length === 0) return b.length;
127
+ if (b.length === 0) return a.length;
128
+ const prev = new Array(b.length + 1);
129
+ const curr = new Array(b.length + 1);
130
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
131
+ for (let i = 1; i <= a.length; i++) {
132
+ curr[0] = i;
133
+ for (let j = 1; j <= b.length; j++) {
134
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
135
+ curr[j] = Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
136
+ }
137
+ for (let j = 0; j <= b.length; j++) prev[j] = curr[j] ?? 0;
138
+ }
139
+ return prev[b.length] ?? 0;
140
+ }
141
+ function detectTyposquats(candidates, options = {}) {
142
+ const known = options.knownTargets ?? HIGH_PRIORITY_PACKAGES;
143
+ const maxDist = options.maxDistance ?? 2;
144
+ const hits = [];
145
+ for (const suspect of candidates) {
146
+ for (const target of known) {
147
+ if (suspect === target) continue;
148
+ const dist = levenshtein(suspect, target);
149
+ if (dist > 0 && dist <= maxDist) {
150
+ hits.push({ suspectName: suspect, knownTarget: target, distance: dist });
151
+ }
152
+ }
153
+ }
154
+ return hits;
155
+ }
156
+
157
+ // src/revocation.ts
158
+ function checkRevocation(recipeName, recipeVersion, list) {
159
+ for (const entry of list.entries) {
160
+ if (entry.recipe_name === recipeName && entry.recipe_version === recipeVersion) {
161
+ return {
162
+ revoked: true,
163
+ reason: entry.reason,
164
+ revokedAt: entry.revoked_at,
165
+ ...entry.ticket ? { ticket: entry.ticket } : {}
166
+ };
167
+ }
168
+ }
169
+ return { revoked: false };
170
+ }
171
+
172
+ // src/lockfile.ts
173
+ import { ForgedLockfile as ForgedLockfileSchema } from "@nexural/schema";
174
+ function buildLockfile(input) {
175
+ const forgedAt = new Date(input.forgedAtMs ?? Date.now()).toISOString();
176
+ const lockfile = {
177
+ schema_version: 1,
178
+ forged_at: forgedAt,
179
+ forged_by_nx_version: input.forgedByNxVersion,
180
+ recipe: input.recipe,
181
+ warehouses_consumed: [...input.warehousesConsumed],
182
+ inputs: input.inputs,
183
+ model_families_used: [...input.modelFamiliesUsed],
184
+ sbom_hash: input.sbomHash
185
+ };
186
+ return ForgedLockfileSchema.parse(lockfile);
187
+ }
188
+
189
+ // src/recipe-loader.ts
190
+ import { RecipeManifest as RecipeManifestSchema } from "@nexural/schema";
191
+ function loadRecipe(rawManifest, revocationList, options = {}) {
192
+ let recipe;
193
+ try {
194
+ recipe = RecipeManifestSchema.parse(rawManifest);
195
+ } catch (e) {
196
+ const message = e instanceof Error ? e.message : "invalid recipe manifest";
197
+ const err = { code: "parse_error", message };
198
+ throw Object.assign(new Error(message), { cause: err });
199
+ }
200
+ const revocation = checkRevocation(recipe.name, recipe.version, revocationList);
201
+ if (revocation.revoked && !options.allowRevoked) {
202
+ const message = `Recipe ${recipe.name}@${recipe.version} is revoked: ${revocation.reason ?? "unknown reason"}`;
203
+ const err = { code: "revoked", revocation };
204
+ throw Object.assign(new Error(message), { cause: err });
205
+ }
206
+ return { recipe, revocation };
207
+ }
208
+ export {
209
+ ALLOWED_BY_DEFAULT,
210
+ COMMERCIAL_RESTRICTED,
211
+ HIGH_PRIORITY_PACKAGES,
212
+ STRONG_COPYLEFT,
213
+ buildLockfile,
214
+ checkRevocation,
215
+ detectTyposquats,
216
+ levenshtein,
217
+ loadRecipe,
218
+ runLicenseGate
219
+ };
220
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/license-gate.ts","../src/typosquat.ts","../src/revocation.ts","../src/lockfile.ts","../src/recipe-loader.ts"],"sourcesContent":["/**\n * License composition gate per ADR-0006 §4.\n *\n * Forge fails if any direct or transitive dep is:\n * - Strong copyleft (GPL, AGPL, LGPL)\n * - Source-available commercial-restricted (BUSL, SSPL, Elastic-2.0)\n * UNLESS recipe explicitly opts in via `commercial_restricted_ok: true`\n * - Unknown license\n */\n\nexport const ALLOWED_BY_DEFAULT: ReadonlySet<string> = new Set([\n \"MIT\",\n \"Apache-2.0\",\n \"ISC\",\n \"BSD-2-Clause\",\n \"BSD-3-Clause\",\n \"MPL-2.0\",\n \"CC0-1.0\",\n \"Unlicense\",\n \"0BSD\",\n]);\n\nexport const STRONG_COPYLEFT: ReadonlySet<string> = new Set([\n \"GPL-2.0\",\n \"GPL-2.0-only\",\n \"GPL-2.0-or-later\",\n \"GPL-3.0\",\n \"GPL-3.0-only\",\n \"GPL-3.0-or-later\",\n \"AGPL-3.0\",\n \"AGPL-3.0-only\",\n \"AGPL-3.0-or-later\",\n \"LGPL-2.1\",\n \"LGPL-2.1-only\",\n \"LGPL-2.1-or-later\",\n \"LGPL-3.0\",\n \"LGPL-3.0-only\",\n \"LGPL-3.0-or-later\",\n]);\n\nexport const COMMERCIAL_RESTRICTED: ReadonlySet<string> = new Set([\n \"BUSL-1.1\",\n \"SSPL-1.0\",\n \"Elastic-2.0\",\n]);\n\nexport interface SbomEntry {\n readonly name: string;\n readonly version: string;\n readonly license: string | null;\n}\n\nexport type GateFailureCode = \"strong_copyleft\" | \"commercial_restricted\" | \"unknown_license\";\n\nexport interface GateFailure {\n readonly package: string;\n readonly version: string;\n readonly license: string | null;\n readonly code: GateFailureCode;\n}\n\nexport interface GateResult {\n readonly passed: boolean;\n readonly failures: ReadonlyArray<GateFailure>;\n}\n\n/**\n * Run the license gate on a list of SBOM entries.\n *\n * @param sbom dep tree from cyclonedx-npm or equivalent\n * @param commercialRestrictedOk if true, BUSL/SSPL/Elastic are accepted (per recipe declaration)\n */\nexport function runLicenseGate(\n sbom: ReadonlyArray<SbomEntry>,\n commercialRestrictedOk = false,\n): GateResult {\n const failures: GateFailure[] = [];\n for (const entry of sbom) {\n if (entry.license === null) {\n failures.push({\n package: entry.name,\n version: entry.version,\n license: null,\n code: \"unknown_license\",\n });\n continue;\n }\n if (STRONG_COPYLEFT.has(entry.license)) {\n failures.push({\n package: entry.name,\n version: entry.version,\n license: entry.license,\n code: \"strong_copyleft\",\n });\n continue;\n }\n if (COMMERCIAL_RESTRICTED.has(entry.license) && !commercialRestrictedOk) {\n failures.push({\n package: entry.name,\n version: entry.version,\n license: entry.license,\n code: \"commercial_restricted\",\n });\n continue;\n }\n // Unknown-but-not-banned licenses also fail — explicit allowlist only.\n if (!ALLOWED_BY_DEFAULT.has(entry.license) && !COMMERCIAL_RESTRICTED.has(entry.license)) {\n failures.push({\n package: entry.name,\n version: entry.version,\n license: entry.license,\n code: \"unknown_license\",\n });\n }\n }\n return { passed: failures.length === 0, failures };\n}\n","/**\n * Typosquat detection per ADR-0009 §1.7.\n *\n * For each package in the to-be-emitted dep tree, compare against a known\n * high-priority package list. If Levenshtein distance is small (1-2 chars),\n * flag as suspicious.\n *\n * Recipes that legitimately depend on a similarly-named package opt in via\n * explicit allowlist in recipe.yaml (out of scope here; consumed by `nx forge`).\n */\n\n/**\n * High-priority package names — common typosquat targets.\n * Update as the ecosystem evolves; sourced from npm download statistics.\n */\nexport const HIGH_PRIORITY_PACKAGES: ReadonlyArray<string> = [\n // React ecosystem\n \"react\",\n \"react-dom\",\n \"react-router\",\n \"react-router-dom\",\n // Next\n \"next\",\n // Vue / Svelte\n \"vue\",\n \"svelte\",\n // Tooling\n \"vite\",\n \"vitest\",\n \"webpack\",\n \"rollup\",\n \"esbuild\",\n \"typescript\",\n \"tsup\",\n \"turbo\",\n // Utils\n \"lodash\",\n \"lodash-es\",\n \"axios\",\n \"node-fetch\",\n \"zod\",\n \"yup\",\n // Auth / data\n \"next-auth\",\n \"stripe\",\n \"@supabase/supabase-js\",\n // Email\n \"resend\",\n \"nodemailer\",\n // LLM\n \"@anthropic-ai/sdk\",\n \"openai\",\n // AWS / cloud\n \"aws-sdk\",\n \"@aws-sdk/client-s3\",\n // Misc top packages\n \"express\",\n \"cors\",\n \"dotenv\",\n \"uuid\",\n];\n\n/**\n * Levenshtein distance (edit distance).\n */\nexport function levenshtein(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n const prev: number[] = new Array<number>(b.length + 1);\n const curr: number[] = new Array<number>(b.length + 1);\n for (let j = 0; j <= b.length; j++) prev[j] = j;\n for (let i = 1; i <= a.length; i++) {\n curr[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min((curr[j - 1] ?? 0) + 1, (prev[j] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);\n }\n for (let j = 0; j <= b.length; j++) prev[j] = curr[j] ?? 0;\n }\n return prev[b.length] ?? 0;\n}\n\nexport interface TyposquatHit {\n readonly suspectName: string;\n readonly knownTarget: string;\n readonly distance: number;\n}\n\n/**\n * Check each candidate name against the high-priority list.\n * Returns hits where distance ≤ `maxDistance` AND names differ (exact matches excluded).\n *\n * Default maxDistance = 2 per ADR-0009 §1.7.\n */\nexport function detectTyposquats(\n candidates: ReadonlyArray<string>,\n options: {\n readonly knownTargets?: ReadonlyArray<string>;\n readonly maxDistance?: number;\n } = {},\n): ReadonlyArray<TyposquatHit> {\n const known = options.knownTargets ?? HIGH_PRIORITY_PACKAGES;\n const maxDist = options.maxDistance ?? 2;\n const hits: TyposquatHit[] = [];\n for (const suspect of candidates) {\n for (const target of known) {\n if (suspect === target) continue; // exact match is fine\n const dist = levenshtein(suspect, target);\n if (dist > 0 && dist <= maxDist) {\n hits.push({ suspectName: suspect, knownTarget: target, distance: dist });\n }\n }\n }\n return hits;\n}\n","/**\n * Recipe revocation check per ADR-0009 §1.6.\n *\n * `nx forge` consults `nexural-meta/security/revoked-recipes.yaml` before\n * emitting. Revoked recipe + version → forge fails immediately.\n */\n\nimport type { RevokedRecipesList } from \"@nexural/schema\";\n\nexport interface RevocationCheckResult {\n readonly revoked: boolean;\n readonly reason?: string;\n readonly revokedAt?: string;\n readonly ticket?: string;\n}\n\n/**\n * Check whether a (name, version) pair is on the revocation list.\n *\n * Exact name AND exact version must match (per ADR-0009 — revocations are precise).\n */\nexport function checkRevocation(\n recipeName: string,\n recipeVersion: string,\n list: RevokedRecipesList,\n): RevocationCheckResult {\n for (const entry of list.entries) {\n if (entry.recipe_name === recipeName && entry.recipe_version === recipeVersion) {\n return {\n revoked: true,\n reason: entry.reason,\n revokedAt: entry.revoked_at,\n ...(entry.ticket ? { ticket: entry.ticket } : {}),\n };\n }\n }\n return { revoked: false };\n}\n","/**\n * Forged-app lockfile writer per ADR-0006 §1.\n *\n * Every `nx forge` writes `.nexural/forged.lock.yaml` in the emitted app.\n * The lockfile pins recipe + warehouse SHAs, inputs, and SBOM hash.\n * `nx upgrade` reads this file to compute clean diffs.\n */\n\nimport type { ForgedLockfile } from \"@nexural/schema\";\nimport { ForgedLockfile as ForgedLockfileSchema } from \"@nexural/schema\";\n\n/**\n * Build (but don't write to disk) a forged lockfile.\n * Validates the result against the schema.\n */\nexport function buildLockfile(input: {\n forgedByNxVersion: string;\n recipe: ForgedLockfile[\"recipe\"];\n warehousesConsumed: ForgedLockfile[\"warehouses_consumed\"];\n inputs: Record<string, unknown>;\n modelFamiliesUsed: ReadonlyArray<string>;\n sbomHash: string;\n forgedAtMs?: number;\n}): ForgedLockfile {\n const forgedAt = new Date(input.forgedAtMs ?? Date.now()).toISOString();\n const lockfile: ForgedLockfile = {\n schema_version: 1,\n forged_at: forgedAt,\n forged_by_nx_version: input.forgedByNxVersion,\n recipe: input.recipe,\n warehouses_consumed: [...input.warehousesConsumed],\n inputs: input.inputs,\n model_families_used: [...input.modelFamiliesUsed],\n sbom_hash: input.sbomHash,\n };\n // Re-parse to enforce schema invariants (and throw on misuse).\n return ForgedLockfileSchema.parse(lockfile);\n}\n","/**\n * Recipe manifest loader per ADRs 0002, 0006.\n *\n * Parses `recipe.yaml` against RecipeManifest schema.\n * Verifies the recipe is NOT in the revocation list.\n * (Signature verification happens via `cosign` shell-out — that is the\n * responsibility of the `nx forge` command, not this library.)\n */\n\nimport type { RecipeManifest, RevokedRecipesList } from \"@nexural/schema\";\nimport { RecipeManifest as RecipeManifestSchema } from \"@nexural/schema\";\nimport { checkRevocation, type RevocationCheckResult } from \"./revocation.js\";\n\nexport interface LoadResult {\n readonly recipe: RecipeManifest;\n readonly revocation: RevocationCheckResult;\n}\n\nexport type LoadError =\n | { readonly code: \"parse_error\"; readonly message: string }\n | { readonly code: \"revoked\"; readonly revocation: RevocationCheckResult };\n\n/**\n * Parse + check revocation. Throws on parse error or revocation.\n *\n * Pass `{ allowRevoked: true }` to bypass revocation check (audit-only consumers).\n */\nexport function loadRecipe(\n rawManifest: unknown,\n revocationList: RevokedRecipesList,\n options: { allowRevoked?: boolean } = {},\n): LoadResult {\n let recipe: RecipeManifest;\n try {\n recipe = RecipeManifestSchema.parse(rawManifest);\n } catch (e) {\n const message = e instanceof Error ? e.message : \"invalid recipe manifest\";\n const err: LoadError = { code: \"parse_error\", message };\n throw Object.assign(new Error(message), { cause: err });\n }\n\n const revocation = checkRevocation(recipe.name, recipe.version, revocationList);\n if (revocation.revoked && !options.allowRevoked) {\n const message = `Recipe ${recipe.name}@${recipe.version} is revoked: ${revocation.reason ?? \"unknown reason\"}`;\n const err: LoadError = { code: \"revoked\", revocation };\n throw Object.assign(new Error(message), { cause: err });\n }\n return { recipe, revocation };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@nexural/factory",
3
+ "version": "0.1.0",
4
+ "description": "Codegen engine for nx forge. Recipe loading + signature verification + SBOM license gate + lockfile writer + typosquat detection + revocation list check. Per ADRs 0002, 0006, 0009.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "dependencies": {
21
+ "zod": "^3.24.1",
22
+ "@nexural/schema": "^0.1.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "provenance": true
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/JasonTeixeira/nexural-meta.git",
31
+ "directory": "packages/factory"
32
+ },
33
+ "homepage": "https://github.com/JasonTeixeira/nexural-meta/tree/main/packages/factory#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/JasonTeixeira/nexural-meta/issues"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "test": "vitest run",
40
+ "test:coverage": "vitest run --coverage",
41
+ "lint": "eslint src test",
42
+ "typecheck": "tsc --noEmit",
43
+ "clean": "rm -rf dist .turbo"
44
+ }
45
+ }