@beignet/cli 0.0.47 → 0.0.49
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 +46 -0
- package/README.md +126 -42
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +8 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +55 -9
- package/dist/check.js.map +1 -1
- package/dist/db.d.ts +38 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +52 -0
- package/dist/db.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +454 -170
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +12 -5
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +3 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/tenancy.js +1 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +56 -19
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts +11 -3
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +139 -16
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +149 -9
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +13 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +122 -8
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -7
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +20 -6
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +1 -0
- package/dist/templates/db/index.d.ts.map +1 -1
- package/dist/templates/db/index.js.map +1 -1
- package/dist/templates/db/migration-status.d.ts +9 -0
- package/dist/templates/db/migration-status.d.ts.map +1 -0
- package/dist/templates/db/migration-status.js +117 -0
- package/dist/templates/db/migration-status.js.map +1 -0
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +23 -0
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +21 -0
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +53 -0
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +4 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +56 -11
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +86 -10
- package/src/db.ts +97 -0
- package/src/explain.ts +4 -4
- package/src/index.ts +108 -7
- package/src/inspect.ts +659 -216
- package/src/lib.ts +9 -1
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +12 -5
- package/src/make/payments.ts +3 -3
- package/src/make/tenancy.ts +1 -1
- package/src/make.ts +225 -8
- package/src/mcp.ts +73 -21
- package/src/preflight.ts +195 -16
- package/src/provider-add.ts +193 -9
- package/src/provider-audit.ts +210 -18
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +29 -7
- package/src/templates/base.ts +21 -4
- package/src/templates/db/index.ts +1 -0
- package/src/templates/db/migration-status.ts +126 -0
- package/src/templates/db/mysql.ts +23 -0
- package/src/templates/db/postgres.ts +21 -0
- package/src/templates/db/sqlite.ts +53 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +4 -2
package/src/inspect.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import {
|
|
6
|
+
type ContractFactoryBindings,
|
|
7
|
+
type ContractFactoryName,
|
|
8
|
+
contractFactoryBindings,
|
|
9
|
+
isImportedContractFactoryCall,
|
|
10
|
+
} from "./analysis/contract-factories.js";
|
|
11
|
+
import { canonicalFeatureLayerPath } from "./analysis/layers.js";
|
|
12
|
+
import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
|
|
13
|
+
import { exportedVariableDeclarations } from "./analysis/source-index.js";
|
|
4
14
|
import { createPainter } from "./ansi.js";
|
|
5
15
|
import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
|
|
6
16
|
import {
|
|
@@ -43,6 +53,7 @@ import {
|
|
|
43
53
|
import {
|
|
44
54
|
detectedProviderVariants,
|
|
45
55
|
diagnosticPackageJsonFile,
|
|
56
|
+
formatProviderEnvAlternatives,
|
|
46
57
|
formatProviderMetadataIssues,
|
|
47
58
|
installedPackageNames,
|
|
48
59
|
providerDoctorRulesForInstalledPackages,
|
|
@@ -65,7 +76,8 @@ import {
|
|
|
65
76
|
identifiersFromArrayExpression,
|
|
66
77
|
insertAfterImports,
|
|
67
78
|
matchingDelimiterIndex,
|
|
68
|
-
|
|
79
|
+
outboxRegistryEntryState,
|
|
80
|
+
outboxRegistryIdentifiers,
|
|
69
81
|
} from "./registry-edits.js";
|
|
70
82
|
import {
|
|
71
83
|
currentGeneratedPackageScripts,
|
|
@@ -220,6 +232,16 @@ type FeatureRouteGroup = {
|
|
|
220
232
|
}>;
|
|
221
233
|
};
|
|
222
234
|
|
|
235
|
+
type ReadContractsResult = {
|
|
236
|
+
contracts: InspectedContract[];
|
|
237
|
+
queryTransportDiagnostics: InspectDiagnostic[];
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type ParsedContract = {
|
|
241
|
+
contract: InspectedContract;
|
|
242
|
+
queryTransportMissing: boolean;
|
|
243
|
+
};
|
|
244
|
+
|
|
223
245
|
export async function inspectApp(
|
|
224
246
|
options: InspectAppOptions = {},
|
|
225
247
|
): Promise<InspectAppResult> {
|
|
@@ -231,7 +253,8 @@ export async function inspectApp(
|
|
|
231
253
|
? resolveConfig(options.config)
|
|
232
254
|
: await loadBeignetConfig(targetDir, files);
|
|
233
255
|
const convention = inspectConvention(files, config);
|
|
234
|
-
const
|
|
256
|
+
const inspectedContracts = await readContracts(targetDir, files, config);
|
|
257
|
+
const { contracts } = inspectedContracts;
|
|
235
258
|
const matchedRoutes = await inspectRouteSurface(
|
|
236
259
|
targetDir,
|
|
237
260
|
files,
|
|
@@ -246,6 +269,7 @@ export async function inspectApp(
|
|
|
246
269
|
contracts,
|
|
247
270
|
matchedRoutes,
|
|
248
271
|
Boolean(options.strict),
|
|
272
|
+
inspectedContracts.queryTransportDiagnostics,
|
|
249
273
|
);
|
|
250
274
|
|
|
251
275
|
return {
|
|
@@ -329,7 +353,7 @@ async function buildDoctorFixPlan(
|
|
|
329
353
|
? resolveConfig(options.config)
|
|
330
354
|
: await loadBeignetConfig(targetDir, files);
|
|
331
355
|
const convention = inspectConvention(files, config);
|
|
332
|
-
const contracts = await readContracts(targetDir, files, config);
|
|
356
|
+
const { contracts } = await readContracts(targetDir, files, config);
|
|
333
357
|
const matchedRoutes = await inspectRouteSurface(
|
|
334
358
|
targetDir,
|
|
335
359
|
files,
|
|
@@ -710,7 +734,6 @@ export const productionHardeningDiagnosticCodes = new Set([
|
|
|
710
734
|
"BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
|
|
711
735
|
"BEIGNET_CORS_CREDENTIALS_WILDCARD",
|
|
712
736
|
"BEIGNET_CRON_SECRET_MISSING",
|
|
713
|
-
"BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
|
|
714
737
|
"BEIGNET_ENTITLEMENTS_PORT_MISSING",
|
|
715
738
|
"BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
|
|
716
739
|
"BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
@@ -829,16 +852,32 @@ async function readContracts(
|
|
|
829
852
|
targetDir: string,
|
|
830
853
|
files: string[],
|
|
831
854
|
config: ResolvedBeignetConfig,
|
|
832
|
-
): Promise<
|
|
855
|
+
): Promise<ReadContractsResult> {
|
|
833
856
|
const contracts: InspectedContract[] = [];
|
|
857
|
+
const queryTransportDiagnostics: InspectDiagnostic[] = [];
|
|
834
858
|
const contractFiles = contractSourceFiles(files, config);
|
|
835
859
|
|
|
836
860
|
for (const file of contractFiles) {
|
|
837
861
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
838
|
-
|
|
862
|
+
for (const parsed of parseContracts(source, file)) {
|
|
863
|
+
contracts.push(parsed.contract);
|
|
864
|
+
if (!parsed.queryTransportMissing) continue;
|
|
865
|
+
|
|
866
|
+
queryTransportDiagnostics.push({
|
|
867
|
+
severity: "error",
|
|
868
|
+
code: "BEIGNET_QUERY_TRANSPORT_MISSING",
|
|
869
|
+
file,
|
|
870
|
+
contract: parsed.contract.exportName,
|
|
871
|
+
subject: { file, exportName: parsed.contract.exportName },
|
|
872
|
+
message: `${parsed.contract.exportName} declares a query schema without an explicit query transport. Pass defineQueryTransport(...) as the second .query(...) argument so the client, server, and OpenAPI document share one wire format.`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
839
875
|
}
|
|
840
876
|
|
|
841
|
-
return
|
|
877
|
+
return {
|
|
878
|
+
contracts: contracts.sort(compareContracts),
|
|
879
|
+
queryTransportDiagnostics,
|
|
880
|
+
};
|
|
842
881
|
}
|
|
843
882
|
|
|
844
883
|
function inspectConvention(
|
|
@@ -900,68 +939,126 @@ function hasRequirement(files: string[], requirement: string): boolean {
|
|
|
900
939
|
return files.includes(requirement);
|
|
901
940
|
}
|
|
902
941
|
|
|
903
|
-
function parseContracts(source: string, file: string):
|
|
904
|
-
const contracts:
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
942
|
+
function parseContracts(source: string, file: string): ParsedContract[] {
|
|
943
|
+
const contracts: ParsedContract[] = [];
|
|
944
|
+
const sourceFile = ts.createSourceFile(
|
|
945
|
+
file,
|
|
946
|
+
source,
|
|
947
|
+
ts.ScriptTarget.Latest,
|
|
948
|
+
true,
|
|
949
|
+
ts.ScriptKind.TS,
|
|
950
|
+
);
|
|
951
|
+
const assignments = variableAssignments(sourceFile);
|
|
952
|
+
const assignmentsByName = new Map(
|
|
953
|
+
assignments.map((assignment) => [assignment.name, assignment.expression]),
|
|
954
|
+
);
|
|
955
|
+
const factoryBindings = contractFactoryBindings(sourceFile);
|
|
909
956
|
|
|
910
|
-
for (const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const routePath = joinContractPaths(pathPrefix, methodMatch[3]);
|
|
957
|
+
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
958
|
+
sourceFile,
|
|
959
|
+
)) {
|
|
960
|
+
if (!declaration.initializer) continue;
|
|
961
|
+
const chain = resolveContractCallChain(
|
|
962
|
+
declaration.initializer,
|
|
963
|
+
assignmentsByName,
|
|
964
|
+
);
|
|
965
|
+
if (!chain) continue;
|
|
920
966
|
|
|
967
|
+
const directContract = parseDirectContract(chain, factoryBindings);
|
|
968
|
+
if (directContract) {
|
|
921
969
|
contracts.push({
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
970
|
+
contract: {
|
|
971
|
+
exportName,
|
|
972
|
+
file,
|
|
973
|
+
metadata: parseContractMetadata(
|
|
974
|
+
applyContractMetadataCalls(
|
|
975
|
+
emptyContractMetadataState(),
|
|
976
|
+
chain.calls,
|
|
977
|
+
),
|
|
978
|
+
),
|
|
979
|
+
method: directContract.method,
|
|
980
|
+
path: normalizeContractPath(directContract.path),
|
|
981
|
+
},
|
|
982
|
+
queryTransportMissing: contractQueryHasTransport(chain) === false,
|
|
930
983
|
});
|
|
931
984
|
continue;
|
|
932
985
|
}
|
|
933
986
|
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
987
|
+
const methodIndex = chain.calls.findIndex((call) =>
|
|
988
|
+
isHttpMethod(call.name.toUpperCase()),
|
|
989
|
+
);
|
|
990
|
+
if (methodIndex === -1) continue;
|
|
991
|
+
const methodCall = chain.calls[methodIndex];
|
|
992
|
+
const routePath = staticString(methodCall.arguments[0]);
|
|
993
|
+
if (!routePath) continue;
|
|
994
|
+
|
|
995
|
+
const hasGroupFactory = contractFactoryCall(
|
|
996
|
+
chain,
|
|
997
|
+
"defineContractGroup",
|
|
998
|
+
factoryBindings,
|
|
999
|
+
);
|
|
1000
|
+
if (!hasGroupFactory) continue;
|
|
1001
|
+
|
|
1002
|
+
const pathPrefix = applyStaticPrefixes(
|
|
1003
|
+
"",
|
|
1004
|
+
chain.calls.slice(0, methodIndex),
|
|
1005
|
+
);
|
|
1006
|
+
if (pathPrefix === undefined) continue;
|
|
1007
|
+
|
|
1008
|
+
contracts.push({
|
|
1009
|
+
contract: {
|
|
1010
|
+
exportName,
|
|
938
1011
|
file,
|
|
939
|
-
metadata: parseContractMetadata(
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1012
|
+
metadata: parseContractMetadata(
|
|
1013
|
+
applyContractMetadataCalls(emptyContractMetadataState(), chain.calls),
|
|
1014
|
+
),
|
|
1015
|
+
method: methodCall.name.toUpperCase() as HttpMethod,
|
|
1016
|
+
path: joinContractPaths(pathPrefix, routePath),
|
|
1017
|
+
},
|
|
1018
|
+
queryTransportMissing: contractQueryHasTransport(chain) === false,
|
|
1019
|
+
});
|
|
944
1020
|
}
|
|
945
1021
|
|
|
946
1022
|
return contracts;
|
|
947
1023
|
}
|
|
948
1024
|
|
|
1025
|
+
function unwrapContractExpression(expression: ts.Expression): ts.Expression {
|
|
1026
|
+
let current = expression;
|
|
1027
|
+
while (
|
|
1028
|
+
ts.isParenthesizedExpression(current) ||
|
|
1029
|
+
ts.isAsExpression(current) ||
|
|
1030
|
+
ts.isTypeAssertionExpression(current) ||
|
|
1031
|
+
ts.isNonNullExpression(current) ||
|
|
1032
|
+
ts.isSatisfiesExpression(current)
|
|
1033
|
+
) {
|
|
1034
|
+
current = current.expression;
|
|
1035
|
+
}
|
|
1036
|
+
return current;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
type ContractMetadataState = {
|
|
1040
|
+
authRequired?: boolean;
|
|
1041
|
+
authorizationAbility?: string;
|
|
1042
|
+
authorizationAuditRequired?: boolean;
|
|
1043
|
+
audit?: boolean;
|
|
1044
|
+
auditRequired?: boolean;
|
|
1045
|
+
catalogErrors: InspectedCatalogErrorRef[];
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
function emptyContractMetadataState(): ContractMetadataState {
|
|
1049
|
+
return { catalogErrors: [] };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
949
1052
|
function parseContractMetadata(
|
|
950
|
-
|
|
951
|
-
inheritedCatalogErrors: InspectedCatalogErrorRef[] = [],
|
|
1053
|
+
state: ContractMetadataState,
|
|
952
1054
|
): InspectedContractMetadata | undefined {
|
|
953
|
-
const authRequired =
|
|
954
|
-
const authorizationAbility =
|
|
955
|
-
/\bauthorization\s*:\s*\{[\s\S]*?\bability\s*:\s*["']([^"']+)["']/.exec(
|
|
956
|
-
block,
|
|
957
|
-
)?.[1];
|
|
1055
|
+
const authRequired = state.authRequired === true;
|
|
1056
|
+
const authorizationAbility = state.authorizationAbility;
|
|
958
1057
|
const auditRequired =
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
...parseCatalogErrorRefs(block),
|
|
964
|
-
]);
|
|
1058
|
+
state.audit === true ||
|
|
1059
|
+
state.auditRequired === true ||
|
|
1060
|
+
state.authorizationAuditRequired === true;
|
|
1061
|
+
const catalogErrors = state.catalogErrors;
|
|
965
1062
|
|
|
966
1063
|
if (
|
|
967
1064
|
!authRequired &&
|
|
@@ -980,21 +1077,129 @@ function parseContractMetadata(
|
|
|
980
1077
|
};
|
|
981
1078
|
}
|
|
982
1079
|
|
|
983
|
-
function
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1080
|
+
function applyContractMetadataCalls(
|
|
1081
|
+
initialState: ContractMetadataState,
|
|
1082
|
+
calls: ContractCall[],
|
|
1083
|
+
): ContractMetadataState {
|
|
1084
|
+
const state: ContractMetadataState = {
|
|
1085
|
+
...initialState,
|
|
1086
|
+
catalogErrors: [...initialState.catalogErrors],
|
|
1087
|
+
};
|
|
988
1088
|
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1089
|
+
for (const call of calls) {
|
|
1090
|
+
if (call.name === "meta") {
|
|
1091
|
+
applyContractMetaObject(state, objectLiteral(call.arguments[0]));
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (call.name === "errors") {
|
|
1095
|
+
state.catalogErrors = applyCatalogErrorCall(
|
|
1096
|
+
state.catalogErrors,
|
|
1097
|
+
call.arguments[0],
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
993
1101
|
|
|
994
|
-
|
|
995
|
-
|
|
1102
|
+
return state;
|
|
1103
|
+
}
|
|
996
1104
|
|
|
997
|
-
|
|
1105
|
+
function applyContractMetaObject(
|
|
1106
|
+
state: ContractMetadataState,
|
|
1107
|
+
metadata: ts.ObjectLiteralExpression | undefined,
|
|
1108
|
+
): void {
|
|
1109
|
+
if (!metadata) {
|
|
1110
|
+
state.authRequired = undefined;
|
|
1111
|
+
state.authorizationAbility = undefined;
|
|
1112
|
+
state.authorizationAuditRequired = undefined;
|
|
1113
|
+
state.audit = undefined;
|
|
1114
|
+
state.auditRequired = undefined;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const auth = objectPropertyResult(metadata, "auth");
|
|
1119
|
+
if (auth.kind !== "absent") {
|
|
1120
|
+
state.authRequired =
|
|
1121
|
+
auth.kind === "value" ? staticRequiredValue(auth.value) : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const authorization = objectPropertyResult(metadata, "authorization");
|
|
1125
|
+
if (authorization.kind !== "absent") {
|
|
1126
|
+
const value =
|
|
1127
|
+
authorization.kind === "value"
|
|
1128
|
+
? objectLiteral(authorization.value)
|
|
1129
|
+
: undefined;
|
|
1130
|
+
if (!value) {
|
|
1131
|
+
state.authorizationAbility = undefined;
|
|
1132
|
+
state.authorizationAuditRequired = undefined;
|
|
1133
|
+
} else {
|
|
1134
|
+
const ability = objectPropertyResult(value, "ability");
|
|
1135
|
+
state.authorizationAbility =
|
|
1136
|
+
ability.kind === "value" ? staticString(ability.value) : undefined;
|
|
1137
|
+
state.authorizationAuditRequired = objectRequiresAudit(value);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const audit = objectPropertyResult(metadata, "audit");
|
|
1142
|
+
const auditRequired = objectPropertyResult(metadata, "auditRequired");
|
|
1143
|
+
if (audit.kind !== "absent") {
|
|
1144
|
+
state.audit =
|
|
1145
|
+
audit.kind === "value" ? staticRequiredValue(audit.value) : undefined;
|
|
1146
|
+
}
|
|
1147
|
+
if (auditRequired.kind !== "absent") {
|
|
1148
|
+
state.auditRequired =
|
|
1149
|
+
auditRequired.kind === "value"
|
|
1150
|
+
? staticBoolean(auditRequired.value)
|
|
1151
|
+
: undefined;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function objectRequiresAudit(
|
|
1156
|
+
metadata: ts.ObjectLiteralExpression,
|
|
1157
|
+
): boolean | undefined {
|
|
1158
|
+
const audit = objectPropertyResult(metadata, "audit");
|
|
1159
|
+
const auditRequired = objectPropertyResult(metadata, "auditRequired");
|
|
1160
|
+
if (audit.kind === "unknown" || auditRequired.kind === "unknown") {
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
const auditValue =
|
|
1164
|
+
audit.kind === "value" ? staticRequiredValue(audit.value) : false;
|
|
1165
|
+
const auditRequiredValue =
|
|
1166
|
+
auditRequired.kind === "value" ? staticBoolean(auditRequired.value) : false;
|
|
1167
|
+
if (auditValue === undefined || auditRequiredValue === undefined) {
|
|
1168
|
+
return undefined;
|
|
1169
|
+
}
|
|
1170
|
+
return auditValue || auditRequiredValue;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function staticRequiredValue(expression: ts.Expression): boolean | undefined {
|
|
1174
|
+
const value = staticString(expression);
|
|
1175
|
+
return value === undefined ? undefined : value === "required";
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function staticBoolean(expression: ts.Expression): boolean | undefined {
|
|
1179
|
+
if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
1180
|
+
if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function parseDirectContract(
|
|
1185
|
+
chain: ContractCallChain,
|
|
1186
|
+
factoryBindings: ContractFactoryBindings,
|
|
1187
|
+
): { method: HttpMethod; path: string } | undefined {
|
|
1188
|
+
const factoryCall = contractFactoryCall(
|
|
1189
|
+
chain,
|
|
1190
|
+
"defineContract",
|
|
1191
|
+
factoryBindings,
|
|
1192
|
+
);
|
|
1193
|
+
if (!factoryCall) return undefined;
|
|
1194
|
+
const options = objectLiteral(factoryCall.arguments[0]);
|
|
1195
|
+
if (!options) return undefined;
|
|
1196
|
+
const methodValue = staticString(objectProperty(options, "method"));
|
|
1197
|
+
const contractPath = staticString(objectProperty(options, "path"));
|
|
1198
|
+
if (!methodValue || !contractPath) return undefined;
|
|
1199
|
+
|
|
1200
|
+
const method = methodValue.toUpperCase();
|
|
1201
|
+
if (!isHttpMethod(method)) return undefined;
|
|
1202
|
+
return { method, path: contractPath };
|
|
998
1203
|
}
|
|
999
1204
|
|
|
1000
1205
|
function isHttpMethod(method: string): method is HttpMethod {
|
|
@@ -1003,118 +1208,245 @@ function isHttpMethod(method: string): method is HttpMethod {
|
|
|
1003
1208
|
);
|
|
1004
1209
|
}
|
|
1005
1210
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
),
|
|
1011
|
-
].map((match) => ({
|
|
1012
|
-
name: match[1],
|
|
1013
|
-
expression: match[2],
|
|
1014
|
-
}));
|
|
1015
|
-
const prefixes = new Map<string, string>();
|
|
1211
|
+
type VariableAssignment = {
|
|
1212
|
+
name: string;
|
|
1213
|
+
expression: ts.Expression;
|
|
1214
|
+
};
|
|
1016
1215
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1216
|
+
type ContractCall = {
|
|
1217
|
+
name: string;
|
|
1218
|
+
arguments: readonly ts.Expression[];
|
|
1219
|
+
node: ts.CallExpression;
|
|
1220
|
+
};
|
|
1022
1221
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1222
|
+
type ContractCallChain = {
|
|
1223
|
+
baseIdentifier?: string;
|
|
1224
|
+
rootCall?: ContractCall;
|
|
1225
|
+
calls: ContractCall[];
|
|
1226
|
+
};
|
|
1025
1227
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1228
|
+
function variableAssignments(sourceFile: ts.SourceFile): VariableAssignment[] {
|
|
1229
|
+
return sourceFile.statements
|
|
1230
|
+
.filter(ts.isVariableStatement)
|
|
1231
|
+
.flatMap((statement) =>
|
|
1232
|
+
[...statement.declarationList.declarations]
|
|
1233
|
+
.filter(
|
|
1234
|
+
(
|
|
1235
|
+
declaration,
|
|
1236
|
+
): declaration is ts.VariableDeclaration & {
|
|
1237
|
+
name: ts.Identifier;
|
|
1238
|
+
initializer: ts.Expression;
|
|
1239
|
+
} =>
|
|
1240
|
+
ts.isIdentifier(declaration.name) &&
|
|
1241
|
+
declaration.initializer !== undefined,
|
|
1242
|
+
)
|
|
1243
|
+
.map((declaration) => ({
|
|
1244
|
+
name: declaration.name.text,
|
|
1245
|
+
expression: declaration.initializer,
|
|
1246
|
+
})),
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function resolveContractCallChain(
|
|
1251
|
+
expression: ts.Expression,
|
|
1252
|
+
assignments: ReadonlyMap<string, ts.Expression>,
|
|
1253
|
+
resolving = new Set<string>(),
|
|
1254
|
+
): ContractCallChain | undefined {
|
|
1255
|
+
const chain = contractCallChain(expression);
|
|
1256
|
+
if (!chain || chain.rootCall || !chain.baseIdentifier) return chain;
|
|
1257
|
+
|
|
1258
|
+
const baseExpression = assignments.get(chain.baseIdentifier);
|
|
1259
|
+
if (!baseExpression || resolving.has(chain.baseIdentifier)) return chain;
|
|
1260
|
+
|
|
1261
|
+
const nextResolving = new Set(resolving);
|
|
1262
|
+
nextResolving.add(chain.baseIdentifier);
|
|
1263
|
+
const baseChain = resolveContractCallChain(
|
|
1264
|
+
baseExpression,
|
|
1265
|
+
assignments,
|
|
1266
|
+
nextResolving,
|
|
1267
|
+
);
|
|
1268
|
+
if (!baseChain) return chain;
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
...baseChain,
|
|
1272
|
+
calls: [...baseChain.calls, ...chain.calls],
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function contractCallChain(
|
|
1277
|
+
rawExpression: ts.Expression,
|
|
1278
|
+
): ContractCallChain | undefined {
|
|
1279
|
+
const calls: ContractCall[] = [];
|
|
1280
|
+
let expression = rawExpression;
|
|
1281
|
+
|
|
1282
|
+
while (true) {
|
|
1283
|
+
expression = unwrapContractExpression(expression);
|
|
1284
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1285
|
+
expression = expression.expression;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (ts.isCallExpression(expression)) {
|
|
1289
|
+
const callee = unwrapContractExpression(expression.expression);
|
|
1290
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
1291
|
+
calls.unshift({
|
|
1292
|
+
name: callee.name.text,
|
|
1293
|
+
arguments: [...expression.arguments],
|
|
1294
|
+
node: expression,
|
|
1295
|
+
});
|
|
1296
|
+
expression = callee.expression;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (ts.isIdentifier(callee)) {
|
|
1300
|
+
return {
|
|
1301
|
+
baseIdentifier: callee.text,
|
|
1302
|
+
rootCall: {
|
|
1303
|
+
name: callee.text,
|
|
1304
|
+
arguments: [...expression.arguments],
|
|
1305
|
+
node: expression,
|
|
1306
|
+
},
|
|
1307
|
+
calls,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return undefined;
|
|
1032
1311
|
}
|
|
1312
|
+
if (ts.isIdentifier(expression)) {
|
|
1313
|
+
return { baseIdentifier: expression.text, calls };
|
|
1314
|
+
}
|
|
1315
|
+
return undefined;
|
|
1033
1316
|
}
|
|
1317
|
+
}
|
|
1034
1318
|
|
|
1035
|
-
|
|
1319
|
+
function contractFactoryCall(
|
|
1320
|
+
chain: ContractCallChain,
|
|
1321
|
+
name: ContractFactoryName,
|
|
1322
|
+
factoryBindings: ContractFactoryBindings,
|
|
1323
|
+
): ContractCall | undefined {
|
|
1324
|
+
const calls = chain.rootCall ? [chain.rootCall] : chain.calls;
|
|
1325
|
+
return calls.find((call) =>
|
|
1326
|
+
isImportedContractFactoryCall(call.node, name, factoryBindings),
|
|
1327
|
+
);
|
|
1036
1328
|
}
|
|
1037
1329
|
|
|
1038
|
-
function
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}));
|
|
1049
|
-
const groupErrors = new Map<string, InspectedCatalogErrorRef[]>();
|
|
1050
|
-
|
|
1051
|
-
let changed = true;
|
|
1052
|
-
while (changed) {
|
|
1053
|
-
changed = false;
|
|
1054
|
-
for (const assignment of assignments) {
|
|
1055
|
-
if (groupErrors.has(assignment.name)) continue;
|
|
1056
|
-
|
|
1057
|
-
const baseErrors = baseGroupErrors(assignment.expression, groupErrors);
|
|
1058
|
-
if (baseErrors === undefined) continue;
|
|
1059
|
-
|
|
1060
|
-
groupErrors.set(
|
|
1061
|
-
assignment.name,
|
|
1062
|
-
dedupeCatalogErrorRefs([
|
|
1063
|
-
...baseErrors,
|
|
1064
|
-
...parseCatalogErrorRefs(assignment.expression),
|
|
1065
|
-
]),
|
|
1066
|
-
);
|
|
1067
|
-
changed = true;
|
|
1068
|
-
}
|
|
1330
|
+
function applyStaticPrefixes(
|
|
1331
|
+
initialPrefix: string,
|
|
1332
|
+
calls: ContractCall[],
|
|
1333
|
+
): string | undefined {
|
|
1334
|
+
let prefix = initialPrefix;
|
|
1335
|
+
for (const call of calls) {
|
|
1336
|
+
if (call.name !== "prefix") continue;
|
|
1337
|
+
const next = staticString(call.arguments[0]);
|
|
1338
|
+
if (next === undefined) return undefined;
|
|
1339
|
+
prefix = joinContractPaths(prefix, next);
|
|
1069
1340
|
}
|
|
1341
|
+
return prefix;
|
|
1342
|
+
}
|
|
1070
1343
|
|
|
1071
|
-
|
|
1344
|
+
function contractQueryHasTransport(
|
|
1345
|
+
chain: ContractCallChain,
|
|
1346
|
+
): boolean | undefined {
|
|
1347
|
+
const queryCall = chain.calls.find((call) => call.name === "query");
|
|
1348
|
+
return queryCall ? queryCall.arguments.length >= 2 : undefined;
|
|
1072
1349
|
}
|
|
1073
1350
|
|
|
1074
|
-
function
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
): InspectedCatalogErrorRef[]
|
|
1078
|
-
|
|
1351
|
+
function applyCatalogErrorCall(
|
|
1352
|
+
current: InspectedCatalogErrorRef[],
|
|
1353
|
+
expression: ts.Expression | undefined,
|
|
1354
|
+
): InspectedCatalogErrorRef[] {
|
|
1355
|
+
const errors = objectLiteral(expression);
|
|
1356
|
+
if (!errors) return [];
|
|
1357
|
+
let merged = [...current];
|
|
1079
1358
|
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1359
|
+
for (const property of errors.properties) {
|
|
1360
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1361
|
+
merged = [];
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const key = staticPropertyName(property.name);
|
|
1365
|
+
if (!key) {
|
|
1366
|
+
merged = [];
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1082
1369
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1370
|
+
merged = merged.filter((ref) => ref.key !== key);
|
|
1371
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
1372
|
+
const value = unwrapContractExpression(property.initializer);
|
|
1373
|
+
if (
|
|
1374
|
+
!ts.isPropertyAccessExpression(value) ||
|
|
1375
|
+
!ts.isIdentifier(value.expression)
|
|
1376
|
+
) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
merged.push({
|
|
1380
|
+
key,
|
|
1381
|
+
catalog: value.expression.text,
|
|
1382
|
+
catalogKey: value.name.text,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1085
1385
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
prefixes: Map<string, string>,
|
|
1089
|
-
): string | undefined {
|
|
1090
|
-
if (expression.includes("defineContractGroup(")) return "";
|
|
1386
|
+
return merged;
|
|
1387
|
+
}
|
|
1091
1388
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1389
|
+
function objectLiteral(
|
|
1390
|
+
expression: ts.Expression | undefined,
|
|
1391
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
1392
|
+
if (!expression) return undefined;
|
|
1393
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1394
|
+
return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : undefined;
|
|
1395
|
+
}
|
|
1094
1396
|
|
|
1095
|
-
|
|
1397
|
+
function objectProperty(
|
|
1398
|
+
object: ts.ObjectLiteralExpression,
|
|
1399
|
+
name: string,
|
|
1400
|
+
): ts.Expression | undefined {
|
|
1401
|
+
const result = objectPropertyResult(object, name);
|
|
1402
|
+
return result.kind === "value" ? result.value : undefined;
|
|
1096
1403
|
}
|
|
1097
1404
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1405
|
+
type ObjectPropertyResult =
|
|
1406
|
+
| { kind: "absent" }
|
|
1407
|
+
| { kind: "unknown" }
|
|
1408
|
+
| { kind: "value"; value: ts.Expression };
|
|
1409
|
+
|
|
1410
|
+
function objectPropertyResult(
|
|
1411
|
+
object: ts.ObjectLiteralExpression,
|
|
1412
|
+
name: string,
|
|
1413
|
+
): ObjectPropertyResult {
|
|
1414
|
+
for (const property of [...object.properties].reverse()) {
|
|
1415
|
+
if (ts.isSpreadAssignment(property)) return { kind: "unknown" };
|
|
1416
|
+
const propertyName = staticPropertyName(property.name);
|
|
1417
|
+
if (propertyName === undefined) return { kind: "unknown" };
|
|
1418
|
+
if (propertyName !== name) continue;
|
|
1419
|
+
if (ts.isPropertyAssignment(property)) {
|
|
1420
|
+
return {
|
|
1421
|
+
kind: "value",
|
|
1422
|
+
value: unwrapContractExpression(property.initializer),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return { kind: "unknown" };
|
|
1426
|
+
}
|
|
1427
|
+
return { kind: "absent" };
|
|
1102
1428
|
}
|
|
1103
1429
|
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
);
|
|
1430
|
+
function staticPropertyName(name: ts.PropertyName): string | undefined {
|
|
1431
|
+
if (
|
|
1432
|
+
ts.isIdentifier(name) ||
|
|
1433
|
+
ts.isStringLiteral(name) ||
|
|
1434
|
+
ts.isNumericLiteral(name)
|
|
1435
|
+
) {
|
|
1436
|
+
return name.text;
|
|
1437
|
+
}
|
|
1438
|
+
return undefined;
|
|
1114
1439
|
}
|
|
1115
1440
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1441
|
+
function staticString(
|
|
1442
|
+
expression: ts.Expression | undefined,
|
|
1443
|
+
): string | undefined {
|
|
1444
|
+
if (!expression) return undefined;
|
|
1445
|
+
const value = unwrapContractExpression(expression);
|
|
1446
|
+
return ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
|
|
1447
|
+
? value.text
|
|
1448
|
+
: undefined;
|
|
1449
|
+
}
|
|
1118
1450
|
|
|
1119
1451
|
function parseErrorCatalogKeys(source: string): Set<string> {
|
|
1120
1452
|
const keys = new Set<string>();
|
|
@@ -1207,23 +1539,6 @@ function topLevelObjectKeys(body: string): string[] {
|
|
|
1207
1539
|
return keys;
|
|
1208
1540
|
}
|
|
1209
1541
|
|
|
1210
|
-
function dedupeCatalogErrorRefs(
|
|
1211
|
-
refs: InspectedCatalogErrorRef[],
|
|
1212
|
-
): InspectedCatalogErrorRef[] {
|
|
1213
|
-
const seen = new Set<string>();
|
|
1214
|
-
const deduped: InspectedCatalogErrorRef[] = [];
|
|
1215
|
-
|
|
1216
|
-
for (const ref of refs) {
|
|
1217
|
-
const key = `${ref.key}:${ref.catalog}:${ref.catalogKey}`;
|
|
1218
|
-
if (seen.has(key)) continue;
|
|
1219
|
-
|
|
1220
|
-
seen.add(key);
|
|
1221
|
-
deduped.push(ref);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return deduped;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
1542
|
function joinContractPaths(prefix: string, routePath: string): string {
|
|
1228
1543
|
if (!prefix) return normalizeContractPath(routePath);
|
|
1229
1544
|
const segments = [
|
|
@@ -1666,8 +1981,9 @@ async function inspectDiagnostics(
|
|
|
1666
1981
|
contracts: InspectedContract[],
|
|
1667
1982
|
matchedRoutes: MatchedRoutesResult,
|
|
1668
1983
|
strict: boolean,
|
|
1984
|
+
queryTransportDiagnostics: InspectDiagnostic[],
|
|
1669
1985
|
): Promise<InspectDiagnostic[]> {
|
|
1670
|
-
const diagnostics: InspectDiagnostic[] = [];
|
|
1986
|
+
const diagnostics: InspectDiagnostic[] = [...queryTransportDiagnostics];
|
|
1671
1987
|
const frameworkLayout =
|
|
1672
1988
|
config.framework === "next" ? convention.nextLayout : convention.webLayout;
|
|
1673
1989
|
const missingFrameworkLayout =
|
|
@@ -1774,6 +2090,7 @@ async function inspectDiagnostics(
|
|
|
1774
2090
|
contracts,
|
|
1775
2091
|
)),
|
|
1776
2092
|
...(await inspectServerlessFootguns(targetDir, files, config, convention)),
|
|
2093
|
+
...(await inspectOutboxJobDispatcherDrift(targetDir, files, config)),
|
|
1777
2094
|
...(await inspectProductionReadiness(targetDir, files, config, convention)),
|
|
1778
2095
|
...(await inspectFeatureArtifactDrift(
|
|
1779
2096
|
targetDir,
|
|
@@ -2471,14 +2788,14 @@ async function inspectProductionReadiness(
|
|
|
2471
2788
|
|
|
2472
2789
|
if (
|
|
2473
2790
|
containsCallExpression(source, "createDevtoolsRoute") &&
|
|
2474
|
-
|
|
2475
|
-
!/\
|
|
2791
|
+
!/\bauthorize\s*(?::|\()/.test(source) &&
|
|
2792
|
+
!/\ballowUnauthenticatedDevelopmentAccess\s*:\s*true\b/.test(source)
|
|
2476
2793
|
) {
|
|
2477
2794
|
diagnostics.push({
|
|
2478
2795
|
severity: "warning",
|
|
2479
|
-
code: "
|
|
2796
|
+
code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
|
|
2480
2797
|
file,
|
|
2481
|
-
message: `${file}
|
|
2798
|
+
message: `${file} creates a devtools route without an access policy, so the route remains hidden. Add app-owned authorize(...), or explicitly set allowUnauthenticatedDevelopmentAccess: true only for a development server bound exclusively to loopback.`,
|
|
2482
2799
|
});
|
|
2483
2800
|
}
|
|
2484
2801
|
|
|
@@ -2619,6 +2936,27 @@ async function inspectProductionReadiness(
|
|
|
2619
2936
|
provider.variants,
|
|
2620
2937
|
providerEntries,
|
|
2621
2938
|
)) {
|
|
2939
|
+
const envAlternatives = configuredProviderRequiredEnvAlternatives(
|
|
2940
|
+
variant.requiredEnvAlternatives,
|
|
2941
|
+
config,
|
|
2942
|
+
);
|
|
2943
|
+
if (
|
|
2944
|
+
envAlternatives.length > 0 &&
|
|
2945
|
+
!(await providerRequiredEnvAlternativeExists(
|
|
2946
|
+
targetDir,
|
|
2947
|
+
configFiles,
|
|
2948
|
+
envAlternatives,
|
|
2949
|
+
sourceCache,
|
|
2950
|
+
))
|
|
2951
|
+
) {
|
|
2952
|
+
diagnostics.push({
|
|
2953
|
+
severity: "warning",
|
|
2954
|
+
code: "BEIGNET_PROVIDER_ENV_MISSING",
|
|
2955
|
+
file: "package.json",
|
|
2956
|
+
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the provider registration.`,
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2622
2960
|
for (const envVar of configuredProviderRequiredEnv(
|
|
2623
2961
|
variant.requiredEnv,
|
|
2624
2962
|
config,
|
|
@@ -2669,6 +3007,27 @@ async function inspectProductionReadiness(
|
|
|
2669
3007
|
continue;
|
|
2670
3008
|
}
|
|
2671
3009
|
|
|
3010
|
+
const envAlternatives = configuredProviderRequiredEnvAlternatives(
|
|
3011
|
+
provider.requiredEnvAlternatives ?? [],
|
|
3012
|
+
config,
|
|
3013
|
+
);
|
|
3014
|
+
if (
|
|
3015
|
+
envAlternatives.length > 0 &&
|
|
3016
|
+
!(await providerRequiredEnvAlternativeExists(
|
|
3017
|
+
targetDir,
|
|
3018
|
+
configFiles,
|
|
3019
|
+
envAlternatives,
|
|
3020
|
+
sourceCache,
|
|
3021
|
+
))
|
|
3022
|
+
) {
|
|
3023
|
+
diagnostics.push({
|
|
3024
|
+
severity: "warning",
|
|
3025
|
+
code: "BEIGNET_PROVIDER_ENV_MISSING",
|
|
3026
|
+
file: "package.json",
|
|
3027
|
+
message: `${provider.packageName} is installed, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the unused provider package.`,
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2672
3031
|
for (const envVar of configuredProviderRequiredEnv(
|
|
2673
3032
|
provider.requiredEnv ?? [],
|
|
2674
3033
|
config,
|
|
@@ -3839,9 +4198,7 @@ async function inspectFeatureArtifactDrift(
|
|
|
3839
4198
|
}
|
|
3840
4199
|
|
|
3841
4200
|
for (const file of files) {
|
|
3842
|
-
const match =
|
|
3843
|
-
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.test\\.ts$`,
|
|
3844
|
-
).exec(file);
|
|
4201
|
+
const match = directFeatureTestMatch(file, featuresPath);
|
|
3845
4202
|
if (!match) continue;
|
|
3846
4203
|
diagnostics.push({
|
|
3847
4204
|
severity: "warning",
|
|
@@ -3853,23 +4210,27 @@ async function inspectFeatureArtifactDrift(
|
|
|
3853
4210
|
|
|
3854
4211
|
for (const file of files) {
|
|
3855
4212
|
if (
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
file.endsWith(".d.ts")
|
|
4213
|
+
!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
|
|
4214
|
+
/\.d\.(?:ts|mts|cts)$/.test(file)
|
|
3859
4215
|
) {
|
|
3860
4216
|
continue;
|
|
3861
4217
|
}
|
|
3862
4218
|
|
|
3863
|
-
const misplaced =
|
|
4219
|
+
const misplaced = directFeatureTestMatch(file, featuresPath)
|
|
4220
|
+
? undefined
|
|
4221
|
+
: misplacedFeatureLayer(file, config);
|
|
3864
4222
|
if (misplaced) {
|
|
3865
4223
|
diagnostics.push({
|
|
3866
4224
|
severity: "warning",
|
|
3867
4225
|
code: "BEIGNET_FEATURE_ARTIFACT_FOLDER",
|
|
3868
4226
|
file,
|
|
3869
|
-
message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned
|
|
4227
|
+
message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned files in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
|
|
3870
4228
|
});
|
|
3871
4229
|
}
|
|
3872
4230
|
|
|
4231
|
+
if (isTestSourceFile(file)) continue;
|
|
4232
|
+
if (!file.endsWith(".ts")) continue;
|
|
4233
|
+
|
|
3873
4234
|
if (
|
|
3874
4235
|
isNotificationDefinitionFile(file, featuresPath) ||
|
|
3875
4236
|
isInfraOrServerFile(file, config)
|
|
@@ -3890,6 +4251,15 @@ async function inspectFeatureArtifactDrift(
|
|
|
3890
4251
|
return diagnostics;
|
|
3891
4252
|
}
|
|
3892
4253
|
|
|
4254
|
+
function directFeatureTestMatch(
|
|
4255
|
+
file: string,
|
|
4256
|
+
featuresPath: string,
|
|
4257
|
+
): RegExpExecArray | null {
|
|
4258
|
+
return new RegExp(
|
|
4259
|
+
`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`,
|
|
4260
|
+
).exec(file);
|
|
4261
|
+
}
|
|
4262
|
+
|
|
3893
4263
|
function isFeatureArtifactFile(
|
|
3894
4264
|
file: string,
|
|
3895
4265
|
featuresPath: string,
|
|
@@ -3922,34 +4292,30 @@ async function appHasUploadRoute(
|
|
|
3922
4292
|
return false;
|
|
3923
4293
|
}
|
|
3924
4294
|
|
|
3925
|
-
function
|
|
4295
|
+
function misplacedFeatureLayer(
|
|
3926
4296
|
file: string,
|
|
3927
|
-
|
|
4297
|
+
config: ResolvedBeignetConfig,
|
|
3928
4298
|
): { actual: string; expected: string } | undefined {
|
|
3929
|
-
const
|
|
3930
|
-
|
|
3931
|
-
|
|
4299
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
4300
|
+
const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/(.+)$`).exec(
|
|
4301
|
+
file,
|
|
4302
|
+
);
|
|
3932
4303
|
if (!match) return undefined;
|
|
3933
4304
|
|
|
3934
|
-
const
|
|
3935
|
-
|
|
3936
|
-
command: "tasks/",
|
|
3937
|
-
commands: "tasks/",
|
|
3938
|
-
event: "domain/events/",
|
|
3939
|
-
events: "domain/events/",
|
|
3940
|
-
job: "jobs/",
|
|
3941
|
-
listener: "listeners/",
|
|
3942
|
-
notification: "notifications/",
|
|
3943
|
-
schedule: "schedules/",
|
|
3944
|
-
scheduled: "schedules/",
|
|
3945
|
-
task: "tasks/",
|
|
3946
|
-
upload: "uploads/",
|
|
3947
|
-
};
|
|
4305
|
+
const canonicalPath = canonicalFeatureLayerPath(file, config);
|
|
4306
|
+
if (!canonicalPath) return undefined;
|
|
3948
4307
|
|
|
3949
|
-
const
|
|
3950
|
-
|
|
4308
|
+
const featurePrefix = `${featuresPath}/${match[1]}/`;
|
|
4309
|
+
const actualRelative = file.slice(featurePrefix.length);
|
|
4310
|
+
const expectedRelative = canonicalPath.slice(featurePrefix.length);
|
|
4311
|
+
const actual = actualRelative.includes("/")
|
|
4312
|
+
? `${actualRelative.split("/")[0]}/`
|
|
4313
|
+
: actualRelative;
|
|
4314
|
+
const expectedDirectory = path.posix.dirname(expectedRelative);
|
|
4315
|
+
const expected =
|
|
4316
|
+
expectedDirectory === "." ? expectedRelative : `${expectedDirectory}/`;
|
|
3951
4317
|
|
|
3952
|
-
return { actual
|
|
4318
|
+
return { actual, expected };
|
|
3953
4319
|
}
|
|
3954
4320
|
|
|
3955
4321
|
function isNotificationDefinitionFile(
|
|
@@ -4328,10 +4694,7 @@ async function workflowRegistrationDrift(
|
|
|
4328
4694
|
// Events without listeners have nothing to deliver on drain, so only
|
|
4329
4695
|
// record-only events stay out of the registry without a warning.
|
|
4330
4696
|
const listened = await listenedEventNames(targetDir, files, config);
|
|
4331
|
-
const
|
|
4332
|
-
const registered = value
|
|
4333
|
-
? identifiersFromArrayExpression(value.text)
|
|
4334
|
-
: new Set<string>();
|
|
4697
|
+
const registered = outboxRegistryIdentifiers(outboxSource, "events");
|
|
4335
4698
|
drift.outbox.events = unregisteredWorkflowRegistries(
|
|
4336
4699
|
eventRegistries,
|
|
4337
4700
|
registered,
|
|
@@ -4340,10 +4703,7 @@ async function workflowRegistrationDrift(
|
|
|
4340
4703
|
}
|
|
4341
4704
|
|
|
4342
4705
|
if (jobRegistries.length > 0) {
|
|
4343
|
-
const
|
|
4344
|
-
const registered = value
|
|
4345
|
-
? identifiersFromArrayExpression(value.text)
|
|
4346
|
-
: new Set<string>();
|
|
4706
|
+
const registered = outboxRegistryIdentifiers(outboxSource, "jobs");
|
|
4347
4707
|
drift.outbox.jobs = unregisteredWorkflowRegistries(
|
|
4348
4708
|
jobRegistries,
|
|
4349
4709
|
registered,
|
|
@@ -5043,6 +5403,56 @@ async function appHasOutboxDrainEntrypoint(
|
|
|
5043
5403
|
return false;
|
|
5044
5404
|
}
|
|
5045
5405
|
|
|
5406
|
+
async function inspectOutboxJobDispatcherDrift(
|
|
5407
|
+
targetDir: string,
|
|
5408
|
+
files: string[],
|
|
5409
|
+
config: ResolvedBeignetConfig,
|
|
5410
|
+
): Promise<InspectDiagnostic[]> {
|
|
5411
|
+
if (!hasOutboxRegistry(files, config)) return [];
|
|
5412
|
+
|
|
5413
|
+
const source = await readFile(
|
|
5414
|
+
path.join(targetDir, config.paths.outbox),
|
|
5415
|
+
"utf8",
|
|
5416
|
+
);
|
|
5417
|
+
const jobsState = outboxRegistryEntryState(source, "jobs");
|
|
5418
|
+
if (jobsState === "absent" || jobsState === "empty") return [];
|
|
5419
|
+
|
|
5420
|
+
const analysis = await analyzePortWiringFiles({ targetDir, files, config });
|
|
5421
|
+
const declaresJobs = analysis.declared.has("jobs");
|
|
5422
|
+
const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
|
|
5423
|
+
if (declaresJobs && wiresJobs) return [];
|
|
5424
|
+
|
|
5425
|
+
const file =
|
|
5426
|
+
jobsState === "indeterminate"
|
|
5427
|
+
? config.paths.outbox
|
|
5428
|
+
: !declaresJobs
|
|
5429
|
+
? config.paths.ports
|
|
5430
|
+
: config.paths.portWiring;
|
|
5431
|
+
const reason = !analysis.portsFileExists
|
|
5432
|
+
? `${config.paths.ports} does not exist at the configured path`
|
|
5433
|
+
: !declaresJobs
|
|
5434
|
+
? analysis.declarationIndeterminate
|
|
5435
|
+
? `${config.paths.ports} does not expose a statically verifiable jobs declaration`
|
|
5436
|
+
: `${config.paths.ports} does not declare a jobs port`
|
|
5437
|
+
: !analysis.portWiringFileExists
|
|
5438
|
+
? `${config.paths.portWiring} does not exist at the configured path`
|
|
5439
|
+
: analysis.indeterminate
|
|
5440
|
+
? `${config.paths.portWiring} does not expose statically verifiable jobs wiring`
|
|
5441
|
+
: `${config.paths.portWiring} does not bind or defer the jobs port`;
|
|
5442
|
+
|
|
5443
|
+
return [
|
|
5444
|
+
{
|
|
5445
|
+
severity: "warning",
|
|
5446
|
+
code: "BEIGNET_OUTBOX_JOB_DISPATCHER_MISSING",
|
|
5447
|
+
file,
|
|
5448
|
+
message:
|
|
5449
|
+
jobsState === "indeterminate"
|
|
5450
|
+
? `The jobs entries in the outbox registry at ${config.paths.outbox} could not be statically resolved, and ${reason}. Keep the jobs property explicit so doctor can verify it, or add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`
|
|
5451
|
+
: `The outbox registry in ${config.paths.outbox} contains jobs, but ${reason}. Add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`,
|
|
5452
|
+
},
|
|
5453
|
+
];
|
|
5454
|
+
}
|
|
5455
|
+
|
|
5046
5456
|
async function inspectPortProviderDrift(
|
|
5047
5457
|
targetDir: string,
|
|
5048
5458
|
files: string[],
|
|
@@ -5175,7 +5585,7 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
5175
5585
|
}
|
|
5176
5586
|
|
|
5177
5587
|
if (hasDrizzleArtifacts) {
|
|
5178
|
-
for (const script of ["db:generate", "db:migrate"] as const) {
|
|
5588
|
+
for (const script of ["db:generate", "db:migrate", "db:status"] as const) {
|
|
5179
5589
|
if (packageScripts[script]) continue;
|
|
5180
5590
|
diagnostics.push({
|
|
5181
5591
|
severity: "warning",
|
|
@@ -5257,6 +5667,12 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
5257
5667
|
file: `${infrastructurePath}/db/reset.ts`,
|
|
5258
5668
|
code: "BEIGNET_DB_RESET_ENTRYPOINT_MISSING",
|
|
5259
5669
|
},
|
|
5670
|
+
{
|
|
5671
|
+
script: "db:status",
|
|
5672
|
+
command: "status",
|
|
5673
|
+
file: `${infrastructurePath}/db/migration-status.ts`,
|
|
5674
|
+
code: "BEIGNET_DB_STATUS_ENTRYPOINT_MISSING",
|
|
5675
|
+
},
|
|
5260
5676
|
] as const;
|
|
5261
5677
|
|
|
5262
5678
|
for (const entrypoint of standardEntrypoints) {
|
|
@@ -6284,6 +6700,32 @@ function configuredProviderRequiredEnv(
|
|
|
6284
6700
|
return envVars.filter((envVar) => !ignored.has(envVar));
|
|
6285
6701
|
}
|
|
6286
6702
|
|
|
6703
|
+
function configuredProviderRequiredEnvAlternatives(
|
|
6704
|
+
alternatives: readonly (readonly string[])[],
|
|
6705
|
+
config: ResolvedBeignetConfig,
|
|
6706
|
+
): string[][] {
|
|
6707
|
+
return alternatives.map((alternative) =>
|
|
6708
|
+
configuredProviderRequiredEnv(alternative, config),
|
|
6709
|
+
);
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
async function providerRequiredEnvAlternativeExists(
|
|
6713
|
+
targetDir: string,
|
|
6714
|
+
configFiles: readonly string[],
|
|
6715
|
+
alternatives: readonly (readonly string[])[],
|
|
6716
|
+
sourceCache: Map<string, string>,
|
|
6717
|
+
): Promise<boolean> {
|
|
6718
|
+
for (const alternative of alternatives) {
|
|
6719
|
+
const exists = await Promise.all(
|
|
6720
|
+
alternative.map((envVar) =>
|
|
6721
|
+
providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache),
|
|
6722
|
+
),
|
|
6723
|
+
);
|
|
6724
|
+
if (exists.every(Boolean)) return true;
|
|
6725
|
+
}
|
|
6726
|
+
return false;
|
|
6727
|
+
}
|
|
6728
|
+
|
|
6287
6729
|
function configuredProviderRequiredTable(
|
|
6288
6730
|
tableName: string,
|
|
6289
6731
|
config: ResolvedBeignetConfig,
|
|
@@ -8309,8 +8751,9 @@ async function contractsFromRoutesSource(
|
|
|
8309
8751
|
source,
|
|
8310
8752
|
routeIdentifier,
|
|
8311
8753
|
);
|
|
8754
|
+
const { contracts } = await readContracts(targetDir, files, config);
|
|
8312
8755
|
const registeredContracts = contractsForRegisteredRouteGroups(
|
|
8313
|
-
|
|
8756
|
+
contracts,
|
|
8314
8757
|
routeGroups,
|
|
8315
8758
|
registeredGroups,
|
|
8316
8759
|
);
|