brainblast 0.2.0 → 0.3.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.
@@ -1,494 +0,0 @@
1
- // src/finder.ts
2
- import { Project, SyntaxKind } from "ts-morph";
3
-
4
- // src/walk.ts
5
- import { readdirSync, statSync } from "fs";
6
- import { join } from "path";
7
- function walk(dir, out = []) {
8
- for (const entry of readdirSync(dir)) {
9
- if (entry === "node_modules" || entry === ".git" || entry === ".gen") continue;
10
- const p = join(dir, entry);
11
- const st = statSync(p);
12
- if (st.isDirectory()) walk(p, out);
13
- else if (p.endsWith(".ts") && !p.endsWith(".test.ts") && !p.endsWith(".d.ts")) out.push(p);
14
- }
15
- return out;
16
- }
17
-
18
- // src/finder.ts
19
- function bodyCallsAnyOf(fn, names) {
20
- if (names.size === 0) return false;
21
- return fn.getDescendantsOfKind(SyntaxKind.CallExpression).some((c) => {
22
- const exp = c.getExpression();
23
- if (exp.getKind() === SyntaxKind.Identifier) return names.has(exp.getText());
24
- if (exp.getKind() === SyntaxKind.PropertyAccessExpression) {
25
- return names.has(exp.asKind(SyntaxKind.PropertyAccessExpression).getName());
26
- }
27
- return false;
28
- });
29
- }
30
- function findCandidates(targetDir, rule) {
31
- const files = walk(targetDir);
32
- const project = new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { allowJs: false } });
33
- const modules = new Set(rule.detect.modules);
34
- const triggers = new Set(rule.detect.triggerCalls);
35
- const nameRe = new RegExp(rule.detect.nameRegex, "i");
36
- const out = [];
37
- for (const file of files) {
38
- const sf = project.addSourceFileAtPath(file);
39
- const importsModule = sf.getImportDeclarations().some((d) => modules.has(d.getModuleSpecifierValue()));
40
- const consider = (fn, name) => {
41
- if (!(importsModule || name && nameRe.test(name) || bodyCallsAnyOf(fn, triggers))) return;
42
- out.push({
43
- filePath: file,
44
- fnName: name || "(anonymous)",
45
- params: fn.getParameters().map((p) => p.getName()),
46
- fn
47
- });
48
- };
49
- for (const fn of sf.getFunctions()) consider(fn, fn.getName() ?? "");
50
- for (const v of sf.getVariableDeclarations()) {
51
- const arrow = v.getInitializerIfKind(SyntaxKind.ArrowFunction);
52
- if (arrow) consider(arrow, v.getName());
53
- }
54
- }
55
- return out;
56
- }
57
-
58
- // src/checkers/positionalArgIdentity.ts
59
- import { SyntaxKind as SyntaxKind2 } from "ts-morph";
60
- var positionalArgIdentity = (c, p) => {
61
- const calls = c.fn.getDescendantsOfKind(SyntaxKind2.CallExpression).filter((call) => {
62
- const exp = call.getExpression();
63
- return exp.getKind() === SyntaxKind2.PropertyAccessExpression && exp.asKind(SyntaxKind2.PropertyAccessExpression).getName() === p.call;
64
- });
65
- if (calls.length === 0) return { result: "fail", detail: p.absentDetail };
66
- const arg = calls[0].getArguments()[p.argIndex];
67
- const wantParam = c.params[p.paramIndex];
68
- if (arg && wantParam && arg.getKind() === SyntaxKind2.Identifier && arg.getText() === wantParam) {
69
- return { result: "pass", detail: String(p.passDetail).replace("{param}", wantParam) };
70
- }
71
- if (arg && arg.getKind() === SyntaxKind2.CallExpression) {
72
- return { result: "fail", detail: p.parsedDetail };
73
- }
74
- return {
75
- result: "cant_tell",
76
- detail: `Could not confirm argument ${p.argIndex} of ${p.call} is the raw input.`
77
- };
78
- };
79
-
80
- // src/checkers/requiredCallWithOptions.ts
81
- import { SyntaxKind as SyntaxKind3 } from "ts-morph";
82
- function callName(call) {
83
- const exp = call.getExpression();
84
- if (exp.getKind() === SyntaxKind3.Identifier) return exp.getText();
85
- if (exp.getKind() === SyntaxKind3.PropertyAccessExpression) {
86
- return exp.asKind(SyntaxKind3.PropertyAccessExpression).getName();
87
- }
88
- return "";
89
- }
90
- function hasAllProps(call, groups) {
91
- for (const arg of call.getArguments()) {
92
- const obj = arg.asKind(SyntaxKind3.ObjectLiteralExpression);
93
- if (!obj) continue;
94
- const names = obj.getProperties().map((pr) => {
95
- const pa = pr.asKind(SyntaxKind3.PropertyAssignment) ?? pr.asKind(SyntaxKind3.ShorthandPropertyAssignment);
96
- return pa?.getName() ?? "";
97
- });
98
- if (groups.every((g) => g.some((n) => names.includes(n)))) return true;
99
- }
100
- return false;
101
- }
102
- var requiredCallWithOptions = (c, p) => {
103
- const calls = c.fn.getDescendantsOfKind(SyntaxKind3.CallExpression);
104
- const verify = calls.filter((x) => p.verifyCalls.includes(callName(x)));
105
- const decode = calls.filter((x) => p.decodeCalls.includes(callName(x)));
106
- if (verify.length > 0) {
107
- if (verify.some((v) => hasAllProps(v, p.requiredProps))) {
108
- return { result: "pass", detail: p.passDetail };
109
- }
110
- return { result: "fail", detail: p.missingPropsDetail };
111
- }
112
- if (decode.length > 0) return { result: "fail", detail: p.decodeOnlyDetail };
113
- return { result: "cant_tell", detail: "No verification or decode call found." };
114
- };
115
-
116
- // src/checkers/feeAllocationShape.ts
117
- import { SyntaxKind as SyntaxKind4 } from "ts-morph";
118
- function callName2(call) {
119
- const exp = call.getExpression();
120
- if (exp.getKind() === SyntaxKind4.Identifier) return exp.getText();
121
- if (exp.getKind() === SyntaxKind4.PropertyAccessExpression) {
122
- return exp.asKind(SyntaxKind4.PropertyAccessExpression).getName();
123
- }
124
- return "";
125
- }
126
- function asArrayLiteral(expr, c) {
127
- if (!expr) return void 0;
128
- const direct = expr.asKind(SyntaxKind4.ArrayLiteralExpression);
129
- if (direct) return direct;
130
- if (expr.getKind() === SyntaxKind4.Identifier) {
131
- const name = expr.getText();
132
- for (const decl of c.fn.getDescendantsOfKind(SyntaxKind4.VariableDeclaration)) {
133
- if (decl.getName() === name) {
134
- return decl.getInitializerIfKind(SyntaxKind4.ArrayLiteralExpression);
135
- }
136
- }
137
- }
138
- return void 0;
139
- }
140
- function feeArray(call, prop, c) {
141
- for (const arg of call.getArguments()) {
142
- const obj = arg.asKind(SyntaxKind4.ObjectLiteralExpression);
143
- if (!obj) continue;
144
- const member = obj.getProperty(prop);
145
- if (!member) continue;
146
- const pa = member.asKind(SyntaxKind4.PropertyAssignment);
147
- if (pa) return asArrayLiteral(pa.getInitializer(), c) ?? null;
148
- const shorthand = member.asKind(SyntaxKind4.ShorthandPropertyAssignment);
149
- if (shorthand) return asArrayLiteral(shorthand.getNameNode(), c) ?? null;
150
- return null;
151
- }
152
- return void 0;
153
- }
154
- function propInit(entry, name) {
155
- return entry.getProperty(name)?.asKind(SyntaxKind4.PropertyAssignment)?.getInitializer();
156
- }
157
- var feeAllocationShape = (c, p) => {
158
- const calls = c.fn.getDescendantsOfKind(SyntaxKind4.CallExpression).filter((x) => callName2(x) === p.configCall);
159
- if (calls.length === 0) return { result: "fail", detail: p.absentDetail };
160
- const arr = feeArray(calls[0], p.arrayProp, c);
161
- if (arr === void 0 || arr === null) {
162
- return { result: "cant_tell", detail: p.dynamicDetail };
163
- }
164
- const entries = arr.getElements().map((e) => e.asKind(SyntaxKind4.ObjectLiteralExpression));
165
- if (entries.length === 0 || entries.some((e) => !e)) {
166
- return { result: "cant_tell", detail: p.dynamicDetail };
167
- }
168
- const creatorParam = c.params.find((name) => new RegExp(p.creatorParamRegex, "i").test(name));
169
- const creatorIncluded = creatorParam ? entries.some((e) => propInit(e, p.walletProp)?.getText() === creatorParam) : false;
170
- if (!creatorIncluded) return { result: "fail", detail: p.creatorMissingDetail };
171
- let sum = 0;
172
- let allNumeric = true;
173
- for (const e of entries) {
174
- const lit = propInit(e, p.bpsProp)?.asKind(SyntaxKind4.NumericLiteral);
175
- if (!lit) {
176
- allNumeric = false;
177
- break;
178
- }
179
- sum += Number(lit.getLiteralValue());
180
- }
181
- if (!allNumeric) return { result: "cant_tell", detail: p.dynamicDetail };
182
- if (sum !== p.bpsTotal) {
183
- return { result: "fail", detail: String(p.bpsSumDetail).replace("{sum}", String(sum)) };
184
- }
185
- return { result: "pass", detail: String(p.passDetail).replace("{param}", creatorParam) };
186
- };
187
-
188
- // src/checkers/index.ts
189
- var registry = {
190
- "positional-arg-identity": positionalArgIdentity,
191
- "required-call-with-options": requiredCallWithOptions,
192
- "fee-allocation-shape": feeAllocationShape
193
- };
194
- function runChecker(kind, c, params) {
195
- const fn = registry[kind];
196
- if (!fn) return { result: "cant_tell", detail: `Unknown checker kind '${kind}'.` };
197
- return fn(c, params);
198
- }
199
- var checkerKinds = Object.keys(registry);
200
-
201
- // src/emit.ts
202
- function buildReport(target, checks, rules2) {
203
- const byId = new Map(rules2.map((r) => [r.id, r]));
204
- const checkTotals = { pass: 0, fail: 0, cant_tell: 0 };
205
- for (const c of checks) checkTotals[c.result]++;
206
- const riskTotals = { critical: 0, high: 0, medium: 0, low: 0 };
207
- for (const c of checks) if (c.result === "fail") riskTotals[c.severity]++;
208
- const ruleIdsSeen = [...new Set(checks.map((c) => c.ruleId))];
209
- const components = ruleIdsSeen.map((id) => {
210
- const rule = byId.get(id);
211
- const fails = checks.filter((c) => c.ruleId === id && c.result === "fail");
212
- return {
213
- name: rule?.component.name ?? id,
214
- type: rule?.component.type ?? "Other",
215
- version: rule?.component.version ?? "unversioned",
216
- sourceUrl: rule?.component.sourceUrl ?? null,
217
- status: "fresh",
218
- risks: fails.map((c) => ({ severity: c.severity, title: c.title, detail: c.detail }))
219
- };
220
- });
221
- const now = /* @__PURE__ */ new Date();
222
- const totalFails = checkTotals.fail;
223
- return {
224
- schemaVersion: "1.0",
225
- run: {
226
- id: now.toISOString().replace(/[-:T]/g, "").slice(0, 14),
227
- date: now.toISOString().slice(0, 10),
228
- requirements: `Catastrophic-integration audit of ${target}`,
229
- generator: "@brainblast/core"
230
- },
231
- summary: {
232
- building: "external integrations",
233
- verdict: totalFails > 0 ? "blocked" : "ready",
234
- topRisk: totalFails > 0 ? checks.find((c) => c.result === "fail")?.detail ?? null : null,
235
- mustDecideFirst: null,
236
- watchOutFor: null
237
- },
238
- components,
239
- riskTotals,
240
- checks: checks.map((c) => ({
241
- ruleId: c.ruleId,
242
- severity: c.severity,
243
- result: c.result,
244
- file: c.file,
245
- line: c.line,
246
- title: c.title,
247
- detail: c.detail
248
- })),
249
- checkTotals,
250
- openQuestions: []
251
- };
252
- }
253
-
254
- // src/audit.ts
255
- function auditWithRule(targetDir, rule) {
256
- return findCandidates(targetDir, rule).map((c) => {
257
- const outcome = runChecker(rule.check.kind, c, rule.check.params);
258
- return {
259
- ruleId: rule.id,
260
- severity: rule.severity,
261
- title: rule.title,
262
- file: c.filePath,
263
- line: c.fn.getStartLineNumber(),
264
- exportName: c.fnName,
265
- ...outcome
266
- };
267
- });
268
- }
269
- function audit(targetDir, rules2) {
270
- const checks = rules2.flatMap((r) => auditWithRule(targetDir, r));
271
- const report = buildReport(targetDir, checks, rules2);
272
- return { checks, report };
273
- }
274
-
275
- // src/testTemplates/stripeWebhookSignature.ts
276
- var stripeWebhookSignature = ({ handlerImportPath, handlerExport }) => `// GENERATED by @brainblast/core. Durable guardrail \u2014 RED until the webhook
277
- // verifies the signature on the RAW body.
278
- import { describe, it, expect, beforeAll } from "vitest";
279
- import Stripe from "stripe";
280
- import { ${handlerExport} } from ${JSON.stringify(handlerImportPath)};
281
-
282
- const SECRET = "whsec_brainblast_test_secret";
283
- const stripe = new Stripe("sk_test_brainblast");
284
- const payload = JSON.stringify({ id: "evt_1", type: "payment_intent.succeeded", data: { object: { id: "pi_1" } } });
285
-
286
- beforeAll(() => {
287
- process.env.STRIPE_WEBHOOK_SECRET = SECRET;
288
- process.env.STRIPE_SECRET_KEY = "sk_test_brainblast";
289
- });
290
-
291
- describe("Stripe webhook signature verification (brainblast contract)", () => {
292
- it("accepts a validly-signed event", () => {
293
- const sig = stripe.webhooks.generateTestHeaderString({ payload, secret: SECRET });
294
- expect(() => ${handlerExport}(payload, sig)).not.toThrow();
295
- });
296
- it("REJECTS an invalid signature (forged event)", () => {
297
- expect(() => ${handlerExport}(payload, "t=1,v1=deadbeef")).toThrow();
298
- });
299
- it("REJECTS a mutated body under a valid-looking signature", () => {
300
- const sig = stripe.webhooks.generateTestHeaderString({ payload, secret: SECRET });
301
- const mutated = payload.replace("succeeded", "failed");
302
- expect(() => ${handlerExport}(mutated, sig)).toThrow();
303
- });
304
- });
305
- `;
306
-
307
- // src/testTemplates/privyJwtClaims.ts
308
- var privyJwtClaims = ({ handlerImportPath, handlerExport }) => `// GENERATED by @brainblast/core. Durable guardrail \u2014 RED until the token is
309
- // verified (signature + aud + iss).
310
- import { describe, it, expect, beforeAll } from "vitest";
311
- import { SignJWT, generateKeyPair, exportSPKI } from "jose";
312
- import { ${handlerExport} } from ${JSON.stringify(handlerImportPath)};
313
-
314
- const APP_ID = "app_brainblast";
315
- let valid = "", badSignature = "", wrongAudience = "", wrongIssuer = "";
316
-
317
- beforeAll(async () => {
318
- const { publicKey, privateKey } = await generateKeyPair("ES256");
319
- const attacker = await generateKeyPair("ES256");
320
- process.env.PRIVY_APP_ID = APP_ID;
321
- process.env.PRIVY_VERIFICATION_KEY = await exportSPKI(publicKey);
322
-
323
- const mint = (signer: any, iss: string, aud: string) =>
324
- new SignJWT({})
325
- .setProtectedHeader({ alg: "ES256" })
326
- .setIssuer(iss)
327
- .setAudience(aud)
328
- .setSubject("did:privy:user_1")
329
- .setExpirationTime("1h")
330
- .sign(signer);
331
-
332
- valid = await mint(privateKey, "privy.io", APP_ID);
333
- badSignature = await mint(attacker.privateKey, "privy.io", APP_ID);
334
- wrongAudience = await mint(privateKey, "privy.io", "app_evil");
335
- wrongIssuer = await mint(privateKey, "evil.com", APP_ID);
336
- });
337
-
338
- const call = (t: string) => Promise.resolve().then(() => ${handlerExport}(t));
339
-
340
- describe("Privy access-token verification (brainblast contract)", () => {
341
- it("accepts a valid token", async () => { await expect(call(valid)).resolves.toBeDefined(); });
342
- it("REJECTS a bad signature (forged token)", async () => { await expect(call(badSignature)).rejects.toThrow(); });
343
- it("REJECTS a wrong audience (token from another app)", async () => { await expect(call(wrongAudience)).rejects.toThrow(); });
344
- it("REJECTS a wrong issuer", async () => { await expect(call(wrongIssuer)).rejects.toThrow(); });
345
- });
346
- `;
347
-
348
- // src/testTemplates/bagsFeeShare.ts
349
- var bagsFeeShare = ({ handlerImportPath, handlerExport }) => `// GENERATED by @brainblast/core. Durable guardrail \u2014 RED until the fee-share
350
- // config includes the creator wallet with a non-zero userBps summing to 10000.
351
- import { describe, it, expect, vi, beforeEach } from "vitest";
352
-
353
- let captured: any;
354
- vi.mock("@bagsfm/bags-sdk", () => ({
355
- createBagsFeeShareConfig: (cfg: any) => {
356
- captured = cfg;
357
- return { meteoraConfigKey: "mock-config-key" };
358
- },
359
- BagsSDK: class {},
360
- }));
361
-
362
- import { ${handlerExport} } from ${JSON.stringify(handlerImportPath)};
363
-
364
- const CREATOR = "CreatorWa11etPubKey1111111111111111111111111";
365
-
366
- describe("Bags fee-share creator inclusion (brainblast contract)", () => {
367
- beforeEach(() => {
368
- captured = undefined;
369
- });
370
-
371
- it("passes the creator wallet into feeClaimers", () => {
372
- ${handlerExport}(CREATOR);
373
- const claimers = captured?.feeClaimers ?? [];
374
- expect(claimers.some((c: any) => c.user === CREATOR)).toBe(true);
375
- });
376
-
377
- it("allocates the creator a non-zero userBps", () => {
378
- ${handlerExport}(CREATOR);
379
- const entry = (captured?.feeClaimers ?? []).find((c: any) => c.user === CREATOR);
380
- expect(entry?.userBps ?? 0).toBeGreaterThan(0);
381
- });
382
-
383
- it("feeClaimers userBps sum to exactly 10000", () => {
384
- ${handlerExport}(CREATOR);
385
- const sum = (captured?.feeClaimers ?? []).reduce((s: number, c: any) => s + c.userBps, 0);
386
- expect(sum).toBe(10000);
387
- });
388
- });
389
- `;
390
-
391
- // src/testTemplates/index.ts
392
- var registry2 = {
393
- "stripe-webhook-signature": stripeWebhookSignature,
394
- "privy-jwt-claims": privyJwtClaims,
395
- "bags-fee-share": bagsFeeShare
396
- };
397
- var JS_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
398
- function renderTest(kind, opts) {
399
- const tpl = registry2[kind];
400
- if (!tpl) throw new Error(`Unknown test template kind '${kind}'.`);
401
- if (!JS_IDENTIFIER.test(opts.handlerExport)) {
402
- throw new Error(`Unsafe handler export name '${opts.handlerExport}' (not a JS identifier).`);
403
- }
404
- return tpl(opts);
405
- }
406
- var testKinds = Object.keys(registry2);
407
-
408
- // src/loadRules.ts
409
- import { readdirSync as readdirSync2, readFileSync } from "fs";
410
- import { join as join2 } from "path";
411
- import { parse } from "yaml";
412
- var SEVERITIES = ["critical", "high", "medium", "low"];
413
- function validateRule(r, file) {
414
- const errs = [];
415
- if (!r || typeof r !== "object") {
416
- throw new Error(`invalid rule in ${file}: not a mapping`);
417
- }
418
- if (!r.id || typeof r.id !== "string") errs.push("missing id");
419
- if (!SEVERITIES.includes(r.severity)) errs.push(`bad severity '${r.severity}'`);
420
- if (!r.title || typeof r.title !== "string") errs.push("missing title");
421
- if (!r.component || !r.component.name || !r.component.type) errs.push("missing component.name/type");
422
- if (!r.detect || !Array.isArray(r.detect.modules) || typeof r.detect.nameRegex !== "string" || !Array.isArray(r.detect.triggerCalls)) {
423
- errs.push("detect must have modules[], nameRegex (string), triggerCalls[]");
424
- } else {
425
- try {
426
- new RegExp(r.detect.nameRegex);
427
- } catch {
428
- errs.push(`detect.nameRegex is not a valid regex: ${r.detect.nameRegex}`);
429
- }
430
- }
431
- if (!r.check || !checkerKinds.includes(r.check.kind)) {
432
- errs.push(`check.kind must be one of ${checkerKinds.join("|")} (got '${r.check?.kind}')`);
433
- }
434
- if (!r.test || !testKinds.includes(r.test.kind)) {
435
- errs.push(`test.kind must be one of ${testKinds.join("|")} (got '${r.test?.kind}')`);
436
- }
437
- if (errs.length) throw new Error(`invalid rule in ${file}: ${errs.join("; ")}`);
438
- }
439
- function loadRules(dir) {
440
- const files = readdirSync2(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).sort();
441
- const rules2 = [];
442
- for (const f of files) {
443
- const raw = parse(readFileSync(join2(dir, f), "utf8"));
444
- validateRule(raw, f);
445
- rules2.push(raw);
446
- }
447
- return rules2;
448
- }
449
-
450
- // rules/index.ts
451
- import { existsSync } from "fs";
452
- import { dirname, join as join3 } from "path";
453
- import { fileURLToPath } from "url";
454
- function bundledRulesDir() {
455
- const here = dirname(fileURLToPath(import.meta.url));
456
- if (existsSync(join3(here, "stripe-webhook-raw-body.yaml"))) return here;
457
- const sub = join3(here, "rules");
458
- if (existsSync(join3(sub, "stripe-webhook-raw-body.yaml"))) return sub;
459
- return here;
460
- }
461
- var rules = loadRules(bundledRulesDir());
462
-
463
- // src/resolveRules.ts
464
- import { existsSync as existsSync2 } from "fs";
465
- import { join as join4 } from "path";
466
- function resolveRules(targetDir) {
467
- const all = [...rules];
468
- const projDir = join4(targetDir, ".agent-research", "rules");
469
- if (existsSync2(projDir)) {
470
- const seen = new Set(all.map((r) => r.id));
471
- for (const r of loadRules(projDir)) {
472
- if (seen.has(r.id)) {
473
- console.warn(`brainblast: project rule '${r.id}' shadows a bundled rule; keeping bundled.`);
474
- continue;
475
- }
476
- all.push(r);
477
- seen.add(r.id);
478
- }
479
- }
480
- return all;
481
- }
482
-
483
- export {
484
- findCandidates,
485
- runChecker,
486
- checkerKinds,
487
- auditWithRule,
488
- audit,
489
- renderTest,
490
- testKinds,
491
- loadRules,
492
- rules,
493
- resolveRules
494
- };