cachelint 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/LICENSE +27 -0
- package/LICENSE-PRO.md +67 -0
- package/README.md +298 -0
- package/dist/check-2ONJCYKP.js +23 -0
- package/dist/chunk-2JSY5BQA.js +340 -0
- package/dist/chunk-4GHSUBAY.js +28 -0
- package/dist/chunk-4LB5LF2P.js +51 -0
- package/dist/chunk-BUZCVKMZ.js +246 -0
- package/dist/chunk-DYQPXD3G.js +92 -0
- package/dist/chunk-GXASKZ4Z.js +182 -0
- package/dist/chunk-JZU4PCTS.js +80 -0
- package/dist/chunk-MW7BXKPR.js +593 -0
- package/dist/chunk-O7ZE6CFI.js +32 -0
- package/dist/chunk-SQ4IAMTX.js +65 -0
- package/dist/chunk-UUVSBJ62.js +77 -0
- package/dist/chunk-VWE5TDL5.js +91 -0
- package/dist/chunk-WKFGRNYK.js +494 -0
- package/dist/chunk-WPJYUQMU.js +26 -0
- package/dist/chunk-XKRPGMZT.js +197 -0
- package/dist/chunk-ZSEZQ422.js +286 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +199 -0
- package/dist/gate-63NKQRXL.js +16 -0
- package/dist/hash-XUTGK6SB.js +15 -0
- package/dist/index.d.ts +1200 -0
- package/dist/index.js +184 -0
- package/dist/license-GFDBTQHG.js +19 -0
- package/dist/lint-PQHIDWOY.js +19 -0
- package/dist/sarif-EQ6VJNGN.js +63 -0
- package/dist/step-summary-NKD4N7AY.js +160 -0
- package/dist/store-KZIQANIT.js +14 -0
- package/package.json +85 -0
- package/schema/cachelint.config.schema.json +114 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/lockfile/diff.ts
|
|
2
|
+
function isRegression(d) {
|
|
3
|
+
return d.policyChanges.length > 0 || d.addedBundles.length > 0 || d.removedBundles.length > 0 || d.changedBundles.length > 0;
|
|
4
|
+
}
|
|
5
|
+
function diffLockfiles(committed, current) {
|
|
6
|
+
const policyChanges = [];
|
|
7
|
+
if (committed.normalizationPolicyVersion !== current.normalizationPolicyVersion) {
|
|
8
|
+
policyChanges.push({
|
|
9
|
+
field: "normalizationPolicyVersion",
|
|
10
|
+
committed: committed.normalizationPolicyVersion,
|
|
11
|
+
current: current.normalizationPolicyVersion
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (committed.ruleTableVersion !== current.ruleTableVersion) {
|
|
15
|
+
policyChanges.push({
|
|
16
|
+
field: "ruleTableVersion",
|
|
17
|
+
committed: committed.ruleTableVersion,
|
|
18
|
+
current: current.ruleTableVersion
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const byName = (lf) => new Map(lf.bundles.map((b) => [b.name, b]));
|
|
22
|
+
const committedByName = byName(committed);
|
|
23
|
+
const currentByName = byName(current);
|
|
24
|
+
const addedBundles = [...currentByName.keys()].filter((n) => !committedByName.has(n)).sort(cmp);
|
|
25
|
+
const removedBundles = [...committedByName.keys()].filter((n) => !currentByName.has(n)).sort(cmp);
|
|
26
|
+
const changedBundles = [];
|
|
27
|
+
for (const name of [...committedByName.keys()].filter((n) => currentByName.has(n)).sort(cmp)) {
|
|
28
|
+
const c = committedByName.get(name);
|
|
29
|
+
const n = currentByName.get(name);
|
|
30
|
+
if (c.stablePrefixHash === n.stablePrefixHash && sameFiles(c.files, n.files)) continue;
|
|
31
|
+
changedBundles.push({
|
|
32
|
+
name,
|
|
33
|
+
committedHash: c.stablePrefixHash,
|
|
34
|
+
currentHash: n.stablePrefixHash,
|
|
35
|
+
fileChanges: fileChanges(c.files, n.files)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { policyChanges, addedBundles, removedBundles, changedBundles };
|
|
39
|
+
}
|
|
40
|
+
function sameFiles(a, b) {
|
|
41
|
+
if (a.length !== b.length) return false;
|
|
42
|
+
for (let i = 0; i < a.length; i++) {
|
|
43
|
+
if (a[i].path !== b[i].path || a[i].sha256 !== b[i].sha256) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function fileChanges(a, b) {
|
|
48
|
+
const am = new Map(a.map((f) => [f.path, f.sha256]));
|
|
49
|
+
const bm = new Map(b.map((f) => [f.path, f.sha256]));
|
|
50
|
+
const paths = [.../* @__PURE__ */ new Set([...am.keys(), ...bm.keys()])].sort(cmp);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const p of paths) {
|
|
53
|
+
const committed = am.get(p) ?? null;
|
|
54
|
+
const current = bm.get(p) ?? null;
|
|
55
|
+
if (committed !== current) out.push({ path: p, committed, current });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function cmp(a, b) {
|
|
60
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
61
|
+
}
|
|
62
|
+
function renderDiffHuman(d) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
for (const pc of d.policyChanges) {
|
|
65
|
+
lines.push(
|
|
66
|
+
`policy changed: ${pc.field} ${pc.committed} \u2192 ${pc.current} \u2014 this is a deliberate cachelint upgrade; re-run \`cachelint hash\` and commit cachelint.lock.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
for (const name of d.addedBundles) lines.push(`+ bundle "${name}" is new (not in cachelint.lock)`);
|
|
70
|
+
for (const name of d.removedBundles) lines.push(`- bundle "${name}" is in cachelint.lock but no longer produced`);
|
|
71
|
+
for (const cb of d.changedBundles) {
|
|
72
|
+
lines.push(`~ bundle "${cb.name}" stablePrefixHash moved:`);
|
|
73
|
+
lines.push(` was: ${cb.committedHash}`);
|
|
74
|
+
lines.push(` now: ${cb.currentHash}`);
|
|
75
|
+
for (const fc of cb.fileChanges) {
|
|
76
|
+
const was = fc.committed ? fc.committed.slice(0, 12) : "(absent)";
|
|
77
|
+
const now = fc.current ? fc.current.slice(0, 12) : "(absent)";
|
|
78
|
+
lines.push(` ${fc.path}: ${was} \u2192 ${now}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (lines.length > 0) {
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push("If this change is intentional: run `cachelint hash \u2026` and commit the updated cachelint.lock.");
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
isRegression,
|
|
90
|
+
diffLockfiles,
|
|
91
|
+
renderDiffHuman
|
|
92
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readStoredLicense
|
|
3
|
+
} from "./chunk-4LB5LF2P.js";
|
|
4
|
+
import {
|
|
5
|
+
LicenseError
|
|
6
|
+
} from "./chunk-XKRPGMZT.js";
|
|
7
|
+
|
|
8
|
+
// src/license/verify.ts
|
|
9
|
+
import { verify as cryptoVerify } from "crypto";
|
|
10
|
+
|
|
11
|
+
// src/license/publicKey.ts
|
|
12
|
+
import { createPublicKey } from "crypto";
|
|
13
|
+
var TRUSTED_KEYS = [
|
|
14
|
+
{
|
|
15
|
+
kid: 1,
|
|
16
|
+
spkiPem: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAudzwHFefdjkvu9doI3XtzSjsdHUteeUUWCWECsQ8SIM=\n-----END PUBLIC KEY-----\n"
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
var REVOKED_NONCES = /* @__PURE__ */ new Set([
|
|
20
|
+
// (none yet)
|
|
21
|
+
]);
|
|
22
|
+
function testKeyOverrideAllowed() {
|
|
23
|
+
return process.env["VITEST"] !== void 0 || process.env["NODE_ENV"] === "test";
|
|
24
|
+
}
|
|
25
|
+
function trustedKeys() {
|
|
26
|
+
const out = /* @__PURE__ */ new Map();
|
|
27
|
+
if (testKeyOverrideAllowed()) {
|
|
28
|
+
const override = process.env["CACHELINT_PUBLIC_KEY"];
|
|
29
|
+
if (override !== void 0 && override.trim() !== "") {
|
|
30
|
+
out.set(1, parsePublicKey(override.trim()));
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const { kid, spkiPem } of TRUSTED_KEYS) out.set(kid, parsePublicKey(spkiPem));
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function parsePublicKey(material) {
|
|
38
|
+
try {
|
|
39
|
+
if (material.includes("BEGIN ")) {
|
|
40
|
+
return createPublicKey({ key: material, format: "pem" });
|
|
41
|
+
}
|
|
42
|
+
return createPublicKey({ key: Buffer.from(material, "base64url"), format: "der", type: "spki" });
|
|
43
|
+
} catch {
|
|
44
|
+
throw new LicenseError({
|
|
45
|
+
code: "PRO_BAD_PUBLIC_KEY",
|
|
46
|
+
message: "could not parse a trusted public key (check CACHELINT_PUBLIC_KEY if you set it)"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/license/verify.ts
|
|
52
|
+
var GRACE_PERIOD_DAYS = 14;
|
|
53
|
+
var GRACE_MS = GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1e3;
|
|
54
|
+
var KNOWN_FEATURES = /* @__PURE__ */ new Set(["sarif", "exact", "pack"]);
|
|
55
|
+
function verifyLicenseToken(token, now = Date.now()) {
|
|
56
|
+
const bad = (msg, code = "PRO_INVALID_LICENSE") => {
|
|
57
|
+
throw new LicenseError({ code, message: msg, hint: "Re-copy the key from your purchase email, or contact support." });
|
|
58
|
+
};
|
|
59
|
+
if (typeof token !== "string" || token.length < 16 || !token.includes(".")) bad("license key is malformed");
|
|
60
|
+
const dot = token.indexOf(".");
|
|
61
|
+
if (token.indexOf(".", dot + 1) !== -1) bad("license key is malformed (more than one '.')");
|
|
62
|
+
const signedPart = token.slice(0, dot);
|
|
63
|
+
const sigPart = token.slice(dot + 1);
|
|
64
|
+
if (signedPart === "" || sigPart === "") bad("license key is malformed (empty segment)");
|
|
65
|
+
let payloadBytes;
|
|
66
|
+
let sigBytes;
|
|
67
|
+
try {
|
|
68
|
+
payloadBytes = Buffer.from(signedPart, "base64url");
|
|
69
|
+
sigBytes = Buffer.from(sigPart, "base64url");
|
|
70
|
+
} catch {
|
|
71
|
+
return bad("license key is not valid base64url");
|
|
72
|
+
}
|
|
73
|
+
if (sigBytes.length !== 64) bad("license signature has the wrong length");
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = JSON.parse(payloadBytes.toString("utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return bad("license payload is not valid JSON");
|
|
79
|
+
}
|
|
80
|
+
if (typeof raw !== "object" || raw === null) return bad("license payload is not an object");
|
|
81
|
+
const p = raw;
|
|
82
|
+
if (p["v"] !== 1) bad(`unsupported license payload version (${String(p["v"])})`);
|
|
83
|
+
if (typeof p["kid"] !== "number") bad("license payload missing 'kid'");
|
|
84
|
+
if (typeof p["sub"] !== "string" || p["sub"].length === 0) bad("license payload missing 'sub'");
|
|
85
|
+
if (p["plan"] !== "pro") bad(`license plan '${String(p["plan"])}' is not recognized`);
|
|
86
|
+
if (typeof p["iat"] !== "number" || typeof p["exp"] !== "number") bad("license payload missing iat/exp");
|
|
87
|
+
if (typeof p["nonce"] !== "string" || p["nonce"].length === 0) bad("license payload missing 'nonce'");
|
|
88
|
+
const features = Array.isArray(p["features"]) ? p["features"] : [];
|
|
89
|
+
const cleanFeatures = features.filter((f) => typeof f === "string" && KNOWN_FEATURES.has(f));
|
|
90
|
+
const kid = p["kid"];
|
|
91
|
+
const keys = trustedKeys();
|
|
92
|
+
const key = keys.get(kid);
|
|
93
|
+
if (key === void 0) bad(`license was signed with an unknown key id (kid=${kid})`, "PRO_UNKNOWN_KID");
|
|
94
|
+
let ok = false;
|
|
95
|
+
try {
|
|
96
|
+
ok = cryptoVerify(null, Buffer.from(signedPart, "utf8"), key, sigBytes);
|
|
97
|
+
} catch {
|
|
98
|
+
ok = false;
|
|
99
|
+
}
|
|
100
|
+
if (!ok) bad("license signature does not verify");
|
|
101
|
+
const nonce = p["nonce"];
|
|
102
|
+
if (REVOKED_NONCES.has(nonce)) bad("this license key has been revoked", "PRO_REVOKED");
|
|
103
|
+
const expMs = p["exp"] * 1e3;
|
|
104
|
+
let inGracePeriod = false;
|
|
105
|
+
let warning = null;
|
|
106
|
+
if (now > expMs) {
|
|
107
|
+
if (now <= expMs + GRACE_MS) {
|
|
108
|
+
inGracePeriod = true;
|
|
109
|
+
const daysLeft = Math.max(0, Math.ceil((expMs + GRACE_MS - now) / (24 * 60 * 60 * 1e3)));
|
|
110
|
+
warning = `license expired on ${new Date(expMs).toISOString().slice(0, 10)} \u2014 still honored for ${daysLeft} more day(s); renew to keep Pro features.`;
|
|
111
|
+
} else {
|
|
112
|
+
bad(`license expired on ${new Date(expMs).toISOString().slice(0, 10)} (past the ${GRACE_PERIOD_DAYS}-day grace window)`, "PRO_EXPIRED");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const payload = {
|
|
116
|
+
v: 1,
|
|
117
|
+
sub: p["sub"],
|
|
118
|
+
plan: "pro",
|
|
119
|
+
features: cleanFeatures,
|
|
120
|
+
iat: p["iat"],
|
|
121
|
+
exp: p["exp"],
|
|
122
|
+
nonce,
|
|
123
|
+
kid
|
|
124
|
+
};
|
|
125
|
+
return { payload, inGracePeriod, warning };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/license/gate.ts
|
|
129
|
+
var PRO_UPGRADE_HINT = "Pro feature \u2014 see https://github.com/davioe/cachelint#pro, then run `cachelint activate <license-key>` (or set CACHELINT_LICENSE_KEY).";
|
|
130
|
+
var cached;
|
|
131
|
+
function loadActive() {
|
|
132
|
+
if (cached !== void 0) return cached.info;
|
|
133
|
+
let info = null;
|
|
134
|
+
try {
|
|
135
|
+
const stored = readStoredLicense();
|
|
136
|
+
if (stored !== null) {
|
|
137
|
+
const v = verifyLicenseToken(stored.token);
|
|
138
|
+
info = {
|
|
139
|
+
plan: "pro",
|
|
140
|
+
sub: v.payload.sub,
|
|
141
|
+
exp: v.payload.exp,
|
|
142
|
+
kid: v.payload.kid,
|
|
143
|
+
features: v.payload.features,
|
|
144
|
+
inGracePeriod: v.inGracePeriod,
|
|
145
|
+
source: stored.source
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
info = null;
|
|
150
|
+
}
|
|
151
|
+
cached = { info };
|
|
152
|
+
return info;
|
|
153
|
+
}
|
|
154
|
+
function activeLicenseInfo() {
|
|
155
|
+
return loadActive();
|
|
156
|
+
}
|
|
157
|
+
function isPro(feature) {
|
|
158
|
+
const info = loadActive();
|
|
159
|
+
if (info === null) return false;
|
|
160
|
+
return feature === void 0 || info.features.includes(feature);
|
|
161
|
+
}
|
|
162
|
+
function requirePro(feature, whatHuman) {
|
|
163
|
+
if (isPro(feature)) return;
|
|
164
|
+
throw new LicenseError({
|
|
165
|
+
code: "PRO_REQUIRED",
|
|
166
|
+
message: `${whatHuman} requires a cachelint Pro license`,
|
|
167
|
+
hint: PRO_UPGRADE_HINT
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function _resetLicenseCache() {
|
|
171
|
+
cached = void 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
GRACE_PERIOD_DAYS,
|
|
176
|
+
verifyLicenseToken,
|
|
177
|
+
PRO_UPGRADE_HINT,
|
|
178
|
+
activeLicenseInfo,
|
|
179
|
+
isPro,
|
|
180
|
+
requirePro,
|
|
181
|
+
_resetLicenseCache
|
|
182
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
_resetLicenseCache,
|
|
3
|
+
activeLicenseInfo,
|
|
4
|
+
verifyLicenseToken
|
|
5
|
+
} from "./chunk-GXASKZ4Z.js";
|
|
6
|
+
import {
|
|
7
|
+
clearStoredLicense,
|
|
8
|
+
hasEnvLicense,
|
|
9
|
+
licenseFilePath,
|
|
10
|
+
writeStoredLicense
|
|
11
|
+
} from "./chunk-4LB5LF2P.js";
|
|
12
|
+
import {
|
|
13
|
+
LicenseError
|
|
14
|
+
} from "./chunk-XKRPGMZT.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/license.ts
|
|
17
|
+
function activate(token) {
|
|
18
|
+
const v = verifyLicenseToken(token);
|
|
19
|
+
const storedAt = writeStoredLicense(token.trim());
|
|
20
|
+
_resetLicenseCache();
|
|
21
|
+
const info = activeLicenseInfo();
|
|
22
|
+
if (info === null) {
|
|
23
|
+
throw new LicenseError({ code: "PRO_ACTIVATE_FAILED", message: "license verified but could not be re-read after storing" });
|
|
24
|
+
}
|
|
25
|
+
return { info, storedAt, warning: v.warning };
|
|
26
|
+
}
|
|
27
|
+
function licenseStatus() {
|
|
28
|
+
const info = activeLicenseInfo();
|
|
29
|
+
return { active: info !== null, info, filePath: licenseFilePath() };
|
|
30
|
+
}
|
|
31
|
+
function deactivate() {
|
|
32
|
+
const removed = clearStoredLicense();
|
|
33
|
+
_resetLicenseCache();
|
|
34
|
+
return { removed, envStillActive: hasEnvLicense(), filePath: licenseFilePath() };
|
|
35
|
+
}
|
|
36
|
+
function renderActivateHuman(r) {
|
|
37
|
+
const lines = [
|
|
38
|
+
`cachelint: Pro license activated \u2014 thanks!`,
|
|
39
|
+
` buyer: ${r.info.sub}`,
|
|
40
|
+
` expires: ${new Date(r.info.exp * 1e3).toISOString().slice(0, 10)}`,
|
|
41
|
+
` features: ${r.info.features.length > 0 ? r.info.features.join(", ") : "(none listed)"}`,
|
|
42
|
+
` stored: ${r.storedAt}`
|
|
43
|
+
];
|
|
44
|
+
if (r.warning) lines.push(` note: ${r.warning}`);
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
function renderLicenseStatusHuman(s) {
|
|
48
|
+
if (!s.active || s.info === null) {
|
|
49
|
+
return [
|
|
50
|
+
"cachelint: no Pro license active (running the free core).",
|
|
51
|
+
` a license would be read from: ${s.filePath} (or the CACHELINT_LICENSE_KEY env var)`,
|
|
52
|
+
" buy + activate: https://github.com/davioe/cachelint#pro"
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
const i = s.info;
|
|
56
|
+
const lines = [
|
|
57
|
+
`cachelint: Pro license active.`,
|
|
58
|
+
` buyer: ${i.sub}`,
|
|
59
|
+
` expires: ${new Date(i.exp * 1e3).toISOString().slice(0, 10)}${i.inGracePeriod ? " (in grace period \u2014 renew soon)" : ""}`,
|
|
60
|
+
` features: ${i.features.length > 0 ? i.features.join(", ") : "(none listed)"}`,
|
|
61
|
+
` kid: ${i.kid}`,
|
|
62
|
+
` source: ${i.source}`
|
|
63
|
+
];
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
function renderDeactivateHuman(r) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(r.removed ? `cachelint: removed the stored license (${r.filePath}).` : `cachelint: no stored license file to remove (${r.filePath}).`);
|
|
69
|
+
if (r.envStillActive) lines.push(" note: CACHELINT_LICENSE_KEY is still set in the environment \u2014 Pro stays active until you unset it.");
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
activate,
|
|
75
|
+
licenseStatus,
|
|
76
|
+
deactivate,
|
|
77
|
+
renderActivateHuman,
|
|
78
|
+
renderLicenseStatusHuman,
|
|
79
|
+
renderDeactivateHuman
|
|
80
|
+
};
|