@beignet/cli 0.0.23 → 0.0.25
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/CHANGELOG.md +28 -0
- package/README.md +55 -6
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +201 -1
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +238 -235
- package/dist/inspect.js.map +1 -1
- package/dist/make.js +16 -15
- package/dist/make.js.map +1 -1
- package/dist/provider-audit.d.ts +131 -0
- package/dist/provider-audit.d.ts.map +1 -0
- package/dist/provider-audit.js +1013 -0
- package/dist/provider-audit.js.map +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +3 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/mysql.js +2 -2
- package/dist/templates/db/postgres.js +2 -2
- package/dist/templates/db/sqlite.js +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +1 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -0
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +90 -20
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/todos.js +5 -5
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +22 -4
- package/src/config.ts +99 -0
- package/src/db.ts +316 -1
- package/src/index.ts +180 -1
- package/src/inspect.ts +412 -369
- package/src/make.ts +16 -15
- package/src/provider-audit.ts +1681 -0
- package/src/templates/base.ts +3 -2
- package/src/templates/db/mysql.ts +2 -2
- package/src/templates/db/postgres.ts +2 -2
- package/src/templates/db/sqlite.ts +2 -2
- package/src/templates/index.ts +1 -0
- package/src/templates/server.ts +90 -20
- package/src/templates/todos.ts +5 -5
package/dist/inspect.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { parseProviderPackageMetadata, } from "@beignet/core/providers";
|
|
3
|
+
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
5
4
|
import { createPainter } from "./ansi.js";
|
|
6
|
-
import { clientDirPath, clientFormsPath, clientIndexPath, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
5
|
+
import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
7
6
|
import { formatGithubAnnotation, } from "./github-annotations.js";
|
|
7
|
+
import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderMetadataIssues, installedPackageNames, providerDoctorRulesForInstalledPackages, providerOperationalConfigFiles, providerRequiredEnvExists, providerRequiredTableExists, providerVariantRegistrationSeverity, readProviderListEntries, readProviderPackageMetadataSource, registrationTokenHint, serverProviderFiles, } from "./provider-audit.js";
|
|
8
8
|
import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryValueInfo, } from "./registry-edits.js";
|
|
9
9
|
import { getCliVersion } from "./version.js";
|
|
10
10
|
export async function inspectApp(options = {}) {
|
|
@@ -135,9 +135,47 @@ export function formatDoctor(result, options = {}) {
|
|
|
135
135
|
${diagnostic.message}`;
|
|
136
136
|
}));
|
|
137
137
|
}
|
|
138
|
+
const checklist = productionHardeningChecklist(result.diagnostics);
|
|
139
|
+
if (checklist.length > 0) {
|
|
140
|
+
lines.push("", paint("Production Hardening Checklist:", "bold"), "");
|
|
141
|
+
lines.push(...checklist.map((item) => `- ${item}`));
|
|
142
|
+
}
|
|
138
143
|
lines.push("", formatDoctorFooter(result));
|
|
139
144
|
return lines.join("\n");
|
|
140
145
|
}
|
|
146
|
+
const productionHardeningDiagnosticCodes = new Set([
|
|
147
|
+
"BEIGNET_AUTH_ROUTE_MISSING",
|
|
148
|
+
"BEIGNET_AUTH_TRUSTED_ORIGINS_MISSING",
|
|
149
|
+
"BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
150
|
+
"BEIGNET_BILLING_CHECKOUT_IDEMPOTENCY_MISSING",
|
|
151
|
+
"BEIGNET_BILLING_ENTITLEMENTS_PROVIDER_MISSING",
|
|
152
|
+
"BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
|
|
153
|
+
"BEIGNET_CORS_CREDENTIALS_WILDCARD",
|
|
154
|
+
"BEIGNET_CRON_SECRET_MISSING",
|
|
155
|
+
"BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
|
|
156
|
+
"BEIGNET_ENTITLEMENTS_PORT_MISSING",
|
|
157
|
+
"BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
|
|
158
|
+
"BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
159
|
+
"BEIGNET_PROVIDER_ENV_MISSING",
|
|
160
|
+
"BEIGNET_PROVIDER_TABLE_MISSING",
|
|
161
|
+
"BEIGNET_READINESS_ROUTE_MISSING",
|
|
162
|
+
"BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
163
|
+
"BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
164
|
+
"BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
|
|
165
|
+
]);
|
|
166
|
+
function productionHardeningChecklist(diagnostics) {
|
|
167
|
+
if (!diagnostics.some((diagnostic) => productionHardeningDiagnosticCodes.has(diagnostic.code))) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return [
|
|
171
|
+
"Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
|
|
172
|
+
"Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
|
|
173
|
+
"Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
|
|
174
|
+
"CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
175
|
+
"Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
|
|
176
|
+
"Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
|
|
177
|
+
];
|
|
178
|
+
}
|
|
141
179
|
function formatDoctorFooter(result) {
|
|
142
180
|
const count = (severity) => result.diagnostics.filter((diagnostic) => diagnostic.severity === severity)
|
|
143
181
|
.length;
|
|
@@ -1187,6 +1225,8 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1187
1225
|
const installedPackages = installedPackageNames(packageJson);
|
|
1188
1226
|
const providerDoctorRules = await providerDoctorRulesForInstalledPackages(targetDir, packageJson);
|
|
1189
1227
|
const configFiles = operationalConfigFiles(files, config);
|
|
1228
|
+
const infrastructurePath = directoryPath(path.dirname(config.paths.infrastructurePorts));
|
|
1229
|
+
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
1190
1230
|
for (const file of files) {
|
|
1191
1231
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
|
|
1192
1232
|
continue;
|
|
@@ -1222,6 +1262,26 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1222
1262
|
message: `${file} defines an upload without maxSizeBytes. Add an explicit size limit so upload behavior is predictable before deployment.`,
|
|
1223
1263
|
});
|
|
1224
1264
|
}
|
|
1265
|
+
if (isFeatureUploadDefinition(file, config) &&
|
|
1266
|
+
containsCallExpression(source, "defineUpload") &&
|
|
1267
|
+
!/\bauthorize\s*(?:\(|:)/.test(source) &&
|
|
1268
|
+
!/\baccess\s*:\s*["']public["']/.test(source)) {
|
|
1269
|
+
diagnostics.push({
|
|
1270
|
+
severity: "warning",
|
|
1271
|
+
code: "BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
1272
|
+
file,
|
|
1273
|
+
message: `${file} defines a protected upload without authorize(...). Add an authorize hook, or set access: "public" only for intentionally public uploads.`,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
if (isRouteServerOrInfraFile(file, config) &&
|
|
1277
|
+
containsRequestTenantHeaderAuthority(source)) {
|
|
1278
|
+
diagnostics.push({
|
|
1279
|
+
severity: "warning",
|
|
1280
|
+
code: "BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
1281
|
+
file,
|
|
1282
|
+
message: `${file} reads x-tenant-id and turns it into tenant authority. Resolve request tenants from verified auth/session claims or trusted gateway metadata instead of a caller-controlled header.`,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1225
1285
|
if (isRouteServerOrInfraFile(file, config) &&
|
|
1226
1286
|
containsCallExpression(source, "createCorsHooks") &&
|
|
1227
1287
|
/\bcredentials\s*:\s*true\b/.test(source) &&
|
|
@@ -1234,6 +1294,17 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1234
1294
|
});
|
|
1235
1295
|
}
|
|
1236
1296
|
}
|
|
1297
|
+
for (const file of configFiles) {
|
|
1298
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
1299
|
+
if (containsBetterAuthLocalSecret(source)) {
|
|
1300
|
+
diagnostics.push({
|
|
1301
|
+
severity: "warning",
|
|
1302
|
+
code: "BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
1303
|
+
file,
|
|
1304
|
+
message: `${file} contains the known local Better Auth placeholder secret. Keep it out of production configuration and require a unique BETTER_AUTH_SECRET before deployment.`,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1237
1308
|
const providerEntries = await readProviderListEntries(targetDir, files, config, sourceCache);
|
|
1238
1309
|
const stripeMemoryProviderMismatch = await hasStripeMemoryProviderMismatch(targetDir, sourceCache, configFiles, installedPackages, providerEntries);
|
|
1239
1310
|
diagnostics.push(...(await inspectEntitlementsProductionReadiness(targetDir, files, config, sourceCache)), ...(await inspectPaymentsProductionReadiness(targetDir, files, config, sourceCache, installedPackages, stripeMemoryProviderMismatch)));
|
|
@@ -1245,7 +1316,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1245
1316
|
// app actually registers; an unused variant must not warn.
|
|
1246
1317
|
for (const variant of detectedProviderVariants(provider.variants, providerEntries)) {
|
|
1247
1318
|
for (const envVar of variant.requiredEnv) {
|
|
1248
|
-
if (await
|
|
1319
|
+
if (await providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache)) {
|
|
1249
1320
|
continue;
|
|
1250
1321
|
}
|
|
1251
1322
|
diagnostics.push({
|
|
@@ -1255,11 +1326,22 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1255
1326
|
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but ${envVar} is not mentioned in app env/config files. ${variant.displayName} requires ${envVar}; add the environment variable or remove the provider registration.`,
|
|
1256
1327
|
});
|
|
1257
1328
|
}
|
|
1329
|
+
for (const tableName of configuredProviderRequiredTables(variant.requiredTables, config)) {
|
|
1330
|
+
if (await providerRequiredTableExists(targetDir, files, config, infrastructurePath, schemaDir, tableName, sourceCache)) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
diagnostics.push({
|
|
1334
|
+
severity: "warning",
|
|
1335
|
+
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
1336
|
+
file: schemaDir,
|
|
1337
|
+
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema generate, include it in your Drizzle schema/migrations, or remove the provider registration.`,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1258
1340
|
}
|
|
1259
1341
|
continue;
|
|
1260
1342
|
}
|
|
1261
1343
|
for (const envVar of provider.requiredEnv ?? []) {
|
|
1262
|
-
if (await
|
|
1344
|
+
if (await providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache)) {
|
|
1263
1345
|
continue;
|
|
1264
1346
|
}
|
|
1265
1347
|
diagnostics.push({
|
|
@@ -1269,6 +1351,17 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1269
1351
|
message: `${provider.packageName} is installed, but ${envVar} is not mentioned in app env/config files. Add the expected environment variable or remove the unused provider package.`,
|
|
1270
1352
|
});
|
|
1271
1353
|
}
|
|
1354
|
+
for (const tableName of configuredProviderRequiredTables(provider.requiredTables ?? [], config)) {
|
|
1355
|
+
if (await providerRequiredTableExists(targetDir, files, config, infrastructurePath, schemaDir, tableName, sourceCache)) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
diagnostics.push({
|
|
1359
|
+
severity: "warning",
|
|
1360
|
+
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
1361
|
+
file: schemaDir,
|
|
1362
|
+
message: `${provider.packageName} is installed, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema generate, include it in your Drizzle schema/migrations, or remove the unused provider package.`,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1272
1365
|
}
|
|
1273
1366
|
if (installedPackages.has("@beignet/provider-auth-better-auth")) {
|
|
1274
1367
|
if (!hasBetterAuthRoute(files, config)) {
|
|
@@ -1323,6 +1416,19 @@ function containsReadinessCheck(source) {
|
|
|
1323
1416
|
return false;
|
|
1324
1417
|
return /\bchecks\s*:/.test(source) || /\.checkHealth\s*\(/.test(source);
|
|
1325
1418
|
}
|
|
1419
|
+
function containsBetterAuthLocalSecret(source) {
|
|
1420
|
+
if (!source.includes("local-dev-better-auth-secret-change-me"))
|
|
1421
|
+
return false;
|
|
1422
|
+
const hasProductionRuntimeGuard = /\bisProductionRuntime\s*=\s*process\.env\.NODE_ENV\s*===\s*["']production["']\s*&&\s*process\.env\.NEXT_PHASE\s*!==\s*["']phase-production-build["']/.test(source) &&
|
|
1423
|
+
/BETTER_AUTH_SECRET\s*:\s*isProductionRuntime\s*\?\s*BetterAuthSecret\s*:\s*BetterAuthSecret\.default\(LOCAL_BETTER_AUTH_SECRET\)/s.test(source);
|
|
1424
|
+
const hasProductionGuard = /BETTER_AUTH_SECRET\s*:\s*process\.env\.NODE_ENV\s*===\s*["']production["']\s*\?\s*BetterAuthSecret\s*:\s*BetterAuthSecret\.default\(LOCAL_BETTER_AUTH_SECRET\)/s.test(source);
|
|
1425
|
+
return !(hasProductionRuntimeGuard || hasProductionGuard);
|
|
1426
|
+
}
|
|
1427
|
+
function containsRequestTenantHeaderAuthority(source) {
|
|
1428
|
+
return (/(?:(?:\breq|\brequest)\.headers|(?<!\.)\bheaders)\.get\(\s*["']x-tenant-id["']\s*\)/.test(source) &&
|
|
1429
|
+
(/\bcreateTenant\s*\(/.test(source) ||
|
|
1430
|
+
/\btenant\s*:\s*\{?\s*id\s*:/.test(source)));
|
|
1431
|
+
}
|
|
1326
1432
|
async function inspectEntitlementsProductionReadiness(targetDir, files, config, sourceCache) {
|
|
1327
1433
|
if (!(await usesEntitlementChecks(targetDir, files, sourceCache)) ||
|
|
1328
1434
|
(await appPortsIncludesPort(targetDir, files, config, sourceCache, "entitlements"))) {
|
|
@@ -1599,58 +1705,13 @@ function isFeatureUploadDefinition(file, config) {
|
|
|
1599
1705
|
return new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/uploads/[^/]+\\.ts$`).test(file);
|
|
1600
1706
|
}
|
|
1601
1707
|
function operationalConfigFiles(files, config) {
|
|
1602
|
-
return files
|
|
1603
|
-
if (file === ".env.example")
|
|
1604
|
-
return true;
|
|
1605
|
-
if (file === "drizzle.config.ts")
|
|
1606
|
-
return true;
|
|
1607
|
-
if (file === "lib/env.ts" || file === "lib/better-auth.ts")
|
|
1608
|
-
return true;
|
|
1609
|
-
if (isInfraOrServerFile(file, config) && file.endsWith(".ts"))
|
|
1610
|
-
return true;
|
|
1611
|
-
return false;
|
|
1612
|
-
});
|
|
1708
|
+
return providerOperationalConfigFiles(files, config);
|
|
1613
1709
|
}
|
|
1614
1710
|
async function readPackageJson(targetDir, files) {
|
|
1615
1711
|
if (!files.includes("package.json"))
|
|
1616
1712
|
return undefined;
|
|
1617
1713
|
return JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
|
|
1618
1714
|
}
|
|
1619
|
-
async function providerDoctorRulesForInstalledPackages(targetDir, packageJson) {
|
|
1620
|
-
const packageNames = [...installedPackageNames(packageJson)];
|
|
1621
|
-
const rules = await Promise.all(packageNames.map(async (packageName) => {
|
|
1622
|
-
const source = await readProviderPackageMetadataSource(targetDir, packageName);
|
|
1623
|
-
if (!source)
|
|
1624
|
-
return undefined;
|
|
1625
|
-
const result = parseProviderPackageMetadata(source.metadata);
|
|
1626
|
-
if (!result.success)
|
|
1627
|
-
return undefined;
|
|
1628
|
-
return providerDoctorRuleFromMetadata(packageName, result.metadata);
|
|
1629
|
-
}));
|
|
1630
|
-
return rules.filter((rule) => Boolean(rule));
|
|
1631
|
-
}
|
|
1632
|
-
async function readProviderPackageMetadataSource(targetDir, packageName) {
|
|
1633
|
-
for (const candidate of providerPackageJsonCandidates(targetDir, packageName)) {
|
|
1634
|
-
const packageJson = await readJsonIfExists(candidate);
|
|
1635
|
-
const metadata = packageJson?.beignet?.provider;
|
|
1636
|
-
if (metadata !== undefined) {
|
|
1637
|
-
return { packageName, file: candidate, metadata };
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
return undefined;
|
|
1641
|
-
}
|
|
1642
|
-
function providerPackageJsonCandidates(targetDir, packageName) {
|
|
1643
|
-
const candidates = [
|
|
1644
|
-
path.join(targetDir, "node_modules", ...packageName.split("/"), "package.json"),
|
|
1645
|
-
];
|
|
1646
|
-
if (packageName.startsWith("@beignet/")) {
|
|
1647
|
-
candidates.push(path.resolve(
|
|
1648
|
-
// import.meta.dir is Bun-only; the published bin also runs under
|
|
1649
|
-
// plain Node, where only import.meta.url is available.
|
|
1650
|
-
path.dirname(fileURLToPath(import.meta.url)), "../../..", "packages", packageName.slice("@beignet/".length), "package.json"));
|
|
1651
|
-
}
|
|
1652
|
-
return candidates;
|
|
1653
|
-
}
|
|
1654
1715
|
async function readJsonIfExists(file) {
|
|
1655
1716
|
try {
|
|
1656
1717
|
return JSON.parse(await readFile(file, "utf8"));
|
|
@@ -1664,158 +1725,6 @@ async function readJsonIfExists(file) {
|
|
|
1664
1725
|
throw error;
|
|
1665
1726
|
}
|
|
1666
1727
|
}
|
|
1667
|
-
function providerDoctorRuleFromMetadata(packageName, metadata) {
|
|
1668
|
-
const registration = metadata.registration;
|
|
1669
|
-
return {
|
|
1670
|
-
packageName,
|
|
1671
|
-
displayName: metadata.displayName ?? packageName,
|
|
1672
|
-
tokens: registration?.tokens ?? [],
|
|
1673
|
-
appPorts: metadata.appPorts ?? [],
|
|
1674
|
-
requiredEnv: metadata.requiredEnv ?? [],
|
|
1675
|
-
registrationSeverity: registrationSeverityFromMetadata(registration),
|
|
1676
|
-
variants: metadata.variants?.map((variant) => ({
|
|
1677
|
-
name: variant.name,
|
|
1678
|
-
displayName: variant.displayName ?? variant.name,
|
|
1679
|
-
tokens: variant.registration?.tokens ?? [],
|
|
1680
|
-
requiredEnv: variant.requiredEnv ?? [],
|
|
1681
|
-
registrationSeverity: registrationSeverityFromMetadata(variant.registration),
|
|
1682
|
-
})),
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
function registrationSeverityFromMetadata(registration) {
|
|
1686
|
-
return (registration?.severity ??
|
|
1687
|
-
(registration?.required === true ? "warning" : undefined));
|
|
1688
|
-
}
|
|
1689
|
-
function detectedProviderVariants(variants, providerEntries) {
|
|
1690
|
-
return variants.filter((variant) => variant.tokens.some((token) => providerEntries.some((entry) => entry.includes(token))));
|
|
1691
|
-
}
|
|
1692
|
-
function providerVariantRegistrationSeverity(variants) {
|
|
1693
|
-
const severities = variants.map((variant) => variant.registrationSeverity);
|
|
1694
|
-
if (severities.includes("warning"))
|
|
1695
|
-
return "warning";
|
|
1696
|
-
if (severities.includes("hint"))
|
|
1697
|
-
return "hint";
|
|
1698
|
-
return undefined;
|
|
1699
|
-
}
|
|
1700
|
-
async function readProviderListEntries(targetDir, files, config, sourceCache) {
|
|
1701
|
-
const providerFiles = serverProviderFiles(files, config);
|
|
1702
|
-
const providerSource = (await Promise.all(providerFiles.map((file) => readCachedSource(targetDir, file, sourceCache)))).join("\n");
|
|
1703
|
-
const providerListSource = extractProviderListSource(providerSource);
|
|
1704
|
-
return providerListSource === undefined
|
|
1705
|
-
? []
|
|
1706
|
-
: providerListEntries(providerListSource);
|
|
1707
|
-
}
|
|
1708
|
-
function registrationTokenHint(tokens) {
|
|
1709
|
-
const token = tokens[0];
|
|
1710
|
-
if (!token)
|
|
1711
|
-
return "the provider";
|
|
1712
|
-
return token.startsWith("create") ? `${token}()` : token;
|
|
1713
|
-
}
|
|
1714
|
-
function diagnosticPackageJsonFile(targetDir, file) {
|
|
1715
|
-
const relative = normalizePath(path.relative(targetDir, file));
|
|
1716
|
-
if (relative.startsWith("../"))
|
|
1717
|
-
return "package.json";
|
|
1718
|
-
return relative;
|
|
1719
|
-
}
|
|
1720
|
-
function formatProviderMetadataIssues(issues) {
|
|
1721
|
-
return issues.map((issue) => `${issue.path} ${issue.message}`).join("; ");
|
|
1722
|
-
}
|
|
1723
|
-
function installedPackageNames(packageJson) {
|
|
1724
|
-
return new Set([
|
|
1725
|
-
...Object.keys(packageJson?.dependencies ?? {}),
|
|
1726
|
-
...Object.keys(packageJson?.devDependencies ?? {}),
|
|
1727
|
-
...Object.keys(packageJson?.peerDependencies ?? {}),
|
|
1728
|
-
]);
|
|
1729
|
-
}
|
|
1730
|
-
function serverProviderFiles(files, config) {
|
|
1731
|
-
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
1732
|
-
const candidates = [`${serverDir}/providers.ts`, config.paths.server];
|
|
1733
|
-
return candidates.filter((file) => files.includes(file));
|
|
1734
|
-
}
|
|
1735
|
-
function extractProviderListSource(source) {
|
|
1736
|
-
const listMatches = [
|
|
1737
|
-
...source.matchAll(/\bproviders\s*=\s*\[([\s\S]*?)\]\s*(?:;|as const)/g),
|
|
1738
|
-
...source.matchAll(/\bproviders\s*:\s*\[([\s\S]*?)\]/g),
|
|
1739
|
-
];
|
|
1740
|
-
if (listMatches.length === 0)
|
|
1741
|
-
return undefined;
|
|
1742
|
-
return listMatches.map((match) => match[1]).join("\n");
|
|
1743
|
-
}
|
|
1744
|
-
function providerListEntries(source) {
|
|
1745
|
-
const entries = [];
|
|
1746
|
-
let current = "";
|
|
1747
|
-
let depth = 0;
|
|
1748
|
-
let quote;
|
|
1749
|
-
let escaped = false;
|
|
1750
|
-
let lineComment = false;
|
|
1751
|
-
let blockComment = false;
|
|
1752
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
1753
|
-
const char = source[index];
|
|
1754
|
-
const next = source[index + 1];
|
|
1755
|
-
if (lineComment) {
|
|
1756
|
-
if (char === "\n") {
|
|
1757
|
-
lineComment = false;
|
|
1758
|
-
current += char;
|
|
1759
|
-
}
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
if (blockComment) {
|
|
1763
|
-
if (char === "*" && next === "/") {
|
|
1764
|
-
blockComment = false;
|
|
1765
|
-
index += 1;
|
|
1766
|
-
}
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
if (quote) {
|
|
1770
|
-
current += char;
|
|
1771
|
-
if (escaped) {
|
|
1772
|
-
escaped = false;
|
|
1773
|
-
continue;
|
|
1774
|
-
}
|
|
1775
|
-
if (char === "\\") {
|
|
1776
|
-
escaped = true;
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
if (char === quote) {
|
|
1780
|
-
quote = undefined;
|
|
1781
|
-
}
|
|
1782
|
-
continue;
|
|
1783
|
-
}
|
|
1784
|
-
if (char === "/" && next === "/") {
|
|
1785
|
-
lineComment = true;
|
|
1786
|
-
index += 1;
|
|
1787
|
-
continue;
|
|
1788
|
-
}
|
|
1789
|
-
if (char === "/" && next === "*") {
|
|
1790
|
-
blockComment = true;
|
|
1791
|
-
index += 1;
|
|
1792
|
-
continue;
|
|
1793
|
-
}
|
|
1794
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
1795
|
-
quote = char;
|
|
1796
|
-
current += char;
|
|
1797
|
-
continue;
|
|
1798
|
-
}
|
|
1799
|
-
if (char === "(" || char === "[" || char === "{") {
|
|
1800
|
-
depth += 1;
|
|
1801
|
-
}
|
|
1802
|
-
else if (char === ")" || char === "]" || char === "}") {
|
|
1803
|
-
depth = Math.max(0, depth - 1);
|
|
1804
|
-
}
|
|
1805
|
-
if (char === "," && depth === 0) {
|
|
1806
|
-
const entry = current.trim();
|
|
1807
|
-
if (entry)
|
|
1808
|
-
entries.push(entry);
|
|
1809
|
-
current = "";
|
|
1810
|
-
continue;
|
|
1811
|
-
}
|
|
1812
|
-
current += char;
|
|
1813
|
-
}
|
|
1814
|
-
const entry = current.trim();
|
|
1815
|
-
if (entry)
|
|
1816
|
-
entries.push(entry);
|
|
1817
|
-
return entries;
|
|
1818
|
-
}
|
|
1819
1728
|
function findProviderEntryIndex(entries, tokens) {
|
|
1820
1729
|
return entries.findIndex((entry) => tokens.some((token) => entry.includes(token)));
|
|
1821
1730
|
}
|
|
@@ -2713,35 +2622,65 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
2713
2622
|
const durableDrizzleTableRequirements = [
|
|
2714
2623
|
{
|
|
2715
2624
|
capability: "idempotency",
|
|
2716
|
-
|
|
2625
|
+
table: "idempotency",
|
|
2717
2626
|
code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
|
|
2718
|
-
|
|
2719
|
-
|
|
2627
|
+
factoryNames: [
|
|
2628
|
+
"createDrizzleSqliteIdempotencyPort",
|
|
2629
|
+
"createDrizzlePostgresIdempotencyPort",
|
|
2630
|
+
"createDrizzleMysqlIdempotencyPort",
|
|
2631
|
+
],
|
|
2632
|
+
setupNames: [
|
|
2633
|
+
"createDrizzleSqliteIdempotencySetupStatements",
|
|
2634
|
+
"createDrizzlePostgresIdempotencySetupStatements",
|
|
2635
|
+
"createDrizzleMysqlIdempotencySetupStatements",
|
|
2636
|
+
],
|
|
2637
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Idempotency(?:Port)?",
|
|
2638
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*IdempotencySetupStatements",
|
|
2720
2639
|
},
|
|
2721
2640
|
{
|
|
2722
2641
|
capability: "outbox",
|
|
2723
|
-
|
|
2642
|
+
table: "outbox",
|
|
2724
2643
|
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
2725
|
-
|
|
2726
|
-
|
|
2644
|
+
factoryNames: [
|
|
2645
|
+
"createDrizzleSqliteOutboxPort",
|
|
2646
|
+
"createDrizzlePostgresOutboxPort",
|
|
2647
|
+
"createDrizzleMysqlOutboxPort",
|
|
2648
|
+
],
|
|
2649
|
+
setupNames: [
|
|
2650
|
+
"createDrizzleSqliteOutboxSetupStatements",
|
|
2651
|
+
"createDrizzlePostgresOutboxSetupStatements",
|
|
2652
|
+
"createDrizzleMysqlOutboxSetupStatements",
|
|
2653
|
+
],
|
|
2654
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
|
|
2655
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
2727
2656
|
},
|
|
2728
2657
|
{
|
|
2729
2658
|
capability: "audit",
|
|
2730
|
-
|
|
2659
|
+
table: "audit",
|
|
2731
2660
|
code: "BEIGNET_AUDIT_TABLE_MISSING",
|
|
2732
|
-
|
|
2733
|
-
|
|
2661
|
+
factoryNames: [
|
|
2662
|
+
"createDrizzleSqliteAuditLogPort",
|
|
2663
|
+
"createDrizzlePostgresAuditLogPort",
|
|
2664
|
+
"createDrizzleMysqlAuditLogPort",
|
|
2665
|
+
],
|
|
2666
|
+
setupNames: [
|
|
2667
|
+
"createDrizzleSqliteAuditLogSetupStatements",
|
|
2668
|
+
"createDrizzlePostgresAuditLogSetupStatements",
|
|
2669
|
+
"createDrizzleMysqlAuditLogSetupStatements",
|
|
2670
|
+
],
|
|
2671
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*AuditLog(?:Port)?",
|
|
2672
|
+
explicitSetupPattern: "createDrizzle[A-Za-z]*AuditLogSetupStatements",
|
|
2734
2673
|
},
|
|
2735
2674
|
];
|
|
2736
2675
|
async function inspectDurableDrizzleTableDrift(targetDir, files, config, infrastructurePath, schemaDir, sourceCache) {
|
|
2737
2676
|
const diagnostics = [];
|
|
2738
2677
|
const usageFiles = durableDrizzleUsageFiles(files, config);
|
|
2739
2678
|
for (const requirement of durableDrizzleTableRequirements) {
|
|
2740
|
-
const tableNames = await configuredDurableDrizzleTableNames(targetDir, usageFiles, requirement, sourceCache);
|
|
2679
|
+
const tableNames = await configuredDurableDrizzleTableNames(targetDir, usageFiles, config, requirement, sourceCache);
|
|
2741
2680
|
if (tableNames.size === 0)
|
|
2742
2681
|
continue;
|
|
2743
2682
|
for (const tableName of tableNames) {
|
|
2744
|
-
if (await durableDrizzleTableExists(targetDir, files, infrastructurePath, schemaDir, requirement, tableName, sourceCache)) {
|
|
2683
|
+
if (await durableDrizzleTableExists(targetDir, files, config, infrastructurePath, schemaDir, requirement, tableName, sourceCache)) {
|
|
2745
2684
|
continue;
|
|
2746
2685
|
}
|
|
2747
2686
|
diagnostics.push({
|
|
@@ -2759,45 +2698,50 @@ function durableDrizzleUsageFiles(files, config) {
|
|
|
2759
2698
|
!isTestSourceFile(file) &&
|
|
2760
2699
|
isInfraOrServerFile(file, config));
|
|
2761
2700
|
}
|
|
2762
|
-
async function configuredDurableDrizzleTableNames(targetDir, files, requirement, sourceCache) {
|
|
2701
|
+
async function configuredDurableDrizzleTableNames(targetDir, files, config, requirement, sourceCache) {
|
|
2763
2702
|
const tableNames = new Set();
|
|
2703
|
+
const providerDefaultTableName = defaultBeignetConfig.database.tables[requirement.table];
|
|
2704
|
+
const configuredTableName = config.database.tables[requirement.table];
|
|
2764
2705
|
for (const file of files) {
|
|
2765
2706
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2766
|
-
for (const tableName of
|
|
2707
|
+
for (const tableName of tableNamesFromBeignetProviderCalls(source, requirement.factoryNames, providerDefaultTableName, configuredTableName)) {
|
|
2708
|
+
tableNames.add(tableName);
|
|
2709
|
+
}
|
|
2710
|
+
for (const tableName of tableNamesFromExplicitCalls(source, requirement.explicitFactoryPattern, configuredTableName)) {
|
|
2767
2711
|
tableNames.add(tableName);
|
|
2768
2712
|
}
|
|
2769
2713
|
}
|
|
2770
2714
|
return tableNames;
|
|
2771
2715
|
}
|
|
2772
|
-
async function durableDrizzleTableExists(targetDir, files, infrastructurePath, schemaDir, requirement, tableName, sourceCache) {
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
!isTestSourceFile(file));
|
|
2776
|
-
for (const file of schemaFiles) {
|
|
2777
|
-
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2778
|
-
if (source.includes(tableName))
|
|
2779
|
-
return true;
|
|
2716
|
+
async function durableDrizzleTableExists(targetDir, files, config, infrastructurePath, schemaDir, requirement, tableName, sourceCache) {
|
|
2717
|
+
if (await providerRequiredTableExists(targetDir, files, config, infrastructurePath, schemaDir, tableName, sourceCache)) {
|
|
2718
|
+
return true;
|
|
2780
2719
|
}
|
|
2781
2720
|
const setupFiles = files.filter((file) => !isTestSourceFile(file) &&
|
|
2782
2721
|
(file.startsWith("drizzle/") ||
|
|
2783
2722
|
file.startsWith(`${infrastructurePath}/db/`)) &&
|
|
2784
2723
|
/\.(?:ts|js|mts|mjs|cts|cjs|sql)$/.test(file));
|
|
2724
|
+
const providerDefaultTableName = defaultBeignetConfig.database.tables[requirement.table];
|
|
2725
|
+
const configuredTableName = config.database.tables[requirement.table];
|
|
2785
2726
|
for (const file of setupFiles) {
|
|
2786
2727
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2789
|
-
if (!requirement.setupPattern)
|
|
2790
|
-
continue;
|
|
2791
|
-
if (tableNamesFromCalls(source, requirement.setupPattern, requirement.defaultTableName).has(tableName)) {
|
|
2728
|
+
if (tableNamesFromBeignetProviderCalls(source, requirement.setupNames, providerDefaultTableName, configuredTableName).has(tableName) ||
|
|
2729
|
+
tableNamesFromExplicitCalls(source, requirement.explicitSetupPattern, configuredTableName).has(tableName)) {
|
|
2792
2730
|
return true;
|
|
2793
2731
|
}
|
|
2794
2732
|
}
|
|
2795
2733
|
return false;
|
|
2796
2734
|
}
|
|
2797
|
-
function
|
|
2798
|
-
|
|
2735
|
+
function tableNamesFromBeignetProviderCalls(source, exportNames, defaultTableName, configuredTableName) {
|
|
2736
|
+
const tableNames = new Set();
|
|
2737
|
+
for (const callee of beignetProviderCallees(source, exportNames)) {
|
|
2738
|
+
for (const tableName of tableNamesFromCallee(source, callee, defaultTableName, configuredTableName)) {
|
|
2739
|
+
tableNames.add(tableName);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return tableNames;
|
|
2799
2743
|
}
|
|
2800
|
-
function
|
|
2744
|
+
function tableNamesFromExplicitCalls(source, calleePattern, configuredTableName) {
|
|
2801
2745
|
const tableNames = new Set();
|
|
2802
2746
|
const callPattern = new RegExp(`\\b${calleePattern}\\s*\\(([\\s\\S]{0,800}?)\\)`, "g");
|
|
2803
2747
|
for (const match of source.matchAll(callPattern)) {
|
|
@@ -2805,12 +2749,71 @@ function tableNamesFromCalls(source, calleePattern, defaultTableName) {
|
|
|
2805
2749
|
const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
|
|
2806
2750
|
if (/\bfunction\s+$/.test(prefix))
|
|
2807
2751
|
continue;
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
|
|
2752
|
+
const tableName = tableNameFromArgs(match[1] ?? "", configuredTableName);
|
|
2753
|
+
if (tableName)
|
|
2754
|
+
tableNames.add(tableName);
|
|
2811
2755
|
}
|
|
2812
2756
|
return tableNames;
|
|
2813
2757
|
}
|
|
2758
|
+
function beignetProviderCallees(source, exportNames) {
|
|
2759
|
+
const exportNameSet = new Set(exportNames);
|
|
2760
|
+
const callees = [];
|
|
2761
|
+
for (const match of source.matchAll(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g)) {
|
|
2762
|
+
for (const specifier of match[1].split(",")) {
|
|
2763
|
+
const cleaned = specifier.trim().replace(/^type\s+/, "");
|
|
2764
|
+
if (!cleaned)
|
|
2765
|
+
continue;
|
|
2766
|
+
const [imported, local] = cleaned.split(/\s+as\s+/);
|
|
2767
|
+
if (!exportNameSet.has(imported))
|
|
2768
|
+
continue;
|
|
2769
|
+
callees.push({ kind: "identifier", name: local?.trim() || imported });
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
for (const match of source.matchAll(/\bimport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g)) {
|
|
2773
|
+
for (const exportName of exportNames) {
|
|
2774
|
+
callees.push({
|
|
2775
|
+
kind: "namespace",
|
|
2776
|
+
namespace: match[1],
|
|
2777
|
+
name: exportName,
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return callees;
|
|
2782
|
+
}
|
|
2783
|
+
function tableNamesFromCallee(source, callee, defaultTableName, configuredTableName) {
|
|
2784
|
+
const tableNames = new Set();
|
|
2785
|
+
const calleePattern = callee.kind === "identifier"
|
|
2786
|
+
? `\\b${escapeRegExp(callee.name)}`
|
|
2787
|
+
: `\\b${escapeRegExp(callee.namespace)}\\s*\\.\\s*${escapeRegExp(callee.name)}`;
|
|
2788
|
+
const callPattern = new RegExp(`${calleePattern}\\s*\\(([\\s\\S]{0,800}?)\\)`, "g");
|
|
2789
|
+
for (const match of source.matchAll(callPattern)) {
|
|
2790
|
+
const matchIndex = match.index ?? 0;
|
|
2791
|
+
const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
|
|
2792
|
+
if (/\bfunction\s+$/.test(prefix))
|
|
2793
|
+
continue;
|
|
2794
|
+
tableNames.add(tableNameFromArgs(match[1] ?? "", configuredTableName) ??
|
|
2795
|
+
defaultTableName);
|
|
2796
|
+
}
|
|
2797
|
+
return tableNames;
|
|
2798
|
+
}
|
|
2799
|
+
function tableNameFromArgs(argsSource, configuredTableName) {
|
|
2800
|
+
const literal = /\btableName\s*:\s*["'`]([^"'`]+)["'`]/.exec(argsSource)?.[1];
|
|
2801
|
+
if (literal)
|
|
2802
|
+
return literal;
|
|
2803
|
+
if (/\btableName\s*:/.test(argsSource))
|
|
2804
|
+
return configuredTableName;
|
|
2805
|
+
return undefined;
|
|
2806
|
+
}
|
|
2807
|
+
function configuredProviderRequiredTables(tableNames, config) {
|
|
2808
|
+
return tableNames.map((tableName) => configuredProviderRequiredTable(tableName, config));
|
|
2809
|
+
}
|
|
2810
|
+
function configuredProviderRequiredTable(tableName, config) {
|
|
2811
|
+
for (const [key, defaultTableName] of Object.entries(defaultBeignetConfig.database.tables)) {
|
|
2812
|
+
if (tableName === defaultTableName)
|
|
2813
|
+
return config.database.tables[key];
|
|
2814
|
+
}
|
|
2815
|
+
return tableName;
|
|
2816
|
+
}
|
|
2814
2817
|
function hasRepositoryAdapterFile(files, infrastructureDir) {
|
|
2815
2818
|
return files.some((file) => file.startsWith(infrastructureDir) &&
|
|
2816
2819
|
file.endsWith("-repository.ts") &&
|