@basaltkit/ai 0.9.2 → 0.11.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/dist/index.d.ts +2 -0
- package/dist/index.js +80 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -275,6 +275,8 @@ interface AppFileInfo {
|
|
|
275
275
|
fastifyLoggerConfigured: boolean;
|
|
276
276
|
/** In-memory (non-durable) sources still wired, e.g. `MemoryTenantSource`. */
|
|
277
277
|
memorySources: string[];
|
|
278
|
+
/** Every `<name>Plugin(` factory call in the app file (unfiltered by capability). */
|
|
279
|
+
pluginCalls: string[];
|
|
278
280
|
}
|
|
279
281
|
interface ServerFileInfo {
|
|
280
282
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -364,10 +364,22 @@ function detectAppFile(reader) {
|
|
|
364
364
|
(factory) => new RegExp(`\\b${factory}\\s*\\(`).test(found.content)
|
|
365
365
|
);
|
|
366
366
|
const fastifyLoggerConfigured = /fastify\s*:\s*\{[^}]*\blogger\b/.test(found.content);
|
|
367
|
-
const memorySources = [
|
|
367
|
+
const memorySources = [
|
|
368
|
+
"MemoryTenantSource",
|
|
369
|
+
"MemoryUserSource",
|
|
370
|
+
// Security-critical in-memory stores: auth/authorization/domain state that
|
|
371
|
+
// must survive restarts and be shared across instances in production.
|
|
372
|
+
"MemoryPasskeyStore",
|
|
373
|
+
"MemoryWebAuthnChallengeStore",
|
|
374
|
+
"MemoryAccessStore",
|
|
375
|
+
"MemoryDomainStore"
|
|
376
|
+
].filter(
|
|
368
377
|
(source) => found.content.includes(source)
|
|
369
378
|
);
|
|
370
|
-
|
|
379
|
+
const pluginCalls = [
|
|
380
|
+
...new Set(Array.from(found.content.matchAll(/\b([A-Za-z][A-Za-z0-9]*Plugin)\s*\(/g), (m) => m[1]))
|
|
381
|
+
];
|
|
382
|
+
return { path: found.path, plugins, fastifyLoggerConfigured, memorySources, pluginCalls };
|
|
371
383
|
}
|
|
372
384
|
function detectServerFile(reader) {
|
|
373
385
|
const found = firstExisting(reader, SERVER_CANDIDATES);
|
|
@@ -527,7 +539,47 @@ var DEFAULT_RULES = [
|
|
|
527
539
|
};
|
|
528
540
|
}
|
|
529
541
|
}),
|
|
542
|
+
defineRule({
|
|
543
|
+
id: "missing-security-plugin",
|
|
544
|
+
category: "security",
|
|
545
|
+
check(ctx) {
|
|
546
|
+
if (!ctx.app) return null;
|
|
547
|
+
if (ctx.app.pluginCalls.includes("securityPlugin")) return null;
|
|
548
|
+
return {
|
|
549
|
+
id: "missing-security-plugin",
|
|
550
|
+
title: "No securityPlugin \u2014 responses ship without secure headers",
|
|
551
|
+
severity: "warning",
|
|
552
|
+
category: "security",
|
|
553
|
+
detected: `${ctx.app.path} registers no securityPlugin()`,
|
|
554
|
+
recommended: "add securityPlugin() so secure response headers are set, and enable rate limiting + a CORS allow-list for production",
|
|
555
|
+
reason: "Without it the API returns no security headers (HSTS, X-Frame-Options, nosniff, \u2026) and applies no rate limiting \u2014 a fresh deploy is unprotected at the edge. The security primitives exist but are off until wired.",
|
|
556
|
+
fix: "securityPlugin({ /* headers on by default */ rateLimit: { limit: 120, windowMs: 60_000 } })",
|
|
557
|
+
docs: "/guide/security"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}),
|
|
530
561
|
// ── tenancy ───────────────────────────────────────────────────────────────
|
|
562
|
+
defineRule({
|
|
563
|
+
id: "missing-tenant-membership",
|
|
564
|
+
category: "tenancy",
|
|
565
|
+
check(ctx) {
|
|
566
|
+
if (!ctx.app || !ctx.stack.tenancy || !ctx.stack.auth) return null;
|
|
567
|
+
const teamsAvailable = ctx.app.pluginCalls.includes("teamsPlugin") || ctx.installed.includes("@basaltkit/teams");
|
|
568
|
+
if (!teamsAvailable) return null;
|
|
569
|
+
if (ctx.app.pluginCalls.includes("tenantMembershipPlugin")) return null;
|
|
570
|
+
return {
|
|
571
|
+
id: "missing-tenant-membership",
|
|
572
|
+
title: "Tenant is resolved from the request but membership is never enforced",
|
|
573
|
+
severity: "error",
|
|
574
|
+
category: "tenancy",
|
|
575
|
+
detected: `${ctx.app.path} wires tenancy + auth (+ teams) but no tenantMembershipPlugin`,
|
|
576
|
+
recommended: "register tenantMembershipPlugin() so every authenticated, tenant-scoped request verifies the user belongs to the resolved tenant",
|
|
577
|
+
reason: "Resolvers take the tenant from client input (x-tenant-id / Host). Without a membership check, any authenticated user can act on another tenant just by changing that header \u2014 a cross-tenant data breach. Tenant resolution is identification, never authorization.",
|
|
578
|
+
fix: "tenantMembershipPlugin() // central routes opt out with meta: { central: true }",
|
|
579
|
+
docs: "/guide/security"
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}),
|
|
531
583
|
defineRule({
|
|
532
584
|
id: "tenant-scoping-missing",
|
|
533
585
|
category: "tenancy",
|
|
@@ -588,6 +640,31 @@ var DEFAULT_RULES = [
|
|
|
588
640
|
docs: "/guide/configuration"
|
|
589
641
|
};
|
|
590
642
|
}
|
|
643
|
+
}),
|
|
644
|
+
defineRule({
|
|
645
|
+
id: "in-memory-security-store",
|
|
646
|
+
category: "security",
|
|
647
|
+
check(ctx) {
|
|
648
|
+
const SECURITY_STORES = {
|
|
649
|
+
MemoryPasskeyStore: "WebAuthn passkeys",
|
|
650
|
+
MemoryWebAuthnChallengeStore: "WebAuthn challenges",
|
|
651
|
+
MemoryAccessStore: "roles & permissions",
|
|
652
|
+
MemoryDomainStore: "verified custom domains"
|
|
653
|
+
};
|
|
654
|
+
const hit = (ctx.app?.memorySources ?? []).find((source) => source in SECURITY_STORES);
|
|
655
|
+
if (!hit) return null;
|
|
656
|
+
return {
|
|
657
|
+
id: "in-memory-security-store",
|
|
658
|
+
title: "Security state is kept in an in-memory store",
|
|
659
|
+
severity: "warning",
|
|
660
|
+
category: "security",
|
|
661
|
+
detected: `${hit} holds ${SECURITY_STORES[hit]} in memory.`,
|
|
662
|
+
recommended: "Back it with a durable store (database/Redis) in production.",
|
|
663
|
+
reason: "In-memory security stores are wiped on restart and are not shared across instances \u2014 passkeys/permissions/verified domains would silently reset or diverge per replica, which can lock users out or bypass authorization.",
|
|
664
|
+
fix: `// swap ${hit} for a durable implementation of the same interface`,
|
|
665
|
+
docs: "/guide/security"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
591
668
|
})
|
|
592
669
|
];
|
|
593
670
|
var GLOBAL_MODELS = /* @__PURE__ */ new Set(["Tenant", "User", "Plan", "WebhookEvent", "Migration"]);
|
|
@@ -2007,7 +2084,7 @@ function renderDoctor(diagnostics, io) {
|
|
|
2007
2084
|
}
|
|
2008
2085
|
if (d.docs) {
|
|
2009
2086
|
io.log("");
|
|
2010
|
-
io.log(` Docs: https://
|
|
2087
|
+
io.log(` Docs: https://basaltkit.dev${d.docs}`);
|
|
2011
2088
|
}
|
|
2012
2089
|
io.log("");
|
|
2013
2090
|
io.log(" " + "\u2500".repeat(60));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "AI-native developer experience for Basalt: a provider-agnostic engine plus `basalt ai` CLI that understands the project's stack, analyzes it, and diagnoses issues — following the framework's own conventions, not inventing new ones.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@basaltkit/cli": "^1.
|
|
17
|
+
"@basaltkit/cli": "^1.2.0",
|
|
18
18
|
"@basaltkit/generator": "^1.1.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|