@beignet/cli 0.0.13 → 0.0.15

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/src/inspect.ts CHANGED
@@ -1559,12 +1559,12 @@ async function inspectCanonicalConformance(
1559
1559
  });
1560
1560
  }
1561
1561
 
1562
- if (!/defineRouteGroup\s*<\s*AppContext\s*>/.test(source)) {
1562
+ if (!/defineRouteGroup\s*<\s*AppContext\s*>\s*\(\s*\)\s*\(/.test(source)) {
1563
1563
  diagnostics.push({
1564
1564
  severity: "warning",
1565
1565
  code: "BEIGNET_FEATURE_ROUTE_GROUP_CONTEXT",
1566
1566
  file,
1567
- message: `${file} should call defineRouteGroup<AppContext>(...) for ordinary feature route groups, or defineRouteGroup<AppContext>()({ ... }) when group-level hooks enrich ctx.`,
1567
+ message: `${file} should call defineRouteGroup<AppContext>()({ ... }) so route groups preserve per-route contract and use-case type checks.`,
1568
1568
  });
1569
1569
  }
1570
1570
  }
@@ -1975,6 +1975,30 @@ async function inspectProductionReadiness(
1975
1975
  config,
1976
1976
  sourceCache,
1977
1977
  );
1978
+ const stripeMemoryProviderMismatch = await hasStripeMemoryProviderMismatch(
1979
+ targetDir,
1980
+ sourceCache,
1981
+ configFiles,
1982
+ installedPackages,
1983
+ providerEntries,
1984
+ );
1985
+
1986
+ diagnostics.push(
1987
+ ...(await inspectEntitlementsProductionReadiness(
1988
+ targetDir,
1989
+ files,
1990
+ config,
1991
+ sourceCache,
1992
+ )),
1993
+ ...(await inspectPaymentsProductionReadiness(
1994
+ targetDir,
1995
+ files,
1996
+ config,
1997
+ sourceCache,
1998
+ installedPackages,
1999
+ stripeMemoryProviderMismatch,
2000
+ )),
2001
+ );
1978
2002
 
1979
2003
  for (const provider of providerDoctorRules) {
1980
2004
  if (!installedPackages.has(provider.packageName)) continue;
@@ -2048,6 +2072,184 @@ async function inspectProductionReadiness(
2048
2072
  return diagnostics;
2049
2073
  }
2050
2074
 
2075
+ async function inspectEntitlementsProductionReadiness(
2076
+ targetDir: string,
2077
+ files: string[],
2078
+ config: ResolvedBeignetConfig,
2079
+ sourceCache: Map<string, string>,
2080
+ ): Promise<InspectDiagnostic[]> {
2081
+ if (
2082
+ !(await usesEntitlementChecks(targetDir, files, sourceCache)) ||
2083
+ (await appPortsIncludesPort(
2084
+ targetDir,
2085
+ files,
2086
+ config,
2087
+ sourceCache,
2088
+ "entitlements",
2089
+ ))
2090
+ ) {
2091
+ return [];
2092
+ }
2093
+
2094
+ return [
2095
+ {
2096
+ severity: "warning",
2097
+ code: "BEIGNET_ENTITLEMENTS_PORT_MISSING",
2098
+ file: config.paths.ports,
2099
+ message:
2100
+ "This app checks entitlements, but AppPorts does not declare an entitlements port. Add entitlements: EntitlementsPort; and wire a provider-backed entitlement resolver before deploying paid feature gates.",
2101
+ },
2102
+ ];
2103
+ }
2104
+
2105
+ async function inspectPaymentsProductionReadiness(
2106
+ targetDir: string,
2107
+ files: string[],
2108
+ config: ResolvedBeignetConfig,
2109
+ sourceCache: Map<string, string>,
2110
+ installedPackages: Set<string>,
2111
+ stripeMemoryProviderMismatch: boolean,
2112
+ ): Promise<InspectDiagnostic[]> {
2113
+ if (
2114
+ !(
2115
+ hasBillingOrPaymentsFeature(files, config) ||
2116
+ installedPackages.has("@beignet/provider-payments-stripe")
2117
+ )
2118
+ ) {
2119
+ return [];
2120
+ }
2121
+
2122
+ const diagnostics: InspectDiagnostic[] = [];
2123
+
2124
+ if (stripeMemoryProviderMismatch) {
2125
+ diagnostics.push({
2126
+ severity: "warning",
2127
+ code: "BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
2128
+ file: serverProviderFiles(files, config)[0] ?? config.paths.server,
2129
+ message:
2130
+ "@beignet/provider-payments-stripe is installed and Stripe env/config is present, but server/providers.ts still registers memory payments. Register stripePaymentsProvider before deploying Stripe-backed billing, or remove the Stripe provider/config until the app is ready to switch.",
2131
+ });
2132
+ }
2133
+
2134
+ if (!(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))) {
2135
+ diagnostics.push({
2136
+ severity: "warning",
2137
+ code: "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
2138
+ file: directoryPath(config.paths.routes),
2139
+ message:
2140
+ "This app appears to use payments, but no route calls createPaymentWebhookRoute(...) or createWebhookRoute(...) for payments. Expose payment webhooks under app/api/webhooks/payments/route.ts or an equivalent configured route so provider events can update billing state.",
2141
+ });
2142
+ }
2143
+
2144
+ const billingContractsFile = `${directoryPath(config.paths.features)}/billing/contracts.ts`;
2145
+ if (files.includes(billingContractsFile)) {
2146
+ const source = await readCachedSource(
2147
+ targetDir,
2148
+ billingContractsFile,
2149
+ sourceCache,
2150
+ );
2151
+ const checkoutContract = exportedConstInitializer(
2152
+ source,
2153
+ "createCheckoutSession",
2154
+ );
2155
+ if (
2156
+ checkoutContract &&
2157
+ !(
2158
+ /\bidempotency\s*:/.test(checkoutContract) &&
2159
+ /["']idempotency-key["']/.test(checkoutContract)
2160
+ )
2161
+ ) {
2162
+ diagnostics.push({
2163
+ severity: "warning",
2164
+ code: "BEIGNET_BILLING_CHECKOUT_IDEMPOTENCY_MISSING",
2165
+ file: billingContractsFile,
2166
+ contract: "createCheckoutSession",
2167
+ message:
2168
+ "The billing checkout contract appears to create payment sessions without idempotency metadata and an idempotency-key header. Mark checkout as idempotent so retries do not create duplicate payment sessions.",
2169
+ });
2170
+ }
2171
+ }
2172
+
2173
+ const billingUseCasesFile = `${directoryPath(config.paths.features)}/billing/use-cases/index.ts`;
2174
+ if (files.includes(billingUseCasesFile)) {
2175
+ const source = await readCachedSource(
2176
+ targetDir,
2177
+ billingUseCasesFile,
2178
+ sourceCache,
2179
+ );
2180
+ if (
2181
+ /\bctx\.ports\.idempotency\b/.test(source) &&
2182
+ !(await appPortsIncludesIdempotency(
2183
+ targetDir,
2184
+ files,
2185
+ config,
2186
+ sourceCache,
2187
+ ))
2188
+ ) {
2189
+ diagnostics.push({
2190
+ severity: "warning",
2191
+ code: "BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
2192
+ file: config.paths.ports,
2193
+ message:
2194
+ "Billing use cases reference ctx.ports.idempotency, but AppPorts does not declare an idempotency port. Add idempotency to AppPorts and provider wiring, or remove the stale billing workflow dependency.",
2195
+ });
2196
+ }
2197
+ }
2198
+
2199
+ const billingEntitlementsFile = `${directoryPath(config.paths.features)}/billing/entitlements.ts`;
2200
+ if (
2201
+ files.includes(billingEntitlementsFile) &&
2202
+ !(await hasBillingEntitlementsProviderWiring(
2203
+ targetDir,
2204
+ files,
2205
+ config,
2206
+ sourceCache,
2207
+ ))
2208
+ ) {
2209
+ diagnostics.push({
2210
+ severity: "warning",
2211
+ code: "BEIGNET_BILLING_ENTITLEMENTS_PROVIDER_MISSING",
2212
+ file: path.dirname(config.paths.infrastructurePorts),
2213
+ message:
2214
+ "features/billing/entitlements.ts exists, but no infra or server provider appears to call createBillingEntitlements(...). Wire the billing-backed entitlements port into AppPorts so paid feature gates use app billing state.",
2215
+ });
2216
+ }
2217
+
2218
+ return diagnostics;
2219
+ }
2220
+
2221
+ async function hasStripeMemoryProviderMismatch(
2222
+ targetDir: string,
2223
+ sourceCache: Map<string, string>,
2224
+ configFiles: string[],
2225
+ installedPackages: Set<string>,
2226
+ providerEntries: string[],
2227
+ ): Promise<boolean> {
2228
+ if (!installedPackages.has("@beignet/provider-payments-stripe")) {
2229
+ return false;
2230
+ }
2231
+ if (
2232
+ findProviderEntryIndex(providerEntries, ["stripePaymentsProvider"]) >= 0
2233
+ ) {
2234
+ return false;
2235
+ }
2236
+ if (
2237
+ findProviderEntryIndex(providerEntries, [
2238
+ "createMemoryPaymentsProvider",
2239
+ "createMemoryPayments",
2240
+ ]) < 0
2241
+ ) {
2242
+ return false;
2243
+ }
2244
+
2245
+ return anyFileMatches(
2246
+ targetDir,
2247
+ configFiles,
2248
+ /\bSTRIPE_(?:SECRET_KEY|WEBHOOK_SECRET)\b/,
2249
+ sourceCache,
2250
+ );
2251
+ }
2252
+
2051
2253
  // Local-dev ranges such as the generated-app conformance vendoring point each
2052
2254
  // @beignet/* package at a different path on purpose, so they are exempt from
2053
2255
  // the declared-range comparison. Their installed versions still participate in
@@ -2200,9 +2402,22 @@ async function inspectProviderRegistrationDrift(
2200
2402
  config,
2201
2403
  sourceCache,
2202
2404
  );
2405
+ const stripeMemoryProviderMismatch = await hasStripeMemoryProviderMismatch(
2406
+ targetDir,
2407
+ sourceCache,
2408
+ operationalConfigFiles(files, config),
2409
+ installedPackages,
2410
+ providerEntries,
2411
+ );
2203
2412
 
2204
2413
  for (const provider of providerDoctorRules) {
2205
2414
  if (!installedPackages.has(provider.packageName)) continue;
2415
+ if (
2416
+ provider.packageName === "@beignet/provider-payments-stripe" &&
2417
+ stripeMemoryProviderMismatch
2418
+ ) {
2419
+ continue;
2420
+ }
2206
2421
 
2207
2422
  if (provider.variants !== undefined) {
2208
2423
  if (
@@ -2641,6 +2856,164 @@ function hasBetterAuthRoute(
2641
2856
  );
2642
2857
  }
2643
2858
 
2859
+ function hasBillingOrPaymentsFeature(
2860
+ files: string[],
2861
+ config: ResolvedBeignetConfig,
2862
+ ): boolean {
2863
+ const featuresPath = directoryPath(config.paths.features);
2864
+ return files.some(
2865
+ (file) =>
2866
+ file.startsWith(`${featuresPath}/billing/`) ||
2867
+ file.startsWith(`${featuresPath}/payments/`),
2868
+ );
2869
+ }
2870
+
2871
+ async function hasPaymentWebhookRoute(
2872
+ targetDir: string,
2873
+ files: string[],
2874
+ config: ResolvedBeignetConfig,
2875
+ sourceCache: Map<string, string>,
2876
+ ): Promise<boolean> {
2877
+ const routesPath = directoryPath(config.paths.routes);
2878
+ const routeFiles = files.filter(
2879
+ (file) => file.startsWith(`${routesPath}/`) && file.endsWith("/route.ts"),
2880
+ );
2881
+
2882
+ for (const file of routeFiles) {
2883
+ const source = await readCachedSource(targetDir, file, sourceCache);
2884
+ if (containsCallExpression(source, "createPaymentWebhookRoute")) {
2885
+ return true;
2886
+ }
2887
+ if (containsPaymentWebhookRoute(source)) {
2888
+ return true;
2889
+ }
2890
+ }
2891
+
2892
+ return false;
2893
+ }
2894
+
2895
+ function containsPaymentWebhookRoute(source: string): boolean {
2896
+ if (!containsCallExpression(source, "createWebhookRoute")) return false;
2897
+
2898
+ return (
2899
+ source.includes("payments.verifyWebhook") ||
2900
+ source.includes("paymentWebhook")
2901
+ );
2902
+ }
2903
+
2904
+ async function appPortsIncludesIdempotency(
2905
+ targetDir: string,
2906
+ files: string[],
2907
+ config: ResolvedBeignetConfig,
2908
+ sourceCache: Map<string, string>,
2909
+ ): Promise<boolean> {
2910
+ return appPortsIncludesPort(
2911
+ targetDir,
2912
+ files,
2913
+ config,
2914
+ sourceCache,
2915
+ "idempotency",
2916
+ );
2917
+ }
2918
+
2919
+ async function appPortsIncludesPort(
2920
+ targetDir: string,
2921
+ files: string[],
2922
+ config: ResolvedBeignetConfig,
2923
+ sourceCache: Map<string, string>,
2924
+ portName: string,
2925
+ ): Promise<boolean> {
2926
+ if (!files.includes(config.paths.ports)) return false;
2927
+
2928
+ const source = await readCachedSource(
2929
+ targetDir,
2930
+ config.paths.ports,
2931
+ sourceCache,
2932
+ );
2933
+ const declarationStart = source.search(/\bexport\s+type\s+AppPorts\b/);
2934
+ if (declarationStart < 0) return false;
2935
+
2936
+ const bodyStart = source.indexOf("{", declarationStart);
2937
+ if (bodyStart < 0) return false;
2938
+
2939
+ const body = balancedObjectSource(source, bodyStart);
2940
+ return (
2941
+ body !== undefined &&
2942
+ new RegExp(`\\b${escapeRegExp(portName)}\\s*:`).test(body)
2943
+ );
2944
+ }
2945
+
2946
+ async function usesEntitlementChecks(
2947
+ targetDir: string,
2948
+ files: string[],
2949
+ sourceCache: Map<string, string>,
2950
+ ): Promise<boolean> {
2951
+ for (const file of files) {
2952
+ if (!/\.(?:ts|tsx|mts|cts)$/.test(file)) continue;
2953
+ if (isTestSourceFile(file)) continue;
2954
+
2955
+ const source = await readCachedSource(targetDir, file, sourceCache);
2956
+ if (
2957
+ /\brequireEntitlement\s*\(/.test(source) ||
2958
+ /\bctx\.ports\.entitlements\b/.test(source)
2959
+ ) {
2960
+ return true;
2961
+ }
2962
+ }
2963
+
2964
+ return false;
2965
+ }
2966
+
2967
+ async function hasBillingEntitlementsProviderWiring(
2968
+ targetDir: string,
2969
+ files: string[],
2970
+ config: ResolvedBeignetConfig,
2971
+ sourceCache: Map<string, string>,
2972
+ ): Promise<boolean> {
2973
+ const infraPath = path.dirname(config.paths.infrastructurePorts);
2974
+ const serverPath = path.dirname(config.paths.server);
2975
+ const candidateFiles = files.filter(
2976
+ (file) =>
2977
+ /\.(?:ts|tsx|mts|cts)$/.test(file) &&
2978
+ !isTestSourceFile(file) &&
2979
+ (file.startsWith(`${infraPath}/`) || file.startsWith(`${serverPath}/`)),
2980
+ );
2981
+
2982
+ for (const file of candidateFiles) {
2983
+ const source = await readCachedSource(targetDir, file, sourceCache);
2984
+ if (containsCallExpression(source, "createBillingEntitlements")) {
2985
+ return true;
2986
+ }
2987
+ }
2988
+
2989
+ return false;
2990
+ }
2991
+
2992
+ function isTestSourceFile(file: string): boolean {
2993
+ return (
2994
+ /(?:^|\/)tests\//.test(file) ||
2995
+ /\.(?:test|spec)\.(?:ts|tsx|mts|cts)$/.test(file)
2996
+ );
2997
+ }
2998
+
2999
+ function exportedConstInitializer(
3000
+ source: string,
3001
+ exportName: string,
3002
+ ): string | undefined {
3003
+ const declaration = new RegExp(
3004
+ `\\bexport\\s+const\\s+${escapeRegExp(exportName)}\\s*=`,
3005
+ ).exec(source);
3006
+ if (declaration?.index === undefined) return undefined;
3007
+
3008
+ const start = declaration.index;
3009
+ const nextExport =
3010
+ /\n\s*export\s+(?:const|function|type|interface|class)\s+/g;
3011
+ nextExport.lastIndex = start + declaration[0].length;
3012
+ const next = nextExport.exec(source);
3013
+
3014
+ return source.slice(start, next?.index ?? source.length);
3015
+ }
3016
+
2644
3017
  function featureNameFromContractFile(
2645
3018
  file: string,
2646
3019
  config: ResolvedBeignetConfig,