@letterblack/lbe-core 1.3.3 → 1.3.5
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/README.md +104 -443
- package/RELEASE_WORKSPACE_RULES.md +110 -0
- package/dist/cli/lbe.js +165 -7
- package/dist/cli/lbe_engine.wasm +0 -0
- package/package.json +31 -30
- package/src/cli/commands/assertConsumer.js +198 -0
- package/src/cli/main.js +181 -176
- package/src/cli/parseArgs.js +1 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# LBE Release Workspace Rules
|
|
2
|
+
|
|
3
|
+
This file defines what can and cannot be used as LBE release proof.
|
|
4
|
+
|
|
5
|
+
## Release authority boundary
|
|
6
|
+
|
|
7
|
+
Only the designated LBE release workspace may certify LBE release safety.
|
|
8
|
+
|
|
9
|
+
A downstream project, integration lab, copied repository, downloaded folder, worktree, or consumer app is not release authority for LBE.
|
|
10
|
+
|
|
11
|
+
Consumer projects may prove only their own integration behavior with the installed LBE package.
|
|
12
|
+
|
|
13
|
+
They must not claim:
|
|
14
|
+
|
|
15
|
+
- LBE release-ready
|
|
16
|
+
- LBE published
|
|
17
|
+
- full LBE proof passed
|
|
18
|
+
- npm/GitHub release alignment
|
|
19
|
+
- package release correctness
|
|
20
|
+
|
|
21
|
+
## Consumer dependency rule
|
|
22
|
+
|
|
23
|
+
Other projects must consume LBE as an installed package dependency from the public registry.
|
|
24
|
+
|
|
25
|
+
Allowed consumer model:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @letterblack/lbe-core
|
|
29
|
+
npx lbe init
|
|
30
|
+
npx lbe status
|
|
31
|
+
npx lbe proof --public
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Do not use a copied LBE source tree as the authority for consumer projects.
|
|
35
|
+
|
|
36
|
+
Do not point consumer projects at LBE through:
|
|
37
|
+
|
|
38
|
+
- `file:`
|
|
39
|
+
- `link:`
|
|
40
|
+
- `workspace:`
|
|
41
|
+
- `git+`
|
|
42
|
+
- `github:`
|
|
43
|
+
- local relative paths
|
|
44
|
+
- local absolute paths
|
|
45
|
+
- symlinked `node_modules` packages
|
|
46
|
+
|
|
47
|
+
## assert-consumer rule
|
|
48
|
+
|
|
49
|
+
`npx lbe assert-consumer` is a downstream consumer-safety guard.
|
|
50
|
+
|
|
51
|
+
It answers:
|
|
52
|
+
|
|
53
|
+
- Is this project using `@letterblack/lbe-core` as an installed dependency?
|
|
54
|
+
- Is this project accidentally pointing at a copied source tree, workspace link, git dependency, local path, or symlink?
|
|
55
|
+
|
|
56
|
+
It must always report consumer status only.
|
|
57
|
+
|
|
58
|
+
It is not release proof.
|
|
59
|
+
|
|
60
|
+
It is not package provenance proof.
|
|
61
|
+
|
|
62
|
+
It is not a substitute for:
|
|
63
|
+
|
|
64
|
+
- full test suite
|
|
65
|
+
- `npm run proof`
|
|
66
|
+
- package runtime verification
|
|
67
|
+
- packed tarball inspection
|
|
68
|
+
- npm `gitHead` check
|
|
69
|
+
- GitHub tag alignment
|
|
70
|
+
- GitHub Release verification
|
|
71
|
+
- fresh install smoke from the registry
|
|
72
|
+
|
|
73
|
+
Expected classification for a valid consumer project:
|
|
74
|
+
|
|
75
|
+
```txt
|
|
76
|
+
consumer-project-using-installed-registry-dependency
|
|
77
|
+
releaseClaimsAllowed: false
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If a project passes `assert-consumer`, the only valid conclusion is:
|
|
81
|
+
|
|
82
|
+
```txt
|
|
83
|
+
This project consumes LBE from an installed package dependency.
|
|
84
|
+
This does not certify LBE release safety.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Hard stop conditions
|
|
88
|
+
|
|
89
|
+
Stop and report if:
|
|
90
|
+
|
|
91
|
+
- a consumer project is used to certify an LBE release
|
|
92
|
+
- focused integration tests are used as release proof
|
|
93
|
+
- a copied/lab workspace is treated as package authority
|
|
94
|
+
- local path, git, workspace, or symlink dependencies are used for LBE in a consumer project
|
|
95
|
+
- release claims are made without npm/GitHub/package provenance checks
|
|
96
|
+
|
|
97
|
+
## Agent report format
|
|
98
|
+
|
|
99
|
+
Before making any LBE-related release claim, report:
|
|
100
|
+
|
|
101
|
+
```txt
|
|
102
|
+
Workspace classification:
|
|
103
|
+
- Path:
|
|
104
|
+
- Type: LBE release workspace / consumer project / local lab / copied workspace / unknown
|
|
105
|
+
- npm run proof available: yes/no
|
|
106
|
+
- full suite exit code:
|
|
107
|
+
- release claims allowed: yes/no
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If the workspace is a consumer project, local lab, copied workspace, or unknown, release claims are not allowed.
|
package/dist/cli/lbe.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/main.js
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs30 from "fs";
|
|
5
|
+
import path36 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli/parseArgs.js
|
|
@@ -67,6 +67,7 @@ COMMANDS:
|
|
|
67
67
|
logs Print recent entries from central event log
|
|
68
68
|
open-state Open the central state directory in the file manager
|
|
69
69
|
proof Show latest proof result (--json for raw JSON, --public for redacted)
|
|
70
|
+
assert-consumer Verify this project consumes LBE as an installed registry dependency
|
|
70
71
|
help Show this help message
|
|
71
72
|
|
|
72
73
|
OPTIONS:
|
|
@@ -4143,6 +4144,160 @@ LBE Proof \u2014 ${workspace}`);
|
|
|
4143
4144
|
return { found: true, result: proof.result, profile: proof.profile };
|
|
4144
4145
|
}
|
|
4145
4146
|
|
|
4147
|
+
// src/cli/commands/assertConsumer.js
|
|
4148
|
+
import fs29 from "fs";
|
|
4149
|
+
import path35 from "path";
|
|
4150
|
+
import { createRequire } from "module";
|
|
4151
|
+
var PACKAGE_NAME = "@letterblack/lbe-core";
|
|
4152
|
+
var DEP_SECTIONS = [
|
|
4153
|
+
"dependencies",
|
|
4154
|
+
"devDependencies",
|
|
4155
|
+
"optionalDependencies",
|
|
4156
|
+
"peerDependencies"
|
|
4157
|
+
];
|
|
4158
|
+
function readJson(filePath) {
|
|
4159
|
+
try {
|
|
4160
|
+
return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
4161
|
+
} catch {
|
|
4162
|
+
return null;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
function addCheck2(checks, name, ok, details = {}) {
|
|
4166
|
+
checks.push({ name, ok, ...details });
|
|
4167
|
+
return ok;
|
|
4168
|
+
}
|
|
4169
|
+
function findDependencySpec(packageJson2) {
|
|
4170
|
+
if (!packageJson2 || typeof packageJson2 !== "object") return null;
|
|
4171
|
+
for (const section of DEP_SECTIONS) {
|
|
4172
|
+
const value = packageJson2[section]?.[PACKAGE_NAME];
|
|
4173
|
+
if (value !== void 0) {
|
|
4174
|
+
return { section, spec: String(value) };
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
return null;
|
|
4178
|
+
}
|
|
4179
|
+
function classifyDependencySpec(spec) {
|
|
4180
|
+
const raw = String(spec || "").trim();
|
|
4181
|
+
const lower = raw.toLowerCase();
|
|
4182
|
+
if (!raw) {
|
|
4183
|
+
return { ok: false, reason: "EMPTY_DEPENDENCY_SPEC" };
|
|
4184
|
+
}
|
|
4185
|
+
const blockedPrefixes = [
|
|
4186
|
+
"file:",
|
|
4187
|
+
"link:",
|
|
4188
|
+
"workspace:",
|
|
4189
|
+
"git+",
|
|
4190
|
+
"github:",
|
|
4191
|
+
"git://",
|
|
4192
|
+
"ssh://"
|
|
4193
|
+
];
|
|
4194
|
+
for (const prefix of blockedPrefixes) {
|
|
4195
|
+
if (lower.startsWith(prefix)) {
|
|
4196
|
+
return { ok: false, reason: "LOCAL_OR_GIT_DEPENDENCY_SPEC", prefix };
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (/^[a-z]:[\\/]/i.test(raw) || raw.startsWith("./") || raw.startsWith("../")) {
|
|
4200
|
+
return { ok: false, reason: "PATH_DEPENDENCY_SPEC" };
|
|
4201
|
+
}
|
|
4202
|
+
return { ok: true };
|
|
4203
|
+
}
|
|
4204
|
+
function findPackageRoot(startFile) {
|
|
4205
|
+
let dir = fs29.statSync(startFile).isDirectory() ? startFile : path35.dirname(startFile);
|
|
4206
|
+
while (true) {
|
|
4207
|
+
const candidate = path35.join(dir, "package.json");
|
|
4208
|
+
const pkg = readJson(candidate);
|
|
4209
|
+
if (pkg?.name === PACKAGE_NAME) return dir;
|
|
4210
|
+
const parent = path35.dirname(dir);
|
|
4211
|
+
if (parent === dir) return null;
|
|
4212
|
+
dir = parent;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
function inspectPackageLock(root) {
|
|
4216
|
+
const lockPath = path35.join(root, "package-lock.json");
|
|
4217
|
+
const lock = readJson(lockPath);
|
|
4218
|
+
if (!lock) return { present: false };
|
|
4219
|
+
const packageEntry = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
|
|
4220
|
+
if (!packageEntry) return { present: true, packageEntryPresent: false };
|
|
4221
|
+
const resolved = String(packageEntry.resolved || "");
|
|
4222
|
+
const blockedResolved = packageEntry.link === true || resolved.startsWith("file:") || resolved.startsWith("git+") || resolved.startsWith("github:") || resolved.startsWith("ssh://") || /^[a-z]:[\\/]/i.test(resolved);
|
|
4223
|
+
return {
|
|
4224
|
+
present: true,
|
|
4225
|
+
packageEntryPresent: true,
|
|
4226
|
+
link: packageEntry.link === true,
|
|
4227
|
+
resolved: resolved || null,
|
|
4228
|
+
blockedResolved
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
function getInstalledPackagePath(root) {
|
|
4232
|
+
return path35.join(root, "node_modules", ...PACKAGE_NAME.split("/"));
|
|
4233
|
+
}
|
|
4234
|
+
async function assertConsumerCommand(opts = {}) {
|
|
4235
|
+
const root = path35.resolve(opts.root || process.cwd());
|
|
4236
|
+
const packageJsonPath2 = path35.join(root, "package.json");
|
|
4237
|
+
const packageJson2 = readJson(packageJsonPath2);
|
|
4238
|
+
const checks = [];
|
|
4239
|
+
addCheck2(checks, "project-package-json-present", Boolean(packageJson2), { path: packageJsonPath2 });
|
|
4240
|
+
const dependency = findDependencySpec(packageJson2);
|
|
4241
|
+
addCheck2(checks, "declares-lbe-package-dependency", Boolean(dependency), dependency || {});
|
|
4242
|
+
const specCheck = dependency ? classifyDependencySpec(dependency.spec) : { ok: false, reason: "DEPENDENCY_NOT_DECLARED" };
|
|
4243
|
+
addCheck2(checks, "dependency-spec-is-registry-package", specCheck.ok, {
|
|
4244
|
+
spec: dependency?.spec || null,
|
|
4245
|
+
reason: specCheck.reason || null,
|
|
4246
|
+
blockedPrefix: specCheck.prefix || null
|
|
4247
|
+
});
|
|
4248
|
+
let resolvedEntry = null;
|
|
4249
|
+
let resolvedRoot = null;
|
|
4250
|
+
try {
|
|
4251
|
+
const req = createRequire(path35.join(root, "package.json"));
|
|
4252
|
+
resolvedEntry = req.resolve(PACKAGE_NAME);
|
|
4253
|
+
resolvedRoot = findPackageRoot(resolvedEntry);
|
|
4254
|
+
} catch (error) {
|
|
4255
|
+
addCheck2(checks, "lbe-package-resolves-from-project", false, { message: error.message });
|
|
4256
|
+
}
|
|
4257
|
+
if (resolvedEntry) {
|
|
4258
|
+
addCheck2(checks, "lbe-package-resolves-from-project", true, { resolvedEntry });
|
|
4259
|
+
}
|
|
4260
|
+
if (resolvedRoot) {
|
|
4261
|
+
const stat = fs29.lstatSync(resolvedRoot);
|
|
4262
|
+
addCheck2(checks, "lbe-package-is-not-symlink", !stat.isSymbolicLink(), { resolvedRoot });
|
|
4263
|
+
} else {
|
|
4264
|
+
addCheck2(checks, "lbe-package-is-not-symlink", false, { reason: "PACKAGE_ROOT_NOT_FOUND" });
|
|
4265
|
+
}
|
|
4266
|
+
const installedPackagePath = getInstalledPackagePath(root);
|
|
4267
|
+
if (fs29.existsSync(installedPackagePath)) {
|
|
4268
|
+
const installedStat = fs29.lstatSync(installedPackagePath);
|
|
4269
|
+
addCheck2(checks, "installed-node_modules-package-is-not-symlink", !installedStat.isSymbolicLink(), {
|
|
4270
|
+
installedPackagePath
|
|
4271
|
+
});
|
|
4272
|
+
} else {
|
|
4273
|
+
addCheck2(checks, "installed-node_modules-package-is-not-symlink", false, {
|
|
4274
|
+
installedPackagePath,
|
|
4275
|
+
reason: "PACKAGE_NOT_INSTALLED_LOCALLY"
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
const lockInfo = inspectPackageLock(root);
|
|
4279
|
+
if (lockInfo.present && lockInfo.packageEntryPresent) {
|
|
4280
|
+
addCheck2(checks, "package-lock-does-not-link-local-lbe", !lockInfo.blockedResolved, lockInfo);
|
|
4281
|
+
} else {
|
|
4282
|
+
addCheck2(checks, "package-lock-does-not-link-local-lbe", true, lockInfo);
|
|
4283
|
+
}
|
|
4284
|
+
const ok = checks.every((check) => check.ok);
|
|
4285
|
+
const result = {
|
|
4286
|
+
ok,
|
|
4287
|
+
command: "assert-consumer",
|
|
4288
|
+
package: PACKAGE_NAME,
|
|
4289
|
+
root,
|
|
4290
|
+
classification: ok ? "consumer-project-using-installed-registry-dependency" : "not-proven-consumer-installed-dependency",
|
|
4291
|
+
releaseClaimsAllowed: false,
|
|
4292
|
+
message: ok ? "This project consumes LBE as an installed package dependency. This does not certify LBE release safety." : "This project is not proven to consume LBE only as an installed registry dependency.",
|
|
4293
|
+
checks
|
|
4294
|
+
};
|
|
4295
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4296
|
+
if (!ok) {
|
|
4297
|
+
process.exitCode = 7;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4146
4301
|
// src/cli/main.js
|
|
4147
4302
|
function toBoolean4(value, defaultValue = false) {
|
|
4148
4303
|
if (value === void 0) return defaultValue;
|
|
@@ -4152,9 +4307,9 @@ function toBoolean4(value, defaultValue = false) {
|
|
|
4152
4307
|
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
4153
4308
|
return defaultValue;
|
|
4154
4309
|
}
|
|
4155
|
-
var __dirname2 =
|
|
4156
|
-
var packageJsonPath =
|
|
4157
|
-
var packageJson = JSON.parse(
|
|
4310
|
+
var __dirname2 = path36.dirname(fileURLToPath3(import.meta.url));
|
|
4311
|
+
var packageJsonPath = path36.join(__dirname2, "../../package.json");
|
|
4312
|
+
var packageJson = JSON.parse(fs30.readFileSync(packageJsonPath, "utf-8"));
|
|
4158
4313
|
async function main() {
|
|
4159
4314
|
const argv = process.argv.slice(2);
|
|
4160
4315
|
if (argv.includes("--version")) {
|
|
@@ -4177,7 +4332,7 @@ async function main() {
|
|
|
4177
4332
|
try {
|
|
4178
4333
|
if (opts["pub-key-file"]) {
|
|
4179
4334
|
try {
|
|
4180
|
-
opts["pub-key"] =
|
|
4335
|
+
opts["pub-key"] = fs30.readFileSync(path36.resolve(opts["pub-key-file"]), "utf-8").trim();
|
|
4181
4336
|
} catch (error) {
|
|
4182
4337
|
console.error(`Error reading public key file: ${error.message}`);
|
|
4183
4338
|
process.exit(1);
|
|
@@ -4185,7 +4340,7 @@ async function main() {
|
|
|
4185
4340
|
}
|
|
4186
4341
|
if (["verify", "dryrun", "run"].includes(command)) {
|
|
4187
4342
|
const integrityStrict = toBoolean4(opts["integrity-strict"], false);
|
|
4188
|
-
const integrityManifestPath =
|
|
4343
|
+
const integrityManifestPath = path36.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
|
|
4189
4344
|
const integrityResult = await performIntegrityCheck({
|
|
4190
4345
|
strict: integrityStrict,
|
|
4191
4346
|
manifestPath: integrityManifestPath
|
|
@@ -4246,6 +4401,9 @@ async function main() {
|
|
|
4246
4401
|
case "proof":
|
|
4247
4402
|
await proofCommand(opts);
|
|
4248
4403
|
break;
|
|
4404
|
+
case "assert-consumer":
|
|
4405
|
+
await assertConsumerCommand(opts);
|
|
4406
|
+
break;
|
|
4249
4407
|
default:
|
|
4250
4408
|
console.error(`Unknown command: ${command}`);
|
|
4251
4409
|
printHelp(packageJson.version);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@letterblack/lbe-core",
|
|
3
|
-
"version": "1.3.
|
|
2
|
+
"name": "@letterblack/lbe-core",
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Local-first execution governance SDK for AI agents. Agents propose → Controller validates → Adapters execute.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "types.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./types.d.ts",
|
|
11
|
-
"default": "./index.js"
|
|
12
|
-
},
|
|
13
|
-
"./engine": "./runtime/engine.js",
|
|
14
|
-
"./adapters": "./src/adapters/index.js",
|
|
15
|
-
"./exec": "./exec/index.js",
|
|
16
|
-
"./hooks/register.cjs": "./dist/hooks/register.cjs"
|
|
17
|
-
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./types.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./engine": "./runtime/engine.js",
|
|
14
|
+
"./adapters": "./src/adapters/index.js",
|
|
15
|
+
"./exec": "./exec/index.js",
|
|
16
|
+
"./hooks/register.cjs": "./dist/hooks/register.cjs"
|
|
17
|
+
},
|
|
18
18
|
"bin": {
|
|
19
19
|
"lbe": "bin/lbe.js"
|
|
20
20
|
},
|
|
@@ -29,24 +29,25 @@
|
|
|
29
29
|
"health": "node bin/lbe.js health --json true",
|
|
30
30
|
"integrity:generate": "node bin/lbe.js integrity-generate",
|
|
31
31
|
"integrity:check": "node bin/lbe.js integrity-check",
|
|
32
|
-
"build:engine": "node scripts/build-engine.js",
|
|
33
|
-
"build:public-sdk": "node scripts/build-public-sdk.mjs",
|
|
34
|
-
"build:public-exec": "node scripts/build-public-exec.mjs",
|
|
35
|
-
"build:package-runtime": "node scripts/build-package-runtime.mjs",
|
|
36
|
-
"proof": "node _proof.mjs",
|
|
37
|
-
"audit:public-docs": "node scripts/audit-public-docs.mjs",
|
|
38
|
-
"verify:package-runtime": "node scripts/verify-package-runtime.mjs",
|
|
39
|
-
"
|
|
40
|
-
"verify:public-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
32
|
+
"build:engine": "node scripts/build-engine.js",
|
|
33
|
+
"build:public-sdk": "node scripts/build-public-sdk.mjs",
|
|
34
|
+
"build:public-exec": "node scripts/build-public-exec.mjs",
|
|
35
|
+
"build:package-runtime": "node scripts/build-package-runtime.mjs",
|
|
36
|
+
"proof": "node _proof.mjs",
|
|
37
|
+
"audit:public-docs": "node scripts/audit-public-docs.mjs",
|
|
38
|
+
"verify:package-runtime": "node scripts/verify-package-runtime.mjs",
|
|
39
|
+
"assert-consumer": "node bin/lbe.js assert-consumer",
|
|
40
|
+
"verify:public-exec": "npm run proof && npm run build:public-exec && node scripts/check-public-exec.mjs && cd release-exec && npm pack --dry-run",
|
|
41
|
+
"verify:public-sdk": "npm run build:public-sdk && node scripts/check-public-artifact.mjs && cd release-public && npm pack --dry-run",
|
|
42
|
+
"engine:check": "node scripts/check-engine.js",
|
|
43
|
+
"pack:check": "npm pack --dry-run",
|
|
44
|
+
"audit:verify": "node bin/lbe.js audit-verify",
|
|
45
|
+
"guard:mainhead": "node scripts/mainhead-guard.mjs",
|
|
46
|
+
"hooks:install": "node scripts/install-git-hooks.mjs",
|
|
47
|
+
"prepack": "npm run build:package-runtime",
|
|
48
|
+
"validate:all": "npm run guard:mainhead && npm run engine:check && npm run lint && npm run test",
|
|
49
|
+
"publish:release": "node scripts/publish.mjs"
|
|
50
|
+
},
|
|
50
51
|
"keywords": [
|
|
51
52
|
"ai-governance",
|
|
52
53
|
"execution-governance",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// src/cli/commands/assertConsumer.js
|
|
2
|
+
// Guard that proves a consuming project is using LBE as an installed package,
|
|
3
|
+
// not treating a copied/source repository as release authority.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = '@letterblack/lbe-core';
|
|
10
|
+
const DEP_SECTIONS = [
|
|
11
|
+
'dependencies',
|
|
12
|
+
'devDependencies',
|
|
13
|
+
'optionalDependencies',
|
|
14
|
+
'peerDependencies'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function readJson(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addCheck(checks, name, ok, details = {}) {
|
|
26
|
+
checks.push({ name, ok, ...details });
|
|
27
|
+
return ok;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findDependencySpec(packageJson) {
|
|
31
|
+
if (!packageJson || typeof packageJson !== 'object') return null;
|
|
32
|
+
|
|
33
|
+
for (const section of DEP_SECTIONS) {
|
|
34
|
+
const value = packageJson[section]?.[PACKAGE_NAME];
|
|
35
|
+
if (value !== undefined) {
|
|
36
|
+
return { section, spec: String(value) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function classifyDependencySpec(spec) {
|
|
44
|
+
const raw = String(spec || '').trim();
|
|
45
|
+
const lower = raw.toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (!raw) {
|
|
48
|
+
return { ok: false, reason: 'EMPTY_DEPENDENCY_SPEC' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const blockedPrefixes = [
|
|
52
|
+
'file:',
|
|
53
|
+
'link:',
|
|
54
|
+
'workspace:',
|
|
55
|
+
'git+',
|
|
56
|
+
'github:',
|
|
57
|
+
'git://',
|
|
58
|
+
'ssh://'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const prefix of blockedPrefixes) {
|
|
62
|
+
if (lower.startsWith(prefix)) {
|
|
63
|
+
return { ok: false, reason: 'LOCAL_OR_GIT_DEPENDENCY_SPEC', prefix };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (/^[a-z]:[\\/]/i.test(raw) || raw.startsWith('./') || raw.startsWith('../')) {
|
|
68
|
+
return { ok: false, reason: 'PATH_DEPENDENCY_SPEC' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function findPackageRoot(startFile) {
|
|
75
|
+
let dir = fs.statSync(startFile).isDirectory() ? startFile : path.dirname(startFile);
|
|
76
|
+
|
|
77
|
+
while (true) {
|
|
78
|
+
const candidate = path.join(dir, 'package.json');
|
|
79
|
+
const pkg = readJson(candidate);
|
|
80
|
+
if (pkg?.name === PACKAGE_NAME) return dir;
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(dir);
|
|
83
|
+
if (parent === dir) return null;
|
|
84
|
+
dir = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inspectPackageLock(root) {
|
|
89
|
+
const lockPath = path.join(root, 'package-lock.json');
|
|
90
|
+
const lock = readJson(lockPath);
|
|
91
|
+
if (!lock) return { present: false };
|
|
92
|
+
|
|
93
|
+
const packageEntry = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
|
|
94
|
+
if (!packageEntry) return { present: true, packageEntryPresent: false };
|
|
95
|
+
|
|
96
|
+
const resolved = String(packageEntry.resolved || '');
|
|
97
|
+
const blockedResolved =
|
|
98
|
+
packageEntry.link === true ||
|
|
99
|
+
resolved.startsWith('file:') ||
|
|
100
|
+
resolved.startsWith('git+') ||
|
|
101
|
+
resolved.startsWith('github:') ||
|
|
102
|
+
resolved.startsWith('ssh://') ||
|
|
103
|
+
/^[a-z]:[\\/]/i.test(resolved);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
present: true,
|
|
107
|
+
packageEntryPresent: true,
|
|
108
|
+
link: packageEntry.link === true,
|
|
109
|
+
resolved: resolved || null,
|
|
110
|
+
blockedResolved
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getInstalledPackagePath(root) {
|
|
115
|
+
return path.join(root, 'node_modules', ...PACKAGE_NAME.split('/'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function assertConsumerCommand(opts = {}) {
|
|
119
|
+
const root = path.resolve(opts.root || process.cwd());
|
|
120
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
121
|
+
const packageJson = readJson(packageJsonPath);
|
|
122
|
+
const checks = [];
|
|
123
|
+
|
|
124
|
+
addCheck(checks, 'project-package-json-present', Boolean(packageJson), { path: packageJsonPath });
|
|
125
|
+
|
|
126
|
+
const dependency = findDependencySpec(packageJson);
|
|
127
|
+
addCheck(checks, 'declares-lbe-package-dependency', Boolean(dependency), dependency || {});
|
|
128
|
+
|
|
129
|
+
const specCheck = dependency ? classifyDependencySpec(dependency.spec) : { ok: false, reason: 'DEPENDENCY_NOT_DECLARED' };
|
|
130
|
+
addCheck(checks, 'dependency-spec-is-registry-package', specCheck.ok, {
|
|
131
|
+
spec: dependency?.spec || null,
|
|
132
|
+
reason: specCheck.reason || null,
|
|
133
|
+
blockedPrefix: specCheck.prefix || null
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
let resolvedEntry = null;
|
|
137
|
+
let resolvedRoot = null;
|
|
138
|
+
try {
|
|
139
|
+
const req = createRequire(path.join(root, 'package.json'));
|
|
140
|
+
resolvedEntry = req.resolve(PACKAGE_NAME);
|
|
141
|
+
resolvedRoot = findPackageRoot(resolvedEntry);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
addCheck(checks, 'lbe-package-resolves-from-project', false, { message: error.message });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (resolvedEntry) {
|
|
147
|
+
addCheck(checks, 'lbe-package-resolves-from-project', true, { resolvedEntry });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (resolvedRoot) {
|
|
151
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
152
|
+
addCheck(checks, 'lbe-package-is-not-symlink', !stat.isSymbolicLink(), { resolvedRoot });
|
|
153
|
+
} else {
|
|
154
|
+
addCheck(checks, 'lbe-package-is-not-symlink', false, { reason: 'PACKAGE_ROOT_NOT_FOUND' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const installedPackagePath = getInstalledPackagePath(root);
|
|
158
|
+
if (fs.existsSync(installedPackagePath)) {
|
|
159
|
+
const installedStat = fs.lstatSync(installedPackagePath);
|
|
160
|
+
addCheck(checks, 'installed-node_modules-package-is-not-symlink', !installedStat.isSymbolicLink(), {
|
|
161
|
+
installedPackagePath
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
addCheck(checks, 'installed-node_modules-package-is-not-symlink', false, {
|
|
165
|
+
installedPackagePath,
|
|
166
|
+
reason: 'PACKAGE_NOT_INSTALLED_LOCALLY'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lockInfo = inspectPackageLock(root);
|
|
171
|
+
if (lockInfo.present && lockInfo.packageEntryPresent) {
|
|
172
|
+
addCheck(checks, 'package-lock-does-not-link-local-lbe', !lockInfo.blockedResolved, lockInfo);
|
|
173
|
+
} else {
|
|
174
|
+
addCheck(checks, 'package-lock-does-not-link-local-lbe', true, lockInfo);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ok = checks.every((check) => check.ok);
|
|
178
|
+
const result = {
|
|
179
|
+
ok,
|
|
180
|
+
command: 'assert-consumer',
|
|
181
|
+
package: PACKAGE_NAME,
|
|
182
|
+
root,
|
|
183
|
+
classification: ok
|
|
184
|
+
? 'consumer-project-using-installed-registry-dependency'
|
|
185
|
+
: 'not-proven-consumer-installed-dependency',
|
|
186
|
+
releaseClaimsAllowed: false,
|
|
187
|
+
message: ok
|
|
188
|
+
? 'This project consumes LBE as an installed package dependency. This does not certify LBE release safety.'
|
|
189
|
+
: 'This project is not proven to consume LBE only as an installed registry dependency.',
|
|
190
|
+
checks
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
console.log(JSON.stringify(result, null, 2));
|
|
194
|
+
|
|
195
|
+
if (!ok) {
|
|
196
|
+
process.exitCode = 7;
|
|
197
|
+
}
|
|
198
|
+
}
|