@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/src/inspect.ts CHANGED
@@ -1,16 +1,14 @@
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 {
5
- type ProviderPackageMetadata,
6
- parseProviderPackageMetadata,
7
- } from "@beignet/core/providers";
3
+ import { parseProviderPackageMetadata } from "@beignet/core/providers";
8
4
  import { createPainter } from "./ansi.js";
9
5
  import {
10
6
  type BeignetConfig,
7
+ type BeignetDatabaseTables,
11
8
  clientDirPath,
12
9
  clientFormsPath,
13
10
  clientIndexPath,
11
+ defaultBeignetConfig,
14
12
  directoryPath,
15
13
  loadBeignetConfig,
16
14
  normalizePath,
@@ -21,6 +19,21 @@ import {
21
19
  formatGithubAnnotation,
22
20
  type GithubAnnotationSeverity,
23
21
  } from "./github-annotations.js";
22
+ import {
23
+ detectedProviderVariants,
24
+ diagnosticPackageJsonFile,
25
+ formatProviderMetadataIssues,
26
+ installedPackageNames,
27
+ providerDoctorRulesForInstalledPackages,
28
+ providerOperationalConfigFiles,
29
+ providerRequiredEnvExists,
30
+ providerRequiredTableExists,
31
+ providerVariantRegistrationSeverity,
32
+ readProviderListEntries,
33
+ readProviderPackageMetadataSource,
34
+ registrationTokenHint,
35
+ serverProviderFiles,
36
+ } from "./provider-audit.js";
24
37
  import {
25
38
  type AppendResult,
26
39
  appendToArrayExpression,
@@ -363,10 +376,58 @@ export function formatDoctor(
363
376
  );
364
377
  }
365
378
 
379
+ const checklist = productionHardeningChecklist(result.diagnostics);
380
+ if (checklist.length > 0) {
381
+ lines.push("", paint("Production Hardening Checklist:", "bold"), "");
382
+ lines.push(...checklist.map((item) => `- ${item}`));
383
+ }
384
+
366
385
  lines.push("", formatDoctorFooter(result));
367
386
  return lines.join("\n");
368
387
  }
369
388
 
389
+ const productionHardeningDiagnosticCodes = new Set([
390
+ "BEIGNET_AUTH_ROUTE_MISSING",
391
+ "BEIGNET_AUTH_TRUSTED_ORIGINS_MISSING",
392
+ "BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
393
+ "BEIGNET_BILLING_CHECKOUT_IDEMPOTENCY_MISSING",
394
+ "BEIGNET_BILLING_ENTITLEMENTS_PROVIDER_MISSING",
395
+ "BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
396
+ "BEIGNET_CORS_CREDENTIALS_WILDCARD",
397
+ "BEIGNET_CRON_SECRET_MISSING",
398
+ "BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
399
+ "BEIGNET_ENTITLEMENTS_PORT_MISSING",
400
+ "BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
401
+ "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
402
+ "BEIGNET_PROVIDER_ENV_MISSING",
403
+ "BEIGNET_PROVIDER_TABLE_MISSING",
404
+ "BEIGNET_READINESS_ROUTE_MISSING",
405
+ "BEIGNET_TENANT_HEADER_AUTHORITY",
406
+ "BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
407
+ "BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
408
+ ]);
409
+
410
+ function productionHardeningChecklist(
411
+ diagnostics: InspectDiagnostic[],
412
+ ): string[] {
413
+ if (
414
+ !diagnostics.some((diagnostic) =>
415
+ productionHardeningDiagnosticCodes.has(diagnostic.code),
416
+ )
417
+ ) {
418
+ return [];
419
+ }
420
+
421
+ return [
422
+ "Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
423
+ "Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
424
+ "Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
425
+ "CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
426
+ "Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
427
+ "Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
428
+ ];
429
+ }
430
+
370
431
  function formatDoctorFooter(result: InspectAppResult): string {
371
432
  const count = (severity: InspectDiagnostic["severity"]) =>
372
433
  result.diagnostics.filter((diagnostic) => diagnostic.severity === severity)
@@ -1870,30 +1931,6 @@ async function inspectRouteErrorCatalogDrift(
1870
1931
  return diagnostics;
1871
1932
  }
1872
1933
 
1873
- type ProviderDoctorVariantRule = {
1874
- name: string;
1875
- displayName: string;
1876
- tokens: readonly string[];
1877
- requiredEnv: readonly string[];
1878
- registrationSeverity?: "warning" | "hint";
1879
- };
1880
-
1881
- type ProviderDoctorRule = {
1882
- packageName: string;
1883
- displayName: string;
1884
- tokens: readonly string[];
1885
- appPorts?: readonly { name: string; type: string }[];
1886
- requiredEnv?: readonly string[];
1887
- registrationSeverity?: "warning" | "hint";
1888
- variants?: readonly ProviderDoctorVariantRule[];
1889
- };
1890
-
1891
- type ProviderPackageMetadataSource = {
1892
- packageName: string;
1893
- file: string;
1894
- metadata: unknown;
1895
- };
1896
-
1897
1934
  const readinessRelevantProviderPackages = new Set([
1898
1935
  "@beignet/provider-auth-better-auth",
1899
1936
  "@beignet/provider-db-drizzle",
@@ -1928,6 +1965,10 @@ async function inspectProductionReadiness(
1928
1965
  packageJson,
1929
1966
  );
1930
1967
  const configFiles = operationalConfigFiles(files, config);
1968
+ const infrastructurePath = directoryPath(
1969
+ path.dirname(config.paths.infrastructurePorts),
1970
+ );
1971
+ const schemaDir = `${infrastructurePath}/db/schema`;
1931
1972
 
1932
1973
  for (const file of files) {
1933
1974
  if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
@@ -1974,6 +2015,32 @@ async function inspectProductionReadiness(
1974
2015
  });
1975
2016
  }
1976
2017
 
2018
+ if (
2019
+ isFeatureUploadDefinition(file, config) &&
2020
+ containsCallExpression(source, "defineUpload") &&
2021
+ !/\bauthorize\s*(?:\(|:)/.test(source) &&
2022
+ !/\baccess\s*:\s*["']public["']/.test(source)
2023
+ ) {
2024
+ diagnostics.push({
2025
+ severity: "warning",
2026
+ code: "BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
2027
+ file,
2028
+ message: `${file} defines a protected upload without authorize(...). Add an authorize hook, or set access: "public" only for intentionally public uploads.`,
2029
+ });
2030
+ }
2031
+
2032
+ if (
2033
+ isRouteServerOrInfraFile(file, config) &&
2034
+ containsRequestTenantHeaderAuthority(source)
2035
+ ) {
2036
+ diagnostics.push({
2037
+ severity: "warning",
2038
+ code: "BEIGNET_TENANT_HEADER_AUTHORITY",
2039
+ file,
2040
+ 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.`,
2041
+ });
2042
+ }
2043
+
1977
2044
  if (
1978
2045
  isRouteServerOrInfraFile(file, config) &&
1979
2046
  containsCallExpression(source, "createCorsHooks") &&
@@ -1991,6 +2058,19 @@ async function inspectProductionReadiness(
1991
2058
  }
1992
2059
  }
1993
2060
 
2061
+ for (const file of configFiles) {
2062
+ const source = await readCachedSource(targetDir, file, sourceCache);
2063
+
2064
+ if (containsBetterAuthLocalSecret(source)) {
2065
+ diagnostics.push({
2066
+ severity: "warning",
2067
+ code: "BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
2068
+ file,
2069
+ 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.`,
2070
+ });
2071
+ }
2072
+ }
2073
+
1994
2074
  const providerEntries = await readProviderListEntries(
1995
2075
  targetDir,
1996
2076
  files,
@@ -2034,7 +2114,12 @@ async function inspectProductionReadiness(
2034
2114
  )) {
2035
2115
  for (const envVar of variant.requiredEnv) {
2036
2116
  if (
2037
- await anyFileContains(targetDir, configFiles, envVar, sourceCache)
2117
+ await providerRequiredEnvExists(
2118
+ targetDir,
2119
+ configFiles,
2120
+ envVar,
2121
+ sourceCache,
2122
+ )
2038
2123
  ) {
2039
2124
  continue;
2040
2125
  }
@@ -2045,12 +2130,44 @@ async function inspectProductionReadiness(
2045
2130
  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.`,
2046
2131
  });
2047
2132
  }
2133
+
2134
+ for (const tableName of configuredProviderRequiredTables(
2135
+ variant.requiredTables,
2136
+ config,
2137
+ )) {
2138
+ if (
2139
+ await providerRequiredTableExists(
2140
+ targetDir,
2141
+ files,
2142
+ config,
2143
+ infrastructurePath,
2144
+ schemaDir,
2145
+ tableName,
2146
+ sourceCache,
2147
+ )
2148
+ ) {
2149
+ continue;
2150
+ }
2151
+ diagnostics.push({
2152
+ severity: "warning",
2153
+ code: "BEIGNET_PROVIDER_TABLE_MISSING",
2154
+ file: schemaDir,
2155
+ 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.`,
2156
+ });
2157
+ }
2048
2158
  }
2049
2159
  continue;
2050
2160
  }
2051
2161
 
2052
2162
  for (const envVar of provider.requiredEnv ?? []) {
2053
- if (await anyFileContains(targetDir, configFiles, envVar, sourceCache)) {
2163
+ if (
2164
+ await providerRequiredEnvExists(
2165
+ targetDir,
2166
+ configFiles,
2167
+ envVar,
2168
+ sourceCache,
2169
+ )
2170
+ ) {
2054
2171
  continue;
2055
2172
  }
2056
2173
  diagnostics.push({
@@ -2060,6 +2177,31 @@ async function inspectProductionReadiness(
2060
2177
  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.`,
2061
2178
  });
2062
2179
  }
2180
+
2181
+ for (const tableName of configuredProviderRequiredTables(
2182
+ provider.requiredTables ?? [],
2183
+ config,
2184
+ )) {
2185
+ if (
2186
+ await providerRequiredTableExists(
2187
+ targetDir,
2188
+ files,
2189
+ config,
2190
+ infrastructurePath,
2191
+ schemaDir,
2192
+ tableName,
2193
+ sourceCache,
2194
+ )
2195
+ ) {
2196
+ continue;
2197
+ }
2198
+ diagnostics.push({
2199
+ severity: "warning",
2200
+ code: "BEIGNET_PROVIDER_TABLE_MISSING",
2201
+ file: schemaDir,
2202
+ 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.`,
2203
+ });
2204
+ }
2063
2205
  }
2064
2206
 
2065
2207
  if (installedPackages.has("@beignet/provider-auth-better-auth")) {
@@ -2138,6 +2280,34 @@ function containsReadinessCheck(source: string): boolean {
2138
2280
  return /\bchecks\s*:/.test(source) || /\.checkHealth\s*\(/.test(source);
2139
2281
  }
2140
2282
 
2283
+ function containsBetterAuthLocalSecret(source: string): boolean {
2284
+ if (!source.includes("local-dev-better-auth-secret-change-me")) return false;
2285
+
2286
+ const hasProductionRuntimeGuard =
2287
+ /\bisProductionRuntime\s*=\s*process\.env\.NODE_ENV\s*===\s*["']production["']\s*&&\s*process\.env\.NEXT_PHASE\s*!==\s*["']phase-production-build["']/.test(
2288
+ source,
2289
+ ) &&
2290
+ /BETTER_AUTH_SECRET\s*:\s*isProductionRuntime\s*\?\s*BetterAuthSecret\s*:\s*BetterAuthSecret\.default\(LOCAL_BETTER_AUTH_SECRET\)/s.test(
2291
+ source,
2292
+ );
2293
+ const hasProductionGuard =
2294
+ /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(
2295
+ source,
2296
+ );
2297
+
2298
+ return !(hasProductionRuntimeGuard || hasProductionGuard);
2299
+ }
2300
+
2301
+ function containsRequestTenantHeaderAuthority(source: string): boolean {
2302
+ return (
2303
+ /(?:(?:\breq|\brequest)\.headers|(?<!\.)\bheaders)\.get\(\s*["']x-tenant-id["']\s*\)/.test(
2304
+ source,
2305
+ ) &&
2306
+ (/\bcreateTenant\s*\(/.test(source) ||
2307
+ /\btenant\s*:\s*\{?\s*id\s*:/.test(source))
2308
+ );
2309
+ }
2310
+
2141
2311
  async function inspectEntitlementsProductionReadiness(
2142
2312
  targetDir: string,
2143
2313
  files: string[],
@@ -2571,13 +2741,7 @@ function operationalConfigFiles(
2571
2741
  files: string[],
2572
2742
  config: ResolvedBeignetConfig,
2573
2743
  ): string[] {
2574
- return files.filter((file) => {
2575
- if (file === ".env.example") return true;
2576
- if (file === "drizzle.config.ts") return true;
2577
- if (file === "lib/env.ts" || file === "lib/better-auth.ts") return true;
2578
- if (isInfraOrServerFile(file, config) && file.endsWith(".ts")) return true;
2579
- return false;
2580
- });
2744
+ return providerOperationalConfigFiles(files, config);
2581
2745
  }
2582
2746
 
2583
2747
  async function readPackageJson(
@@ -2598,83 +2762,6 @@ async function readPackageJson(
2598
2762
  );
2599
2763
  }
2600
2764
 
2601
- async function providerDoctorRulesForInstalledPackages(
2602
- targetDir: string,
2603
- packageJson:
2604
- | {
2605
- dependencies?: Record<string, string>;
2606
- devDependencies?: Record<string, string>;
2607
- peerDependencies?: Record<string, string>;
2608
- }
2609
- | undefined,
2610
- ): Promise<ProviderDoctorRule[]> {
2611
- const packageNames = [...installedPackageNames(packageJson)];
2612
- const rules = await Promise.all(
2613
- packageNames.map(async (packageName) => {
2614
- const source = await readProviderPackageMetadataSource(
2615
- targetDir,
2616
- packageName,
2617
- );
2618
- if (!source) return undefined;
2619
- const result = parseProviderPackageMetadata(source.metadata);
2620
- if (!result.success) return undefined;
2621
- return providerDoctorRuleFromMetadata(packageName, result.metadata);
2622
- }),
2623
- );
2624
-
2625
- return rules.filter((rule): rule is ProviderDoctorRule => Boolean(rule));
2626
- }
2627
-
2628
- async function readProviderPackageMetadataSource(
2629
- targetDir: string,
2630
- packageName: string,
2631
- ): Promise<ProviderPackageMetadataSource | undefined> {
2632
- for (const candidate of providerPackageJsonCandidates(
2633
- targetDir,
2634
- packageName,
2635
- )) {
2636
- const packageJson = await readJsonIfExists<{
2637
- beignet?: { provider?: unknown };
2638
- }>(candidate);
2639
- const metadata = packageJson?.beignet?.provider;
2640
- if (metadata !== undefined) {
2641
- return { packageName, file: candidate, metadata };
2642
- }
2643
- }
2644
-
2645
- return undefined;
2646
- }
2647
-
2648
- function providerPackageJsonCandidates(
2649
- targetDir: string,
2650
- packageName: string,
2651
- ): string[] {
2652
- const candidates = [
2653
- path.join(
2654
- targetDir,
2655
- "node_modules",
2656
- ...packageName.split("/"),
2657
- "package.json",
2658
- ),
2659
- ];
2660
-
2661
- if (packageName.startsWith("@beignet/")) {
2662
- candidates.push(
2663
- path.resolve(
2664
- // import.meta.dir is Bun-only; the published bin also runs under
2665
- // plain Node, where only import.meta.url is available.
2666
- path.dirname(fileURLToPath(import.meta.url)),
2667
- "../../..",
2668
- "packages",
2669
- packageName.slice("@beignet/".length),
2670
- "package.json",
2671
- ),
2672
- );
2673
- }
2674
-
2675
- return candidates;
2676
- }
2677
-
2678
2765
  async function readJsonIfExists<T>(file: string): Promise<T | undefined> {
2679
2766
  try {
2680
2767
  return JSON.parse(await readFile(file, "utf8")) as T;
@@ -2690,218 +2777,6 @@ async function readJsonIfExists<T>(file: string): Promise<T | undefined> {
2690
2777
  }
2691
2778
  }
2692
2779
 
2693
- function providerDoctorRuleFromMetadata(
2694
- packageName: string,
2695
- metadata: ProviderPackageMetadata,
2696
- ): ProviderDoctorRule {
2697
- const registration = metadata.registration;
2698
-
2699
- return {
2700
- packageName,
2701
- displayName: metadata.displayName ?? packageName,
2702
- tokens: registration?.tokens ?? [],
2703
- appPorts: metadata.appPorts ?? [],
2704
- requiredEnv: metadata.requiredEnv ?? [],
2705
- registrationSeverity: registrationSeverityFromMetadata(registration),
2706
- variants: metadata.variants?.map((variant) => ({
2707
- name: variant.name,
2708
- displayName: variant.displayName ?? variant.name,
2709
- tokens: variant.registration?.tokens ?? [],
2710
- requiredEnv: variant.requiredEnv ?? [],
2711
- registrationSeverity: registrationSeverityFromMetadata(
2712
- variant.registration,
2713
- ),
2714
- })),
2715
- };
2716
- }
2717
-
2718
- function registrationSeverityFromMetadata(
2719
- registration:
2720
- | { required?: boolean; severity?: "warning" | "hint" }
2721
- | undefined,
2722
- ): "warning" | "hint" | undefined {
2723
- return (
2724
- registration?.severity ??
2725
- (registration?.required === true ? "warning" : undefined)
2726
- );
2727
- }
2728
-
2729
- function detectedProviderVariants(
2730
- variants: readonly ProviderDoctorVariantRule[],
2731
- providerEntries: readonly string[],
2732
- ): ProviderDoctorVariantRule[] {
2733
- return variants.filter((variant) =>
2734
- variant.tokens.some((token) =>
2735
- providerEntries.some((entry) => entry.includes(token)),
2736
- ),
2737
- );
2738
- }
2739
-
2740
- function providerVariantRegistrationSeverity(
2741
- variants: readonly ProviderDoctorVariantRule[],
2742
- ): "warning" | "hint" | undefined {
2743
- const severities = variants.map((variant) => variant.registrationSeverity);
2744
- if (severities.includes("warning")) return "warning";
2745
- if (severities.includes("hint")) return "hint";
2746
- return undefined;
2747
- }
2748
-
2749
- async function readProviderListEntries(
2750
- targetDir: string,
2751
- files: string[],
2752
- config: ResolvedBeignetConfig,
2753
- sourceCache: Map<string, string>,
2754
- ): Promise<string[]> {
2755
- const providerFiles = serverProviderFiles(files, config);
2756
- const providerSource = (
2757
- await Promise.all(
2758
- providerFiles.map((file) =>
2759
- readCachedSource(targetDir, file, sourceCache),
2760
- ),
2761
- )
2762
- ).join("\n");
2763
- const providerListSource = extractProviderListSource(providerSource);
2764
- return providerListSource === undefined
2765
- ? []
2766
- : providerListEntries(providerListSource);
2767
- }
2768
-
2769
- function registrationTokenHint(tokens: readonly string[]): string {
2770
- const token = tokens[0];
2771
- if (!token) return "the provider";
2772
- return token.startsWith("create") ? `${token}()` : token;
2773
- }
2774
-
2775
- function diagnosticPackageJsonFile(targetDir: string, file: string): string {
2776
- const relative = normalizePath(path.relative(targetDir, file));
2777
- if (relative.startsWith("../")) return "package.json";
2778
- return relative;
2779
- }
2780
-
2781
- function formatProviderMetadataIssues(
2782
- issues: readonly { path: string; message: string }[],
2783
- ): string {
2784
- return issues.map((issue) => `${issue.path} ${issue.message}`).join("; ");
2785
- }
2786
-
2787
- function installedPackageNames(
2788
- packageJson:
2789
- | {
2790
- dependencies?: Record<string, string>;
2791
- devDependencies?: Record<string, string>;
2792
- peerDependencies?: Record<string, string>;
2793
- }
2794
- | undefined,
2795
- ): Set<string> {
2796
- return new Set([
2797
- ...Object.keys(packageJson?.dependencies ?? {}),
2798
- ...Object.keys(packageJson?.devDependencies ?? {}),
2799
- ...Object.keys(packageJson?.peerDependencies ?? {}),
2800
- ]);
2801
- }
2802
-
2803
- function serverProviderFiles(
2804
- files: string[],
2805
- config: ResolvedBeignetConfig,
2806
- ): string[] {
2807
- const serverDir = directoryPath(path.dirname(config.paths.server));
2808
- const candidates = [`${serverDir}/providers.ts`, config.paths.server];
2809
- return candidates.filter((file) => files.includes(file));
2810
- }
2811
-
2812
- function extractProviderListSource(source: string): string | undefined {
2813
- const listMatches = [
2814
- ...source.matchAll(/\bproviders\s*=\s*\[([\s\S]*?)\]\s*(?:;|as const)/g),
2815
- ...source.matchAll(/\bproviders\s*:\s*\[([\s\S]*?)\]/g),
2816
- ];
2817
- if (listMatches.length === 0) return undefined;
2818
- return listMatches.map((match) => match[1]).join("\n");
2819
- }
2820
-
2821
- function providerListEntries(source: string): string[] {
2822
- const entries: string[] = [];
2823
- let current = "";
2824
- let depth = 0;
2825
- let quote: '"' | "'" | "`" | undefined;
2826
- let escaped = false;
2827
- let lineComment = false;
2828
- let blockComment = false;
2829
-
2830
- for (let index = 0; index < source.length; index += 1) {
2831
- const char = source[index];
2832
- const next = source[index + 1];
2833
-
2834
- if (lineComment) {
2835
- if (char === "\n") {
2836
- lineComment = false;
2837
- current += char;
2838
- }
2839
- continue;
2840
- }
2841
-
2842
- if (blockComment) {
2843
- if (char === "*" && next === "/") {
2844
- blockComment = false;
2845
- index += 1;
2846
- }
2847
- continue;
2848
- }
2849
-
2850
- if (quote) {
2851
- current += char;
2852
- if (escaped) {
2853
- escaped = false;
2854
- continue;
2855
- }
2856
- if (char === "\\") {
2857
- escaped = true;
2858
- continue;
2859
- }
2860
- if (char === quote) {
2861
- quote = undefined;
2862
- }
2863
- continue;
2864
- }
2865
-
2866
- if (char === "/" && next === "/") {
2867
- lineComment = true;
2868
- index += 1;
2869
- continue;
2870
- }
2871
-
2872
- if (char === "/" && next === "*") {
2873
- blockComment = true;
2874
- index += 1;
2875
- continue;
2876
- }
2877
-
2878
- if (char === '"' || char === "'" || char === "`") {
2879
- quote = char;
2880
- current += char;
2881
- continue;
2882
- }
2883
-
2884
- if (char === "(" || char === "[" || char === "{") {
2885
- depth += 1;
2886
- } else if (char === ")" || char === "]" || char === "}") {
2887
- depth = Math.max(0, depth - 1);
2888
- }
2889
-
2890
- if (char === "," && depth === 0) {
2891
- const entry = current.trim();
2892
- if (entry) entries.push(entry);
2893
- current = "";
2894
- continue;
2895
- }
2896
-
2897
- current += char;
2898
- }
2899
-
2900
- const entry = current.trim();
2901
- if (entry) entries.push(entry);
2902
- return entries;
2903
- }
2904
-
2905
2780
  function findProviderEntryIndex(
2906
2781
  entries: string[],
2907
2782
  tokens: readonly string[],
@@ -4319,34 +4194,66 @@ async function inspectDatabaseLifecycleDrift(
4319
4194
 
4320
4195
  type DurableDrizzleTableRequirement = {
4321
4196
  capability: string;
4322
- defaultTableName: string;
4197
+ table: keyof BeignetDatabaseTables;
4323
4198
  code: string;
4324
- factoryPattern: string;
4325
- setupPattern?: string;
4199
+ factoryNames: readonly string[];
4200
+ setupNames: readonly string[];
4201
+ explicitFactoryPattern: string;
4202
+ explicitSetupPattern: string;
4326
4203
  };
4327
4204
 
4328
4205
  const durableDrizzleTableRequirements: readonly DurableDrizzleTableRequirement[] =
4329
4206
  [
4330
4207
  {
4331
4208
  capability: "idempotency",
4332
- defaultTableName: "idempotency_records",
4209
+ table: "idempotency",
4333
4210
  code: "BEIGNET_IDEMPOTENCY_TABLE_MISSING",
4334
- factoryPattern: "createDrizzle[A-Za-z]+IdempotencyPort",
4335
- setupPattern: "createDrizzle[A-Za-z]+IdempotencySetupStatements",
4211
+ factoryNames: [
4212
+ "createDrizzleSqliteIdempotencyPort",
4213
+ "createDrizzlePostgresIdempotencyPort",
4214
+ "createDrizzleMysqlIdempotencyPort",
4215
+ ],
4216
+ setupNames: [
4217
+ "createDrizzleSqliteIdempotencySetupStatements",
4218
+ "createDrizzlePostgresIdempotencySetupStatements",
4219
+ "createDrizzleMysqlIdempotencySetupStatements",
4220
+ ],
4221
+ explicitFactoryPattern: "createDrizzle[A-Za-z]*Idempotency(?:Port)?",
4222
+ explicitSetupPattern: "createDrizzle[A-Za-z]*IdempotencySetupStatements",
4336
4223
  },
4337
4224
  {
4338
4225
  capability: "outbox",
4339
- defaultTableName: "outbox_messages",
4226
+ table: "outbox",
4340
4227
  code: "BEIGNET_OUTBOX_TABLE_MISSING",
4341
- factoryPattern: "createDrizzle[A-Za-z]+OutboxPort",
4342
- setupPattern: "createDrizzle[A-Za-z]+OutboxSetupStatements",
4228
+ factoryNames: [
4229
+ "createDrizzleSqliteOutboxPort",
4230
+ "createDrizzlePostgresOutboxPort",
4231
+ "createDrizzleMysqlOutboxPort",
4232
+ ],
4233
+ setupNames: [
4234
+ "createDrizzleSqliteOutboxSetupStatements",
4235
+ "createDrizzlePostgresOutboxSetupStatements",
4236
+ "createDrizzleMysqlOutboxSetupStatements",
4237
+ ],
4238
+ explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
4239
+ explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
4343
4240
  },
4344
4241
  {
4345
4242
  capability: "audit",
4346
- defaultTableName: "audit_log",
4243
+ table: "audit",
4347
4244
  code: "BEIGNET_AUDIT_TABLE_MISSING",
4348
- factoryPattern: "createDrizzle(?:[A-Za-z]+AuditLogPort|AuditLog)",
4349
- setupPattern: "createDrizzle[A-Za-z]+AuditLogSetupStatements",
4245
+ factoryNames: [
4246
+ "createDrizzleSqliteAuditLogPort",
4247
+ "createDrizzlePostgresAuditLogPort",
4248
+ "createDrizzleMysqlAuditLogPort",
4249
+ ],
4250
+ setupNames: [
4251
+ "createDrizzleSqliteAuditLogSetupStatements",
4252
+ "createDrizzlePostgresAuditLogSetupStatements",
4253
+ "createDrizzleMysqlAuditLogSetupStatements",
4254
+ ],
4255
+ explicitFactoryPattern: "createDrizzle[A-Za-z]*AuditLog(?:Port)?",
4256
+ explicitSetupPattern: "createDrizzle[A-Za-z]*AuditLogSetupStatements",
4350
4257
  },
4351
4258
  ];
4352
4259
 
@@ -4365,6 +4272,7 @@ async function inspectDurableDrizzleTableDrift(
4365
4272
  const tableNames = await configuredDurableDrizzleTableNames(
4366
4273
  targetDir,
4367
4274
  usageFiles,
4275
+ config,
4368
4276
  requirement,
4369
4277
  sourceCache,
4370
4278
  );
@@ -4375,6 +4283,7 @@ async function inspectDurableDrizzleTableDrift(
4375
4283
  await durableDrizzleTableExists(
4376
4284
  targetDir,
4377
4285
  files,
4286
+ config,
4378
4287
  infrastructurePath,
4379
4288
  schemaDir,
4380
4289
  requirement,
@@ -4412,17 +4321,29 @@ function durableDrizzleUsageFiles(
4412
4321
  async function configuredDurableDrizzleTableNames(
4413
4322
  targetDir: string,
4414
4323
  files: string[],
4324
+ config: ResolvedBeignetConfig,
4415
4325
  requirement: DurableDrizzleTableRequirement,
4416
4326
  sourceCache: Map<string, string>,
4417
4327
  ): Promise<Set<string>> {
4418
4328
  const tableNames = new Set<string>();
4329
+ const providerDefaultTableName =
4330
+ defaultBeignetConfig.database.tables[requirement.table];
4331
+ const configuredTableName = config.database.tables[requirement.table];
4419
4332
 
4420
4333
  for (const file of files) {
4421
4334
  const source = await readCachedSource(targetDir, file, sourceCache);
4422
- for (const tableName of tableNamesFromCalls(
4335
+ for (const tableName of tableNamesFromBeignetProviderCalls(
4336
+ source,
4337
+ requirement.factoryNames,
4338
+ providerDefaultTableName,
4339
+ configuredTableName,
4340
+ )) {
4341
+ tableNames.add(tableName);
4342
+ }
4343
+ for (const tableName of tableNamesFromExplicitCalls(
4423
4344
  source,
4424
- requirement.factoryPattern,
4425
- requirement.defaultTableName,
4345
+ requirement.explicitFactoryPattern,
4346
+ configuredTableName,
4426
4347
  )) {
4427
4348
  tableNames.add(tableName);
4428
4349
  }
@@ -4434,21 +4355,25 @@ async function configuredDurableDrizzleTableNames(
4434
4355
  async function durableDrizzleTableExists(
4435
4356
  targetDir: string,
4436
4357
  files: string[],
4358
+ config: ResolvedBeignetConfig,
4437
4359
  infrastructurePath: string,
4438
4360
  schemaDir: string,
4439
4361
  requirement: DurableDrizzleTableRequirement,
4440
4362
  tableName: string,
4441
4363
  sourceCache: Map<string, string>,
4442
4364
  ): Promise<boolean> {
4443
- const schemaFiles = files.filter(
4444
- (file) =>
4445
- file.startsWith(`${schemaDir}/`) &&
4446
- /\.(?:ts|mts|cts)$/.test(file) &&
4447
- !isTestSourceFile(file),
4448
- );
4449
- for (const file of schemaFiles) {
4450
- const source = await readCachedSource(targetDir, file, sourceCache);
4451
- if (source.includes(tableName)) return true;
4365
+ if (
4366
+ await providerRequiredTableExists(
4367
+ targetDir,
4368
+ files,
4369
+ config,
4370
+ infrastructurePath,
4371
+ schemaDir,
4372
+ tableName,
4373
+ sourceCache,
4374
+ )
4375
+ ) {
4376
+ return true;
4452
4377
  }
4453
4378
 
4454
4379
  const setupFiles = files.filter(
@@ -4458,16 +4383,22 @@ async function durableDrizzleTableExists(
4458
4383
  file.startsWith(`${infrastructurePath}/db/`)) &&
4459
4384
  /\.(?:ts|js|mts|mjs|cts|cjs|sql)$/.test(file),
4460
4385
  );
4386
+ const providerDefaultTableName =
4387
+ defaultBeignetConfig.database.tables[requirement.table];
4388
+ const configuredTableName = config.database.tables[requirement.table];
4461
4389
  for (const file of setupFiles) {
4462
4390
  const source = await readCachedSource(targetDir, file, sourceCache);
4463
- if (sourceCreatesTable(source, tableName)) return true;
4464
-
4465
- if (!requirement.setupPattern) continue;
4466
4391
  if (
4467
- tableNamesFromCalls(
4392
+ tableNamesFromBeignetProviderCalls(
4468
4393
  source,
4469
- requirement.setupPattern,
4470
- requirement.defaultTableName,
4394
+ requirement.setupNames,
4395
+ providerDefaultTableName,
4396
+ configuredTableName,
4397
+ ).has(tableName) ||
4398
+ tableNamesFromExplicitCalls(
4399
+ source,
4400
+ requirement.explicitSetupPattern,
4401
+ configuredTableName,
4471
4402
  ).has(tableName)
4472
4403
  ) {
4473
4404
  return true;
@@ -4477,17 +4408,30 @@ async function durableDrizzleTableExists(
4477
4408
  return false;
4478
4409
  }
4479
4410
 
4480
- function sourceCreatesTable(source: string, tableName: string): boolean {
4481
- return new RegExp(
4482
- `\\bCREATE\\s+TABLE\\b[\\s\\S]{0,200}${escapeRegExp(tableName)}\\b`,
4483
- "i",
4484
- ).test(source);
4411
+ function tableNamesFromBeignetProviderCalls(
4412
+ source: string,
4413
+ exportNames: readonly string[],
4414
+ defaultTableName: string,
4415
+ configuredTableName: string,
4416
+ ): Set<string> {
4417
+ const tableNames = new Set<string>();
4418
+ for (const callee of beignetProviderCallees(source, exportNames)) {
4419
+ for (const tableName of tableNamesFromCallee(
4420
+ source,
4421
+ callee,
4422
+ defaultTableName,
4423
+ configuredTableName,
4424
+ )) {
4425
+ tableNames.add(tableName);
4426
+ }
4427
+ }
4428
+ return tableNames;
4485
4429
  }
4486
4430
 
4487
- function tableNamesFromCalls(
4431
+ function tableNamesFromExplicitCalls(
4488
4432
  source: string,
4489
4433
  calleePattern: string,
4490
- defaultTableName: string,
4434
+ configuredTableName: string,
4491
4435
  ): Set<string> {
4492
4436
  const tableNames = new Set<string>();
4493
4437
  const callPattern = new RegExp(
@@ -4500,16 +4444,115 @@ function tableNamesFromCalls(
4500
4444
  const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
4501
4445
  if (/\bfunction\s+$/.test(prefix)) continue;
4502
4446
 
4503
- const argsSource = match[1] ?? "";
4504
- const tableName = /\btableName\s*:\s*["'`]([^"'`]+)["'`]/.exec(
4505
- argsSource,
4506
- )?.[1];
4507
- tableNames.add(tableName ?? defaultTableName);
4447
+ const tableName = tableNameFromArgs(match[1] ?? "", configuredTableName);
4448
+ if (tableName) tableNames.add(tableName);
4449
+ }
4450
+
4451
+ return tableNames;
4452
+ }
4453
+
4454
+ type BeignetProviderCallee =
4455
+ | { kind: "identifier"; name: string }
4456
+ | { kind: "namespace"; namespace: string; name: string };
4457
+
4458
+ function beignetProviderCallees(
4459
+ source: string,
4460
+ exportNames: readonly string[],
4461
+ ): BeignetProviderCallee[] {
4462
+ const exportNameSet = new Set(exportNames);
4463
+ const callees: BeignetProviderCallee[] = [];
4464
+
4465
+ for (const match of source.matchAll(
4466
+ /\bimport\s+(?:type\s+)?\{([^}]*)\}\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g,
4467
+ )) {
4468
+ for (const specifier of match[1].split(",")) {
4469
+ const cleaned = specifier.trim().replace(/^type\s+/, "");
4470
+ if (!cleaned) continue;
4471
+ const [imported, local] = cleaned.split(/\s+as\s+/);
4472
+ if (!exportNameSet.has(imported)) continue;
4473
+ callees.push({ kind: "identifier", name: local?.trim() || imported });
4474
+ }
4475
+ }
4476
+
4477
+ for (const match of source.matchAll(
4478
+ /\bimport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/provider-db-drizzle\/(?:sqlite|postgres|mysql)["']/g,
4479
+ )) {
4480
+ for (const exportName of exportNames) {
4481
+ callees.push({
4482
+ kind: "namespace",
4483
+ namespace: match[1],
4484
+ name: exportName,
4485
+ });
4486
+ }
4487
+ }
4488
+
4489
+ return callees;
4490
+ }
4491
+
4492
+ function tableNamesFromCallee(
4493
+ source: string,
4494
+ callee: BeignetProviderCallee,
4495
+ defaultTableName: string,
4496
+ configuredTableName: string,
4497
+ ): Set<string> {
4498
+ const tableNames = new Set<string>();
4499
+ const calleePattern =
4500
+ callee.kind === "identifier"
4501
+ ? `\\b${escapeRegExp(callee.name)}`
4502
+ : `\\b${escapeRegExp(callee.namespace)}\\s*\\.\\s*${escapeRegExp(
4503
+ callee.name,
4504
+ )}`;
4505
+ const callPattern = new RegExp(
4506
+ `${calleePattern}\\s*\\(([\\s\\S]{0,800}?)\\)`,
4507
+ "g",
4508
+ );
4509
+
4510
+ for (const match of source.matchAll(callPattern)) {
4511
+ const matchIndex = match.index ?? 0;
4512
+ const prefix = source.slice(Math.max(0, matchIndex - 32), matchIndex);
4513
+ if (/\bfunction\s+$/.test(prefix)) continue;
4514
+
4515
+ tableNames.add(
4516
+ tableNameFromArgs(match[1] ?? "", configuredTableName) ??
4517
+ defaultTableName,
4518
+ );
4508
4519
  }
4509
4520
 
4510
4521
  return tableNames;
4511
4522
  }
4512
4523
 
4524
+ function tableNameFromArgs(
4525
+ argsSource: string,
4526
+ configuredTableName: string,
4527
+ ): string | undefined {
4528
+ const literal = /\btableName\s*:\s*["'`]([^"'`]+)["'`]/.exec(argsSource)?.[1];
4529
+ if (literal) return literal;
4530
+ if (/\btableName\s*:/.test(argsSource)) return configuredTableName;
4531
+ return undefined;
4532
+ }
4533
+
4534
+ function configuredProviderRequiredTables(
4535
+ tableNames: readonly string[],
4536
+ config: ResolvedBeignetConfig,
4537
+ ): string[] {
4538
+ return tableNames.map((tableName) =>
4539
+ configuredProviderRequiredTable(tableName, config),
4540
+ );
4541
+ }
4542
+
4543
+ function configuredProviderRequiredTable(
4544
+ tableName: string,
4545
+ config: ResolvedBeignetConfig,
4546
+ ): string {
4547
+ for (const [key, defaultTableName] of Object.entries(
4548
+ defaultBeignetConfig.database.tables,
4549
+ ) as Array<[keyof BeignetDatabaseTables, string]>) {
4550
+ if (tableName === defaultTableName) return config.database.tables[key];
4551
+ }
4552
+
4553
+ return tableName;
4554
+ }
4555
+
4513
4556
  function hasRepositoryAdapterFile(
4514
4557
  files: string[],
4515
4558
  infrastructureDir: string,