@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +55 -6
  3. package/dist/config.d.ts +21 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +51 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/db.d.ts +30 -0
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +201 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +109 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspect.d.ts.map +1 -1
  15. package/dist/inspect.js +238 -235
  16. package/dist/inspect.js.map +1 -1
  17. package/dist/make.js +16 -15
  18. package/dist/make.js.map +1 -1
  19. package/dist/provider-audit.d.ts +131 -0
  20. package/dist/provider-audit.d.ts.map +1 -0
  21. package/dist/provider-audit.js +1013 -0
  22. package/dist/provider-audit.js.map +1 -0
  23. package/dist/templates/base.d.ts.map +1 -1
  24. package/dist/templates/base.js +3 -2
  25. package/dist/templates/base.js.map +1 -1
  26. package/dist/templates/db/mysql.js +2 -2
  27. package/dist/templates/db/postgres.js +2 -2
  28. package/dist/templates/db/sqlite.js +2 -2
  29. package/dist/templates/index.d.ts.map +1 -1
  30. package/dist/templates/index.js +1 -0
  31. package/dist/templates/index.js.map +1 -1
  32. package/dist/templates/server.d.ts +1 -0
  33. package/dist/templates/server.d.ts.map +1 -1
  34. package/dist/templates/server.js +90 -20
  35. package/dist/templates/server.js.map +1 -1
  36. package/dist/templates/todos.js +5 -5
  37. package/package.json +2 -2
  38. package/skills/app-structure/SKILL.md +22 -4
  39. package/src/config.ts +99 -0
  40. package/src/db.ts +316 -1
  41. package/src/index.ts +180 -1
  42. package/src/inspect.ts +412 -369
  43. package/src/make.ts +16 -15
  44. package/src/provider-audit.ts +1681 -0
  45. package/src/templates/base.ts +3 -2
  46. package/src/templates/db/mysql.ts +2 -2
  47. package/src/templates/db/postgres.ts +2 -2
  48. package/src/templates/db/sqlite.ts +2 -2
  49. package/src/templates/index.ts +1 -0
  50. package/src/templates/server.ts +90 -20
  51. 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 { fileURLToPath } from "node:url";
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 anyFileContains(targetDir, configFiles, envVar, sourceCache)) {
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 anyFileContains(targetDir, configFiles, envVar, sourceCache)) {
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.filter((file) => {
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
- defaultTableName: "idempotency_records",
2625
+ table: "idempotency",
2717
2626
  code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
2718
- factoryPattern: "createDrizzle[A-Za-z]+IdempotencyPort",
2719
- setupPattern: "createDrizzle[A-Za-z]+IdempotencySetupStatements",
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
- defaultTableName: "outbox_messages",
2642
+ table: "outbox",
2724
2643
  code: "BEIGNET_OUTBOX_TABLE_MISSING",
2725
- factoryPattern: "createDrizzle[A-Za-z]+OutboxPort",
2726
- setupPattern: "createDrizzle[A-Za-z]+OutboxSetupStatements",
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
- defaultTableName: "audit_log",
2659
+ table: "audit",
2731
2660
  code: "BEIGNET_AUDIT_TABLE_MISSING",
2732
- factoryPattern: "createDrizzle(?:[A-Za-z]+AuditLogPort|AuditLog)",
2733
- setupPattern: "createDrizzle[A-Za-z]+AuditLogSetupStatements",
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 tableNamesFromCalls(source, requirement.factoryPattern, requirement.defaultTableName)) {
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
- const schemaFiles = files.filter((file) => file.startsWith(`${schemaDir}/`) &&
2774
- /\.(?:ts|mts|cts)$/.test(file) &&
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 (sourceCreatesTable(source, tableName))
2788
- return true;
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 sourceCreatesTable(source, tableName) {
2798
- return new RegExp(`\\bCREATE\\s+TABLE\\b[\\s\\S]{0,200}${escapeRegExp(tableName)}\\b`, "i").test(source);
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 tableNamesFromCalls(source, calleePattern, defaultTableName) {
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 argsSource = match[1] ?? "";
2809
- const tableName = /\btableName\s*:\s*["'`]([^"'`]+)["'`]/.exec(argsSource)?.[1];
2810
- tableNames.add(tableName ?? defaultTableName);
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") &&