@basaltkit/ai 1.2.0 → 1.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.
- package/dist/context/project.js +95 -2
- package/dist/doctor/rules.js +11 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/make/fields.d.ts +6 -0
- package/dist/make/fields.js +15 -1
- package/dist/make/make.js +11 -1
- package/dist/make/repository.js +2 -2
- package/dist/plan/identifiers.d.ts +22 -0
- package/dist/plan/identifiers.js +54 -0
- package/dist/schema/index.js +9 -5
- package/dist/workflows.d.ts +1 -0
- package/dist/workflows.js +1 -0
- package/package.json +5 -5
package/dist/context/project.js
CHANGED
|
@@ -84,9 +84,13 @@ function firstExisting(reader, candidates) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
function detectAppFile(reader) {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
87
|
+
const raw = firstExisting(reader, APP_CANDIDATES);
|
|
88
|
+
if (!raw)
|
|
89
89
|
return null;
|
|
90
|
+
// Match against CODE only: a commented-out `// tenantMembershipPlugin()` (or the
|
|
91
|
+
// name inside a string) must never count as a registration — that silenced
|
|
92
|
+
// security rules for exactly the apps that had disabled the guard.
|
|
93
|
+
const found = { path: raw.path, content: codeOnly(raw.content) };
|
|
90
94
|
const plugins = PLUGIN_FACTORIES.filter((factory) => new RegExp(`\\b${factory}\\s*\\(`).test(found.content));
|
|
91
95
|
const fastifyLoggerConfigured = /fastify\s*:\s*\{[^}]*\blogger\b/.test(found.content);
|
|
92
96
|
const memorySources = [
|
|
@@ -104,6 +108,95 @@ function detectAppFile(reader) {
|
|
|
104
108
|
];
|
|
105
109
|
return { path: found.path, plugins, fastifyLoggerConfigured, memorySources, pluginCalls };
|
|
106
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The source with comments removed and string/template-literal TEXT blanked
|
|
113
|
+
* (quotes and `${…}` expressions are kept, newlines preserved), so detection
|
|
114
|
+
* regexes only ever see code. A lexer-lite, not a parser: regex literals are
|
|
115
|
+
* not special-cased, which is fine for app composition files.
|
|
116
|
+
*/
|
|
117
|
+
function codeOnly(source) {
|
|
118
|
+
let out = '';
|
|
119
|
+
let i = 0;
|
|
120
|
+
// Stack of open template literals; each entry is the `${` brace depth inside it.
|
|
121
|
+
const templates = [];
|
|
122
|
+
const blank = (ch) => (ch === '\n' ? '\n' : ' ');
|
|
123
|
+
const inTemplateText = () => templates.length > 0 && templates[templates.length - 1] === -1;
|
|
124
|
+
while (i < source.length) {
|
|
125
|
+
const ch = source[i];
|
|
126
|
+
const next = source[i + 1];
|
|
127
|
+
if (inTemplateText()) {
|
|
128
|
+
if (ch === '\\') {
|
|
129
|
+
out += ' ';
|
|
130
|
+
i += 2;
|
|
131
|
+
}
|
|
132
|
+
else if (ch === '`') {
|
|
133
|
+
templates.pop();
|
|
134
|
+
out += ch;
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
else if (ch === '$' && next === '{') {
|
|
138
|
+
templates[templates.length - 1] = 0;
|
|
139
|
+
out += '${';
|
|
140
|
+
i += 2;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
out += blank(ch);
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (ch === '/' && next === '/') {
|
|
149
|
+
while (i < source.length && source[i] !== '\n') {
|
|
150
|
+
out += ' ';
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (ch === '/' && next === '*') {
|
|
155
|
+
const end = source.indexOf('*/', i + 2);
|
|
156
|
+
const stop = end === -1 ? source.length : end + 2;
|
|
157
|
+
for (; i < stop; i++)
|
|
158
|
+
out += blank(source[i]);
|
|
159
|
+
}
|
|
160
|
+
else if (ch === "'" || ch === '"') {
|
|
161
|
+
out += ch;
|
|
162
|
+
i++;
|
|
163
|
+
while (i < source.length && source[i] !== ch && source[i] !== '\n') {
|
|
164
|
+
if (source[i] === '\\') {
|
|
165
|
+
out += ' ';
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
if (i < source.length)
|
|
169
|
+
out += blank(source[i]);
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
if (i < source.length) {
|
|
173
|
+
out += source[i];
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (ch === '`') {
|
|
178
|
+
templates.push(-1);
|
|
179
|
+
out += ch;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
if (templates.length > 0) {
|
|
184
|
+
const top = templates.length - 1;
|
|
185
|
+
if (ch === '{')
|
|
186
|
+
templates[top] = templates[top] + 1;
|
|
187
|
+
else if (ch === '}') {
|
|
188
|
+
if (templates[top] === 0)
|
|
189
|
+
templates[top] = -1;
|
|
190
|
+
else
|
|
191
|
+
templates[top] = templates[top] - 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
out += ch;
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
107
200
|
function detectServerFile(reader) {
|
|
108
201
|
const found = firstExisting(reader, SERVER_CANDIDATES);
|
|
109
202
|
if (!found)
|
package/dist/doctor/rules.js
CHANGED
|
@@ -109,23 +109,28 @@ export const DEFAULT_RULES = [
|
|
|
109
109
|
check(ctx) {
|
|
110
110
|
if (!ctx.app || !ctx.stack.tenancy || !ctx.stack.auth)
|
|
111
111
|
return null;
|
|
112
|
-
const teamsAvailable = ctx.app.pluginCalls.includes('teamsPlugin') || ctx.installed.includes('@basaltkit/teams');
|
|
113
|
-
if (!teamsAvailable)
|
|
114
|
-
return null;
|
|
115
112
|
if (ctx.app.pluginCalls.includes('tenantMembershipPlugin'))
|
|
116
113
|
return null;
|
|
114
|
+
// Fire whether or not @basaltkit/teams is installed: tenancy + auth with no
|
|
115
|
+
// membership guard is the vulnerable shape regardless — staying silent
|
|
116
|
+
// because the package providing the fix is absent hid it in fresh apps.
|
|
117
|
+
const teamsInstalled = ctx.app.pluginCalls.includes('teamsPlugin') || ctx.installed.includes('@basaltkit/teams');
|
|
117
118
|
return {
|
|
118
119
|
id: 'missing-tenant-membership',
|
|
119
120
|
title: 'Tenant is resolved from the request but membership is never enforced',
|
|
120
121
|
severity: 'error',
|
|
121
122
|
category: 'tenancy',
|
|
122
|
-
detected: `${ctx.app.path} wires tenancy + auth (+ teams) but no tenantMembershipPlugin`,
|
|
123
|
-
recommended:
|
|
123
|
+
detected: `${ctx.app.path} wires tenancy + auth${teamsInstalled ? ' (+ teams)' : ''} but no tenantMembershipPlugin`,
|
|
124
|
+
recommended: teamsInstalled
|
|
125
|
+
? 'register tenantMembershipPlugin() so every authenticated, tenant-scoped request verifies the user belongs to the resolved tenant'
|
|
126
|
+
: 'install @basaltkit/teams and register teamsPlugin() + tenantMembershipPlugin() so every authenticated, tenant-scoped request verifies the user belongs to the resolved tenant',
|
|
124
127
|
reason: 'Resolvers take the tenant from client input (x-tenant-id / Host). Without a ' +
|
|
125
128
|
'membership check, any authenticated user can act on another tenant just by ' +
|
|
126
129
|
'changing that header — a cross-tenant data breach. Tenant resolution is ' +
|
|
127
130
|
'identification, never authorization.',
|
|
128
|
-
fix:
|
|
131
|
+
fix: teamsInstalled
|
|
132
|
+
? 'tenantMembershipPlugin() // central routes opt out with meta: { central: true }'
|
|
133
|
+
: 'pnpm add @basaltkit/teams\n// then: teamsPlugin(), tenantMembershipPlugin() // central routes opt out with meta: { central: true }',
|
|
129
134
|
docs: '/guide/security',
|
|
130
135
|
};
|
|
131
136
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
|
18
18
|
export { renderPlan } from './plan/render.js';
|
|
19
19
|
export type { ArchitecturePlan, PlanEntity, PlanField, PlanRelation, PlanStep, PlanStepKind, } from './plan/types.js';
|
|
20
20
|
export { runMake } from './make/make.js';
|
|
21
|
+
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
|
21
22
|
export { renderMakeResult } from './make/render.js';
|
|
22
23
|
export { verifyProject, type VerifyResult } from './make/verify.js';
|
|
23
24
|
export { canonicalType, domainFields, injectPrismaFields, injectZodFields, prismaType, zodForField, zodValidator, type CanonicalType, } from './make/fields.js';
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
|
24
24
|
export { renderPlan } from './plan/render.js';
|
|
25
25
|
// Implementation (spec §4 Implement) — execute a plan via the generator + review gate.
|
|
26
26
|
export { runMake } from './make/make.js';
|
|
27
|
+
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
|
27
28
|
export { renderMakeResult } from './make/render.js';
|
|
28
29
|
export { verifyProject } from './make/verify.js';
|
|
29
30
|
export { canonicalType, domainFields, injectPrismaFields, injectZodFields, prismaType, zodForField, zodValidator, } from './make/fields.js';
|
package/dist/make/fields.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare function prismaType(t: CanonicalType): string;
|
|
|
11
11
|
* ISO string, matching the mapper.
|
|
12
12
|
*/
|
|
13
13
|
export declare function zodValidator(t: CanonicalType, create: boolean): string;
|
|
14
|
+
/**
|
|
15
|
+
* A single-quoted TS string literal for arbitrary text. Plan enum values are
|
|
16
|
+
* untrusted (LLM output / MCP client input): escaping via JSON.stringify means
|
|
17
|
+
* quotes, backslashes and line breaks can never end the literal early.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tsStringLiteral(value: string): string;
|
|
14
20
|
/** Zod validator for a field, honoring an enum (`z.enum([...])`) over its base type. */
|
|
15
21
|
export declare function zodForField(field: PlanField, create: boolean): string;
|
|
16
22
|
/** The entity's own fields, minus reserved/base columns and duplicates. */
|
package/dist/make/fields.js
CHANGED
|
@@ -55,10 +55,24 @@ export function zodValidator(t, create) {
|
|
|
55
55
|
function usable(field) {
|
|
56
56
|
return field.name.trim() !== '' && !RESERVED.has(field.name.toLowerCase());
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A single-quoted TS string literal for arbitrary text. Plan enum values are
|
|
60
|
+
* untrusted (LLM output / MCP client input): escaping via JSON.stringify means
|
|
61
|
+
* quotes, backslashes and line breaks can never end the literal early.
|
|
62
|
+
*/
|
|
63
|
+
export function tsStringLiteral(value) {
|
|
64
|
+
const escaped = JSON.stringify(String(value))
|
|
65
|
+
.slice(1, -1)
|
|
66
|
+
.replace(/\\"/g, '"')
|
|
67
|
+
.replace(/'/g, "\\'")
|
|
68
|
+
.replace(/\u2028/g, '\\u2028')
|
|
69
|
+
.replace(/\u2029/g, '\\u2029');
|
|
70
|
+
return `'${escaped}'`;
|
|
71
|
+
}
|
|
58
72
|
/** Zod validator for a field, honoring an enum (`z.enum([...])`) over its base type. */
|
|
59
73
|
export function zodForField(field, create) {
|
|
60
74
|
if (field.enum && field.enum.length > 0) {
|
|
61
|
-
return `z.enum([${field.enum.map(
|
|
75
|
+
return `z.enum([${field.enum.map(tsStringLiteral).join(', ')}])`;
|
|
62
76
|
}
|
|
63
77
|
return zodValidator(canonicalType(field.type), create);
|
|
64
78
|
}
|
package/dist/make/make.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { unifiedDiff } from './diff.js';
|
|
7
7
|
import { domainFields, injectPrismaFields, injectZodFields } from './fields.js';
|
|
8
|
+
import { assertSafePlan } from '../plan/identifiers.js';
|
|
8
9
|
import { externalRelationTargets, injectPrismaRelations, inverseRelationLines, relationFieldLines, relationForeignKeys, } from './relations.js';
|
|
9
10
|
import { injectOpenApiMeta } from './openapi.js';
|
|
10
11
|
import { renderPermissionsFile } from './permissions.js';
|
|
@@ -22,6 +23,9 @@ import { injectAuditPlugin, injectAuditService, injectPermissionGuards } from '.
|
|
|
22
23
|
* touches the database, never deletes.
|
|
23
24
|
*/
|
|
24
25
|
export async function runMake(ctx, plan, options = {}) {
|
|
26
|
+
// The plan is untrusted (LLM / MCP client): refuse any name that could
|
|
27
|
+
// inject code into the generated sources before generating anything.
|
|
28
|
+
assertSafePlan(plan);
|
|
25
29
|
const entities = plan.entities.filter((e) => e.name.trim() !== '');
|
|
26
30
|
if (entities.length === 0) {
|
|
27
31
|
throw new Error('ai:make — the plan has no entity to generate.');
|
|
@@ -36,7 +40,13 @@ export async function runMake(ctx, plan, options = {}) {
|
|
|
36
40
|
const gen = findGeneratorCommand(plan, entity.name);
|
|
37
41
|
const prisma = options.prisma ?? gen?.prisma ?? prismaDefault;
|
|
38
42
|
const softDelete = options.softDelete ?? gen?.softDelete ?? false;
|
|
39
|
-
|
|
43
|
+
// Routes are authenticated by the generator's secure default. A
|
|
44
|
+
// tenant-scoped entity gets the generator's tenant-owned in-memory
|
|
45
|
+
// repository (requireTenantId()); with Prisma, augmentFiles below replaces
|
|
46
|
+
// the repository and injects tenantId + @@index itself, so the generator's
|
|
47
|
+
// tenant output is not requested there (it would duplicate the index).
|
|
48
|
+
const tenant = ctx.stack.tenancy && entity.tenantScoped && !prisma;
|
|
49
|
+
const generated = generateResource(entity.name, { prisma, softDelete, tenant });
|
|
40
50
|
const { files, augmented, guarded, audited } = augmentFiles(generated, entity, plan, ctx, { prisma, softDelete });
|
|
41
51
|
const build = {
|
|
42
52
|
name: entity.name,
|
package/dist/make/repository.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { names } from '@basaltkit/generator/resource';
|
|
2
|
-
import { canonicalType, domainFields } from './fields.js';
|
|
2
|
+
import { canonicalType, domainFields, tsStringLiteral } from './fields.js';
|
|
3
3
|
/** TypeScript type of a Prisma row column for the mapper's parameter. */
|
|
4
4
|
function rowType(t) {
|
|
5
5
|
switch (t) {
|
|
@@ -38,7 +38,7 @@ export function renderPrismaRepository(name, fields, options) {
|
|
|
38
38
|
}
|
|
39
39
|
// Enum columns are stored as String; the mapper narrows them to the union.
|
|
40
40
|
const colType = (f) => (f.values ? 'string' : rowType(f.canon));
|
|
41
|
-
const colExpr = (f) => f.values ? `r.${f.name} as ${f.values.map(
|
|
41
|
+
const colExpr = (f) => f.values ? `r.${f.name} as ${f.values.map(tsStringLiteral).join(' | ')}` : mapExpr(f.name, f.canon);
|
|
42
42
|
const rowTypeEntries = [
|
|
43
43
|
'id: string',
|
|
44
44
|
...bodyFields.map((f) => `${f.name}: ${colType(f)}`),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ArchitecturePlan } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A plan is untrusted input — an LLM output, or a plan object an MCP client
|
|
4
|
+
* hands to `basalt_make`. Every name in it is interpolated into generated
|
|
5
|
+
* TypeScript/Prisma source, so names are restricted to shapes that can never
|
|
6
|
+
* break out of their syntactic position (identifier, string literal).
|
|
7
|
+
*/
|
|
8
|
+
/** Field and relation names — emitted verbatim as TS properties and Prisma columns. */
|
|
9
|
+
export declare const PLAN_IDENTIFIER: RegExp;
|
|
10
|
+
/** Entity / related-model names — normalized by the generator's `names()` (words → PascalCase). */
|
|
11
|
+
export declare const PLAN_ENTITY_NAME: RegExp;
|
|
12
|
+
/** Audit event names (`invoice.created`, `billing:paid`) — emitted inside string literals. */
|
|
13
|
+
export declare const PLAN_EVENT_NAME: RegExp;
|
|
14
|
+
export declare class UnsafePlanError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Throws {@link UnsafePlanError} if any name in the plan could inject code into
|
|
19
|
+
* the generated sources. Enum VALUES are free text: they are always emitted as
|
|
20
|
+
* escaped string literals (see `tsStringLiteral`), never raw.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertSafePlan(plan: ArchitecturePlan): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plan is untrusted input — an LLM output, or a plan object an MCP client
|
|
3
|
+
* hands to `basalt_make`. Every name in it is interpolated into generated
|
|
4
|
+
* TypeScript/Prisma source, so names are restricted to shapes that can never
|
|
5
|
+
* break out of their syntactic position (identifier, string literal).
|
|
6
|
+
*/
|
|
7
|
+
/** Field and relation names — emitted verbatim as TS properties and Prisma columns. */
|
|
8
|
+
export const PLAN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
/** Entity / related-model names — normalized by the generator's `names()` (words → PascalCase). */
|
|
10
|
+
export const PLAN_ENTITY_NAME = /^[A-Za-z][A-Za-z0-9 _-]{0,99}$/;
|
|
11
|
+
/** Audit event names (`invoice.created`, `billing:paid`) — emitted inside string literals. */
|
|
12
|
+
export const PLAN_EVENT_NAME = /^[A-Za-z0-9_]+(?:[.:-][A-Za-z0-9_]+)*$/;
|
|
13
|
+
export class UnsafePlanError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(`ai:make — unsafe plan: ${message}`);
|
|
16
|
+
this.name = 'UnsafePlanError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Throws {@link UnsafePlanError} if any name in the plan could inject code into
|
|
21
|
+
* the generated sources. Enum VALUES are free text: they are always emitted as
|
|
22
|
+
* escaped string literals (see `tsStringLiteral`), never raw.
|
|
23
|
+
*/
|
|
24
|
+
export function assertSafePlan(plan) {
|
|
25
|
+
const entities = plan.entities;
|
|
26
|
+
if (!Array.isArray(entities))
|
|
27
|
+
throw new UnsafePlanError('`entities` must be an array.');
|
|
28
|
+
for (const entity of plan.entities) {
|
|
29
|
+
if (typeof entity.name !== 'string' || !PLAN_ENTITY_NAME.test(entity.name)) {
|
|
30
|
+
throw new UnsafePlanError(`entity name ${JSON.stringify(entity.name)} is not a valid identifier.`);
|
|
31
|
+
}
|
|
32
|
+
for (const field of entity.fields ?? []) {
|
|
33
|
+
if (typeof field.name !== 'string' || !PLAN_IDENTIFIER.test(field.name)) {
|
|
34
|
+
throw new UnsafePlanError(`field name ${JSON.stringify(field.name)} on ${entity.name} is not a valid identifier.`);
|
|
35
|
+
}
|
|
36
|
+
if (field.enum !== undefined && !(Array.isArray(field.enum) && field.enum.every((v) => typeof v === 'string'))) {
|
|
37
|
+
throw new UnsafePlanError(`enum of ${entity.name}.${field.name} must be an array of strings.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const relation of entity.relations ?? []) {
|
|
41
|
+
if (typeof relation.name !== 'string' || !PLAN_IDENTIFIER.test(relation.name)) {
|
|
42
|
+
throw new UnsafePlanError(`relation name ${JSON.stringify(relation.name)} on ${entity.name} is not a valid identifier.`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof relation.model !== 'string' || !PLAN_ENTITY_NAME.test(relation.model)) {
|
|
45
|
+
throw new UnsafePlanError(`relation model ${JSON.stringify(relation.model)} on ${entity.name} is not a valid identifier.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const event of plan.auditEvents ?? []) {
|
|
50
|
+
if (typeof event !== 'string' || !PLAN_EVENT_NAME.test(event)) {
|
|
51
|
+
throw new UnsafePlanError(`audit event ${JSON.stringify(event)} is not a valid event name.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/schema/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* tolerant coercion — the model's raw text is never fed to `.parse()` directly).
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
+
import { PLAN_ENTITY_NAME, PLAN_EVENT_NAME, PLAN_IDENTIFIER } from '../plan/identifiers.js';
|
|
13
14
|
/** Bump when the serialized shape of a plan changes incompatibly. */
|
|
14
15
|
export const PLAN_SCHEMA_VERSION = 1;
|
|
15
16
|
/** Bump when the serialized shape of a make result changes incompatibly. */
|
|
@@ -103,17 +104,20 @@ export const AnalysisReportSchema = z.object({
|
|
|
103
104
|
diagnostics: z.array(DiagnosticSchema),
|
|
104
105
|
});
|
|
105
106
|
// --- ArchitecturePlan (plan/types.ts) --------------------------------------
|
|
107
|
+
// Names are interpolated into generated source: only plain identifiers are
|
|
108
|
+
// accepted (see plan/identifiers.ts). Enum values are free text, always
|
|
109
|
+
// emitted as escaped string literals.
|
|
106
110
|
export const PlanFieldSchema = z.object({
|
|
107
|
-
name: z.string(),
|
|
111
|
+
name: z.string().regex(PLAN_IDENTIFIER),
|
|
108
112
|
type: z.string(),
|
|
109
113
|
enum: z.array(z.string()).optional(),
|
|
110
114
|
});
|
|
111
115
|
export const PlanRelationSchema = z.object({
|
|
112
|
-
name: z.string(),
|
|
113
|
-
model: z.string(),
|
|
116
|
+
name: z.string().regex(PLAN_IDENTIFIER),
|
|
117
|
+
model: z.string().regex(PLAN_ENTITY_NAME),
|
|
114
118
|
});
|
|
115
119
|
export const PlanEntitySchema = z.object({
|
|
116
|
-
name: z.string(),
|
|
120
|
+
name: z.string().regex(PLAN_ENTITY_NAME),
|
|
117
121
|
fields: z.array(PlanFieldSchema),
|
|
118
122
|
tenantScoped: z.boolean(),
|
|
119
123
|
relations: z.array(PlanRelationSchema).optional(),
|
|
@@ -145,7 +149,7 @@ export const ArchitecturePlanSchema = z.object({
|
|
|
145
149
|
entities: z.array(PlanEntitySchema),
|
|
146
150
|
steps: z.array(PlanStepSchema),
|
|
147
151
|
permissions: z.array(z.string()),
|
|
148
|
-
auditEvents: z.array(z.string()),
|
|
152
|
+
auditEvents: z.array(z.string().regex(PLAN_EVENT_NAME)),
|
|
149
153
|
tenantScoped: z.boolean(),
|
|
150
154
|
warnings: z.array(z.string()),
|
|
151
155
|
});
|
package/dist/workflows.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export type { ArchitecturePlan, PlanEntity, PlanField, PlanRelation, PlanStep, P
|
|
|
16
16
|
export { buildReviewContext, parseReview, reviewImplementation, type ReviewOptions, } from './review/review.js';
|
|
17
17
|
export type { AgentReview, ReviewIssue } from './review/types.js';
|
|
18
18
|
export { runMake } from './make/make.js';
|
|
19
|
+
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
|
19
20
|
export type { MakeOptions, MakeResult, MakePreview, FilePreview, ResourceBuild, SchemaMerge, Migration, ReviewResult, ReviewItem, ReviewStatus, } from './make/types.js';
|
package/dist/workflows.js
CHANGED
|
@@ -21,3 +21,4 @@ export { buildReviewContext, parseReview, reviewImplementation, } from './review
|
|
|
21
21
|
// dry-run detects clashes and attaches diffs; writes never touch the DB unless
|
|
22
22
|
// `migrate` is set. Framework-free via `@basaltkit/generator/resource`.
|
|
23
23
|
export { runMake } from './make/make.js';
|
|
24
|
+
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"zod": "^4.5.4",
|
|
34
|
-
"@basaltkit/cli": "^1.2.
|
|
35
|
-
"@basaltkit/generator": "^1.
|
|
34
|
+
"@basaltkit/cli": "^1.2.5",
|
|
35
|
+
"@basaltkit/generator": "^1.4.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^26.
|
|
38
|
+
"@types/node": "^26.5.1",
|
|
39
39
|
"typescript": "^7.0.2",
|
|
40
|
-
"vitest": "^
|
|
40
|
+
"vitest": "^5.0.0",
|
|
41
41
|
"@basaltkit/tsconfig": "^0.24.0"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|