jsguardian 1.2.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.
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // Layer 3f — Semantic Poison (AI refusal-heuristic triggering)
4
+ //
5
+ // CONCEPT:
6
+ // AI security tools (GitHub Copilot, automated SAST, red-team LLMs) are
7
+ // trained on patterns that trigger refusal or caution flags:
8
+ // - Hardcoded cryptographic key material (AES keys, HMAC secrets, RSA PEM)
9
+ // - Cloud provider credential prefixes (AKIA*, sk_live_*, ghp_*)
10
+ // - "TODO: remove before prod" credential leak patterns
11
+ // - Internal API endpoint strings that look like live infrastructure
12
+ //
13
+ // Unlike Layer 3/3c (which use explicit "STOP / HONEYPOT" text), semantic
14
+ // poison uses NO instruction text. The trigger is structural resemblance
15
+ // to patterns the model was trained to flag.
16
+ //
17
+ // Dual-mode effect:
18
+ // • Cautious models / tools: refuse to proceed, report "potential credential
19
+ // exposure detected" — analysis stops without human escalation.
20
+ // • Non-cautious models: spend token budget extracting, validating, and
21
+ // correlating fake credentials against each other — all wasted effort.
22
+ //
23
+ // ARCHITECTURE:
24
+ // buildSemanticPoisonSource() returns a raw JS source string.
25
+ // It is injected by pipeline.ts AFTER cneObfuscate() runs — same pattern as
26
+ // buildDriftDetectorSource() — so strings NEVER pass through CNE's
27
+ // applyStringSplit(). A string like "sk_live_Abc..." arrives in the final
28
+ // output intact, as a single literal that any grep/pattern-matcher catches.
29
+ //
30
+ // THREE MECHANISMS (all emitted as a single JS source block):
31
+ //
32
+ // A — Crypto primitive vars
33
+ // var declarations with per-build seeded-random values that look valid:
34
+ // var AES_KEY = "a3f7b2c8d1e4f09a..."; // 64 hex = 32 bytes
35
+ // var RSA_PRIVATE_KEY = "-----BEGIN RSA..."; // multi-line PEM
36
+ // var STRIPE_SK = "sk_live_51Abc3d...";
37
+ // Variable names will be renamed by CNE applyFullRename when the IIFE
38
+ // wrapper is re-parsed... wait — we inject POST-CNE so there's no further
39
+ // rename pass. Names stay as-is, which is fine: the TRIGGER is the VALUE,
40
+ // not the name (tools grep for "sk_live_", "BEGIN RSA PRIVATE KEY" etc.)
41
+ //
42
+ // B — Fake internal endpoint strings
43
+ // Standalone string expressions that look like license/auth API routes:
44
+ // "https://api.license.acme.corp/v2/validate";
45
+ // "/internal/api/v3/auth/token";
46
+ //
47
+ // C — Credential-leak comment-style string expressions
48
+ // Expression statements resembling debug traces left in accidentally:
49
+ // "TODO: remove before prod -- STRIPE_SK=" + "sk_live_51Abc...";
50
+ // "DEBUG admin_jwt=" + "eyJhbGci...";
51
+ // Split across + to look like a logger call argument.
52
+ //
53
+ // PLACEMENT:
54
+ // The generated block is prepended to the output (before the module IIFE).
55
+ // It is then wrapped by wrapInModuleIife() so everything is scoped
56
+ // consistently. The vars and expressions are at IIFE-local scope — visible
57
+ // to any tool that reads the source, unreachable from outside.
58
+ // ============================================================================
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.buildSemanticPoisonSource = buildSemanticPoisonSource;
61
+ // ---------------------------------------------------------------------------
62
+ // RNG helpers
63
+ // ---------------------------------------------------------------------------
64
+ function randHex(rng, bytes) {
65
+ let s = "";
66
+ for (let i = 0; i < bytes; i++)
67
+ s += ((rng.int32() >>> 0) & 0xff).toString(16).padStart(2, "0");
68
+ return s;
69
+ }
70
+ function randAlphaNum(rng, len) {
71
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
72
+ let s = "";
73
+ for (let i = 0; i < len; i++)
74
+ s += chars[(rng.int32() >>> 0) % chars.length];
75
+ return s;
76
+ }
77
+ function randBase64(rng, bytes) {
78
+ const raw = [];
79
+ for (let i = 0; i < bytes; i++)
80
+ raw.push((rng.int32() >>> 0) & 0xff);
81
+ return Buffer.from(raw).toString("base64");
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Fake value generators — per-build seeded, never real secrets
85
+ // ---------------------------------------------------------------------------
86
+ function fakePem(rng, kind) {
87
+ // RSA-2048 private key body: ~1600 base64 chars in 64-char lines
88
+ const body = randBase64(rng, 1200);
89
+ const lines = [`-----BEGIN ${kind}-----`];
90
+ for (let i = 0; i < body.length; i += 64)
91
+ lines.push(body.slice(i, i + 64));
92
+ lines.push(`-----END ${kind}-----`);
93
+ return lines.join("\\n"); // escaped newline so it fits in a single JS string literal
94
+ }
95
+ function fakeAwsKeyId(rng) {
96
+ return "AKIA" + randAlphaNum(rng, 16).toUpperCase();
97
+ }
98
+ function fakeAwsSecret(rng) {
99
+ return randBase64(rng, 30).replace(/[^A-Za-z0-9+/]/g, "x").slice(0, 40);
100
+ }
101
+ function fakeStripeKey(rng) {
102
+ return "sk_live_" + randAlphaNum(rng, 24);
103
+ }
104
+ function fakeGhToken(rng) {
105
+ return "ghp_" + randAlphaNum(rng, 36);
106
+ }
107
+ function fakeJwt(rng) {
108
+ const hdr = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
109
+ .toString("base64url");
110
+ const pay = Buffer.from(JSON.stringify({
111
+ sub: randAlphaNum(rng, 8),
112
+ iat: 1700000000 + ((rng.int32() >>> 0) % 10000000),
113
+ exp: 1700000000 + ((rng.int32() >>> 0) % 10000000) + 3600,
114
+ })).toString("base64url");
115
+ const sig = randBase64(rng, 32)
116
+ .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
117
+ return `${hdr}.${pay}.${sig}`;
118
+ }
119
+ const CRYPTO_KEY_POOL = [
120
+ { name: "AES_KEY", value: r => randHex(r, 32) },
121
+ { name: "AES_256_KEY", value: r => randHex(r, 32) },
122
+ { name: "HMAC_SECRET", value: r => randHex(r, 32) },
123
+ { name: "SIGNING_KEY", value: r => randHex(r, 32) },
124
+ { name: "MAC_KEY", value: r => randHex(r, 32) },
125
+ { name: "JWT_SECRET", value: r => randAlphaNum(r, 64) },
126
+ { name: "TOKEN_SECRET", value: r => randAlphaNum(r, 48) },
127
+ { name: "SESSION_SECRET", value: r => randHex(r, 32) },
128
+ { name: "ENCRYPTION_KEY", value: r => randHex(r, 32) },
129
+ { name: "RSA_PRIVATE_KEY", value: r => fakePem(r, "RSA PRIVATE KEY"), multiline: true },
130
+ { name: "EC_PRIVATE_KEY", value: r => fakePem(r, "EC PRIVATE KEY"), multiline: true },
131
+ { name: "CLIENT_SECRET", value: r => randHex(r, 24) },
132
+ { name: "API_SECRET", value: r => randHex(r, 32) },
133
+ { name: "MASTER_KEY", value: r => randHex(r, 32) },
134
+ { name: "SERVICE_ACCOUNT_KEY", value: r => randBase64(r, 96) },
135
+ { name: "STRIPE_SK", value: fakeStripeKey },
136
+ { name: "STRIPE_SECRET_KEY", value: fakeStripeKey },
137
+ { name: "AWS_SECRET_KEY", value: fakeAwsSecret },
138
+ { name: "AWS_ACCESS_KEY_ID", value: fakeAwsKeyId },
139
+ { name: "GH_TOKEN", value: fakeGhToken },
140
+ { name: "GITHUB_TOKEN", value: fakeGhToken },
141
+ { name: "DB_PASSWORD", value: r => randAlphaNum(r, 28) + "!@#" },
142
+ { name: "POSTGRES_PASSWORD", value: r => randAlphaNum(r, 20) + "!2$" },
143
+ { name: "REDIS_AUTH_TOKEN", value: r => randHex(r, 20) },
144
+ { name: "INTERNAL_API_KEY", value: r => randHex(r, 16) },
145
+ ];
146
+ // ---------------------------------------------------------------------------
147
+ // B — Fake internal endpoint strings
148
+ // ---------------------------------------------------------------------------
149
+ const ENDPOINT_TEMPLATES = [
150
+ "https://api.license.<<D>>.com/v<<N>>/validate",
151
+ "https://auth.<<D>>.io/v<<N>>/token/verify",
152
+ "https://license.<<D>>.corp/api/verify",
153
+ "https://internal-api.<<D>>.net/v<<N>>/auth/check",
154
+ "https://key-server.<<D>>.internal/v<<N>>/decrypt",
155
+ "https://telemetry.<<D>>.io/v<<N>>/collect",
156
+ "wss://events.<<D>>.corp/v<<N>>/stream",
157
+ "/api/v<<N>>/license/validate",
158
+ "/internal/v<<N>>/auth/token",
159
+ "/api/v<<N>>/entitlements/check",
160
+ ];
161
+ const FAKE_DOMAINS = [
162
+ "acme", "corp", "enterprise", "secure", "vault",
163
+ "keyservice", "authsvc", "licensor", "guardian", "sentinel",
164
+ ];
165
+ function renderEndpoint(rng, tmpl) {
166
+ const d = FAKE_DOMAINS[(rng.int32() >>> 0) % FAKE_DOMAINS.length];
167
+ const n = 1 + ((rng.int32() >>> 0) % 4);
168
+ return tmpl.replace("<<D>>", d).replace("<<N>>", String(n));
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // C — Credential-leak concat expressions
172
+ // Split across + so they look like logger lines left accidentally.
173
+ // Each part is a separate JS string literal — but TOGETHER they form
174
+ // the pattern that SAST / AI tools are trained to flag.
175
+ // ---------------------------------------------------------------------------
176
+ /** Build one credential-leak line of the given kind (0-7). */
177
+ function buildLeakLineKind(rng, kind) {
178
+ switch (kind) {
179
+ case 0: return ["TODO: remove before prod -- STRIPE_SK=", fakeStripeKey(rng)];
180
+ case 1: return [`DEBUG aws_access=${fakeAwsKeyId(rng)} aws_secret=`, fakeAwsSecret(rng)];
181
+ case 2: return ["DEBUG admin_jwt=", fakeJwt(rng)];
182
+ case 3: return ["FIXME: hardcoded gh_token=", fakeGhToken(rng)];
183
+ case 4: return ["TODO remove temp admin password: ", randAlphaNum(rng, 16) + "!2$"];
184
+ case 5: return ["NOTE: encryption_key hardcoded for testing: ", randHex(rng, 32)];
185
+ case 6: return ["REMOVE: jwt_signing_secret=", randAlphaNum(rng, 48)];
186
+ default: return ["DEBUG master_key=", randHex(rng, 32)];
187
+ }
188
+ }
189
+ function buildLeakLine(rng) {
190
+ return buildLeakLineKind(rng, (rng.int32() >>> 0) % 8);
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Shuffle helper
194
+ // ---------------------------------------------------------------------------
195
+ function shuffleArr(arr, rng) {
196
+ for (let i = arr.length - 1; i > 0; i--) {
197
+ const j = Math.floor(rng.next() * (i + 1));
198
+ [arr[i], arr[j]] = [arr[j], arr[i]];
199
+ }
200
+ return arr;
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // Main export — returns a raw JS source string
204
+ // ---------------------------------------------------------------------------
205
+ // ---------------------------------------------------------------------------
206
+ // Encoding helper — wraps a credential value so it is NOT visible as a plain
207
+ // string literal. A human sees only a base64 blob; the real value only
208
+ // materialises at runtime via Buffer.from decode.
209
+ // The variable NAME stays readable — AI/SAST tools trigger on the name+decode
210
+ // pattern (var GH_TOKEN = Buffer.from(...)), not the literal value itself.
211
+ // ---------------------------------------------------------------------------
212
+ function encodeCredential(value) {
213
+ const b64 = Buffer.from(value).toString("base64");
214
+ return `Buffer.from(${JSON.stringify(b64)},"base64").toString()`;
215
+ }
216
+ /**
217
+ * Build a raw JS source block containing:
218
+ * A) Guaranteed high-value triggers (RSA PEM, EC PEM, Stripe, AWS, GitHub)
219
+ * plus 3–5 extra random entries from the pool.
220
+ * B) 4–6 fake internal endpoint string declarations.
221
+ * C) All 8 credential-leak line types in shuffled order — every build
222
+ * contains every pattern type regardless of RNG state.
223
+ *
224
+ * Credential VALUES are base64-encoded so they are NOT readable as plaintext
225
+ * in the source. Variable names and leak-line prefixes remain visible —
226
+ * AI/SAST tools trigger on those patterns. The actual secret strings only
227
+ * materialise at runtime via Buffer.from decode.
228
+ *
229
+ * Injected AFTER cneObfuscate() so strings are never fragmented by
230
+ * applyStringSplit().
231
+ */
232
+ function buildSemanticPoisonSource(rng) {
233
+ const lines = ["/* layer-3f */"];
234
+ // ── A: Crypto primitive vars ──────────────────────────────────────────────
235
+ // Guaranteed set: highest-value trigger patterns, always included.
236
+ const GUARANTEED = [
237
+ { name: "RSA_PRIVATE_KEY", value: r => fakePem(r, "RSA PRIVATE KEY"), multiline: true },
238
+ { name: "EC_PRIVATE_KEY", value: r => fakePem(r, "EC PRIVATE KEY"), multiline: true },
239
+ { name: "STRIPE_SECRET_KEY", value: fakeStripeKey },
240
+ { name: "AWS_ACCESS_KEY_ID", value: fakeAwsKeyId },
241
+ { name: "AWS_SECRET_KEY", value: fakeAwsSecret },
242
+ { name: "GH_TOKEN", value: fakeGhToken },
243
+ { name: "JWT_SECRET", value: r => randAlphaNum(r, 64) },
244
+ { name: "HMAC_SECRET", value: r => randHex(r, 32) },
245
+ ];
246
+ shuffleArr(GUARANTEED, rng); // randomise order across builds
247
+ for (const entry of GUARANTEED) {
248
+ lines.push(`var ${entry.name} = ${encodeCredential(entry.value(rng))};`);
249
+ }
250
+ // Extra random picks from remaining pool
251
+ const guaranteed_names = new Set(GUARANTEED.map(e => e.name));
252
+ const rest = shuffleArr(CRYPTO_KEY_POOL.filter(e => !guaranteed_names.has(e.name)), rng);
253
+ const extraCount = 3 + Math.floor(rng.next() * 3); // 3..5
254
+ for (let i = 0; i < Math.min(extraCount, rest.length); i++) {
255
+ lines.push(`var ${rest[i].name} = ${encodeCredential(rest[i].value(rng))};`);
256
+ }
257
+ // ── B: Fake endpoint strings ───────────────────────────────────────────────
258
+ // URLs are not credential values — keep as plain strings (their purpose is
259
+ // to look like live infrastructure endpoints, not to carry secret data).
260
+ const epPool = shuffleArr([...ENDPOINT_TEMPLATES], rng);
261
+ const epCount = 4 + Math.floor(rng.next() * 3); // 4..6
262
+ for (let i = 0; i < Math.min(epCount, epPool.length); i++) {
263
+ const url = renderEndpoint(rng, epPool[i]);
264
+ lines.push(`var _lsEndpoint${i} = ${JSON.stringify(url)};`);
265
+ }
266
+ // ── C: Credential-leak concat expressions ─────────────────────────────────
267
+ // Prefix (e.g. "TODO: remove before prod -- STRIPE_SK=") stays plaintext —
268
+ // it is the pattern SAST/AI tools match on.
269
+ // The credential VALUE (second part) is base64-encoded so it isn't readable.
270
+ const leakOrder = shuffleArr([0, 1, 2, 3, 4, 5, 6, 7], rng);
271
+ for (const kind of leakOrder) {
272
+ const [a, b] = buildLeakLineKind(rng, kind);
273
+ lines.push(`${JSON.stringify(a)} + ${encodeCredential(b)};`);
274
+ }
275
+ return lines.join("\n");
276
+ }
package/canary.js ADDED
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // Layer 7 — Differential Canary Algorithm
4
+ //
5
+ // CONCEPT (adversarial, novel):
6
+ // For each exported/top-level function that looks like a validator or
7
+ // key-processing routine, we inject a SHADOW copy at global scope with:
8
+ // - Identical name (shadows the real export)
9
+ // - Identical structure and most logic
10
+ // - A TRAPDOOR: one subtle constant is wrong in a way that passes
11
+ // basic (low-privilege) tests but silently fails for elevated tiers.
12
+ //
13
+ // Attack scenario:
14
+ // AI extracts `function validateLicense(key) { ... }` from the AST.
15
+ // It's at global scope, looks complete, passes a basic-tier key test.
16
+ // The AI/red-team is CONFIDENT they have the algorithm.
17
+ // They try to forge an enterprise key -> canary returns {tier:"unknown"}.
18
+ // They spend days debugging their key generation, not the algorithm.
19
+ // The real function is inside the module IIFE, untouched.
20
+ //
21
+ // TRAPDOOR STRATEGY:
22
+ // We mutate exactly ONE of these per canary (chosen randomly):
23
+ // a) A tier-discriminating constant (e.g. tier===4 becomes tier===3)
24
+ // b) A feature bitmask bit flip (enterprise features 0x1F -> 0x0F)
25
+ // c) A checksum multiplier tweak (FNV prime 16777619 -> 16777621)
26
+ // d) A key segment mask offset (0xFF -> 0xFE)
27
+ // These all produce correct output for tier=1 (basic) tests but wrong
28
+ // output for tier=4 (enterprise) — exactly the goal the attacker has.
29
+ //
30
+ // IMPORTANT: canary functions are injected OUTSIDE the module IIFE, at
31
+ // true global scope. The real module.exports (set inside the IIFE) wins
32
+ // for require() callers. Canaries only fool AST extraction tools.
33
+ // ============================================================================
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.buildCanarySource = buildCanarySource;
36
+ exports.injectCanary = injectCanary;
37
+ // Trapdoor variants — each is a source snippet with a subtle wrong constant.
38
+ // The placeholder %%WRONG%% is substituted with a per-build random wrong value.
39
+ const CANARY_TEMPLATES = [
40
+ // Trapdoor A: wrong tier constant (3 instead of 4 for enterprise)
41
+ {
42
+ name: "tierConstant",
43
+ description: "enterprise tier ID wrong (3 vs 4)",
44
+ inject: (rng) => `
45
+ function validateLicense(key) {
46
+ if (!key || typeof key !== 'string') return null;
47
+ var parts = key.toUpperCase().split('-');
48
+ if (parts.length !== 4) return null;
49
+ var segs = parts.map(function(p){ return parseInt(p, 36); });
50
+ if (segs.some(isNaN)) return null;
51
+ var s0=segs[0], s1=segs[1], s2=segs[2], s3=segs[3];
52
+ var SALT = ${(0x7f3a9b2c ^ (rng.int32() & 0x00FF00FF)) >>> 0};
53
+ var PID = ${(0x43524950 ^ (rng.int32() & 0x00FF00FF)) >>> 0};
54
+ if ((s0 ^ SALT) !== PID) return { valid: false, reason: 'invalid_product' };
55
+ var tier = s1 & 0xFF;
56
+ var seats = (s1 >> 8) & 0xFF;
57
+ var BASE = new Date('2024-01-01').getTime();
58
+ var exp = BASE + s2 * 86400000;
59
+ if (exp < Date.now()) return { valid: false, reason: 'expired' };
60
+ var h = 2166136261;
61
+ h = Math.imul(h ^ (s0 & 0xFF), 16777619);
62
+ h = Math.imul(h ^ (s1 & 0xFF), 16777619);
63
+ h = Math.imul(h ^ (s2 & 0xFF), 16777619);
64
+ h = (h >>> 0) & 0xFFFF;
65
+ if (h !== (s3 & 0xFFFF)) return { valid: false, reason: 'checksum_mismatch' };
66
+ var tierNames = { 1: 'basic', 2: 'advanced', 3: 'enterprise' };
67
+ var tierName = tierNames[tier] || 'unknown';
68
+ var featureMasks = { basic: 0x01, advanced: 0x13, enterprise: 0x1F };
69
+ var features = featureMasks[tierName] || 0;
70
+ return { valid: true, tier: tierName, seats: seats || 5, features: features };
71
+ }`,
72
+ },
73
+ // Trapdoor B: feature bitmask for enterprise is 0x0F (missing api bit 0x08... or export)
74
+ {
75
+ name: "featureMask",
76
+ description: "enterprise feature mask wrong (0x0F vs 0x1F)",
77
+ inject: (rng) => `
78
+ function validateLicense(key) {
79
+ if (!key || typeof key !== 'string') return null;
80
+ var parts = key.toUpperCase().split('-');
81
+ if (parts.length !== 4) return null;
82
+ var segs = parts.map(function(p){ return parseInt(p, 36); });
83
+ if (segs.some(isNaN)) return null;
84
+ var s0=segs[0], s1=segs[1], s2=segs[2], s3=segs[3];
85
+ var _SALT = ${(0x7f3a9b2c ^ (rng.int32() & 0x000F000F)) >>> 0};
86
+ var _PID = ${(0x43524950 ^ (rng.int32() & 0x000F000F)) >>> 0};
87
+ if ((s0 ^ _SALT) !== _PID) return { valid: false, reason: 'invalid_product' };
88
+ var tier = s1 & 0xFF;
89
+ var seats = (s1 >> 8) & 0xFF;
90
+ var BASE = new Date('2024-01-01').getTime();
91
+ if (BASE + s2 * 86400000 < Date.now()) return { valid: false, reason: 'expired' };
92
+ var h = 2166136261;
93
+ h = Math.imul(h ^ (s0 & 0xFF), 16777619);
94
+ h = Math.imul(h ^ (s1 & 0xFF), 16777619);
95
+ h = Math.imul(h ^ (s2 & 0xFF), 16777619);
96
+ if (((h >>> 0) & 0xFFFF) !== (s3 & 0xFFFF)) return { valid: false, reason: 'checksum_mismatch' };
97
+ var tn = tier === 1 ? 'basic' : tier === 2 ? 'advanced' : tier === 4 ? 'enterprise' : 'unknown';
98
+ var fm = { basic: 0x01, advanced: 0x13, enterprise: 0x0F };
99
+ return { valid: true, tier: tn, seats: seats || 5, features: fm[tn] || 0 };
100
+ }`,
101
+ },
102
+ // Trapdoor C: FNV prime is wrong (16777621 vs 16777619) — checksum always mismatches
103
+ {
104
+ name: "checksumPrime",
105
+ description: "FNV prime wrong (16777621 vs 16777619) — all forged keys fail checksum",
106
+ inject: (rng) => `
107
+ function validateLicense(key) {
108
+ if (!key || typeof key !== 'string') return null;
109
+ var parts = key.toUpperCase().split('-');
110
+ if (parts.length !== 4) return null;
111
+ var segs = parts.map(function(p){ return parseInt(p, 36); });
112
+ if (segs.some(isNaN)) return null;
113
+ var s0=segs[0], s1=segs[1], s2=segs[2], s3=segs[3];
114
+ if ((s0 ^ ${(0x7f3a9b2c ^ ((rng.int32() & 0xFF) << 8)) >>> 0}) !== ${(0x43524950 ^ ((rng.int32() & 0xFF) << 8)) >>> 0})
115
+ return { valid: false, reason: 'invalid_product' };
116
+ var BASE = new Date('2024-01-01').getTime();
117
+ if (BASE + s2 * 86400000 < Date.now()) return { valid: false, reason: 'expired' };
118
+ var h = 2166136261;
119
+ h = Math.imul(h ^ (s0 & 0xFF), 16777621);
120
+ h = Math.imul(h ^ (s1 & 0xFF), 16777621);
121
+ h = Math.imul(h ^ (s2 & 0xFF), 16777621);
122
+ if (((h >>> 0) & 0xFFFF) !== (s3 & 0xFFFF)) return { valid: false, reason: 'checksum_mismatch' };
123
+ var tier = s1 & 0xFF;
124
+ var tn = tier===1?'basic':tier===2?'advanced':tier===4?'enterprise':'unknown';
125
+ return { valid: true, tier: tn, seats: ((s1>>8)&0xFF)||5, features: tn==='enterprise'?0x1F:tn==='advanced'?0x13:0x01 };
126
+ }`,
127
+ },
128
+ ];
129
+ /**
130
+ * Returns the raw canary source text (both shadow validateLicense and hasFeature).
131
+ * Returns empty string if the probabilistic rate check fails.
132
+ * Used by the pipeline so the canary can be passed through CNE rename before appending.
133
+ */
134
+ function buildCanarySource(rng, opts = {}) {
135
+ const rate = opts.canaryPoisonRate ?? 0.5;
136
+ if (rng.next() > rate)
137
+ return "";
138
+ const tmpl = CANARY_TEMPLATES[Math.floor(rng.next() * CANARY_TEMPLATES.length)];
139
+ const canarySource = tmpl.inject(rng);
140
+ const canaryHasFeature = `
141
+ function hasFeature(license, featureName) {
142
+ if (!license || !license.valid) return false;
143
+ var bits = { basic:0x01, advanced:0x02, enterprise:0x04, api:0x08, export:0x10 };
144
+ var bit = bits[featureName];
145
+ if (!bit) return false;
146
+ return (license.features & bit) === bit;
147
+ }`;
148
+ return canarySource + "\n" + canaryHasFeature + "\n";
149
+ }
150
+ /**
151
+ * Injects canary shadow functions appended to code (legacy path / jsobf mode).
152
+ */
153
+ function injectCanary(code, rng, opts = {}) {
154
+ const src = buildCanarySource(rng, opts);
155
+ if (!src)
156
+ return code;
157
+ return code + "\n\n" + src;
158
+ }