@facilis.dev/analyze 0.0.0-stage → 0.1.0

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/analyze.d.ts +17 -0
  4. package/dist/analyze.js +99 -0
  5. package/dist/ast/node.d.ts +29 -0
  6. package/dist/ast/node.js +85 -0
  7. package/dist/ast/walk.d.ts +8 -0
  8. package/dist/ast/walk.js +40 -0
  9. package/dist/diagnostics.d.ts +5 -0
  10. package/dist/diagnostics.js +12 -0
  11. package/dist/fs.d.ts +13 -0
  12. package/dist/fs.js +51 -0
  13. package/dist/graph-edges.d.ts +3 -0
  14. package/dist/graph-edges.js +148 -0
  15. package/dist/graph-ids.d.ts +6 -0
  16. package/dist/graph-ids.js +22 -0
  17. package/dist/graph.d.ts +15 -0
  18. package/dist/graph.js +158 -0
  19. package/dist/index.d.ts +23 -0
  20. package/dist/index.js +15 -0
  21. package/dist/json.d.ts +8 -0
  22. package/dist/json.js +31 -0
  23. package/dist/module-graph.d.ts +33 -0
  24. package/dist/module-graph.js +86 -0
  25. package/dist/module-info.d.ts +27 -0
  26. package/dist/module-info.js +143 -0
  27. package/dist/node.d.ts +16 -0
  28. package/dist/node.js +41 -0
  29. package/dist/parser.d.ts +16 -0
  30. package/dist/parser.js +1 -0
  31. package/dist/passes/clients.d.ts +22 -0
  32. package/dist/passes/clients.js +167 -0
  33. package/dist/passes/config.d.ts +16 -0
  34. package/dist/passes/config.js +229 -0
  35. package/dist/passes/file-facts.d.ts +13 -0
  36. package/dist/passes/file-facts.js +130 -0
  37. package/dist/passes/i18n.d.ts +14 -0
  38. package/dist/passes/i18n.js +55 -0
  39. package/dist/passes/job-relations-diagnostics.d.ts +9 -0
  40. package/dist/passes/job-relations-diagnostics.js +38 -0
  41. package/dist/passes/job-relations.d.ts +18 -0
  42. package/dist/passes/job-relations.js +52 -0
  43. package/dist/passes/jobs.d.ts +20 -0
  44. package/dist/passes/jobs.js +133 -0
  45. package/dist/passes/jsx.d.ts +22 -0
  46. package/dist/passes/jsx.js +89 -0
  47. package/dist/passes/path-expression.d.ts +26 -0
  48. package/dist/passes/path-expression.js +265 -0
  49. package/dist/passes/route-config.d.ts +24 -0
  50. package/dist/passes/route-config.js +152 -0
  51. package/dist/passes/route-description.d.ts +13 -0
  52. package/dist/passes/route-description.js +23 -0
  53. package/dist/passes/route-elevation.d.ts +18 -0
  54. package/dist/passes/route-elevation.js +136 -0
  55. package/dist/passes/route-handler.d.ts +27 -0
  56. package/dist/passes/route-handler.js +87 -0
  57. package/dist/passes/route-location.d.ts +16 -0
  58. package/dist/passes/route-location.js +58 -0
  59. package/dist/passes/route-relation-resolve.d.ts +51 -0
  60. package/dist/passes/route-relation-resolve.js +172 -0
  61. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  62. package/dist/passes/route-relations-diagnostics.js +79 -0
  63. package/dist/passes/route-relations-follow.d.ts +16 -0
  64. package/dist/passes/route-relations-follow.js +116 -0
  65. package/dist/passes/route-relations-usage.d.ts +50 -0
  66. package/dist/passes/route-relations-usage.js +44 -0
  67. package/dist/passes/route-relations-walk.d.ts +25 -0
  68. package/dist/passes/route-relations-walk.js +199 -0
  69. package/dist/passes/route-relations.d.ts +17 -0
  70. package/dist/passes/route-relations.js +43 -0
  71. package/dist/passes/route-table.d.ts +21 -0
  72. package/dist/passes/route-table.js +104 -0
  73. package/dist/passes/route-tables.d.ts +10 -0
  74. package/dist/passes/route-tables.js +42 -0
  75. package/dist/passes/routes.d.ts +18 -0
  76. package/dist/passes/routes.js +152 -0
  77. package/dist/passes/schema-column-types.d.ts +9 -0
  78. package/dist/passes/schema-column-types.js +55 -0
  79. package/dist/passes/schema-columns.d.ts +35 -0
  80. package/dist/passes/schema-columns.js +197 -0
  81. package/dist/passes/schema-dsl.d.ts +24 -0
  82. package/dist/passes/schema-dsl.js +117 -0
  83. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  84. package/dist/passes/schema-file-links-ddl.js +106 -0
  85. package/dist/passes/schema-file-links.d.ts +19 -0
  86. package/dist/passes/schema-file-links.js +144 -0
  87. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  88. package/dist/passes/schema-foreign-keys.js +80 -0
  89. package/dist/passes/schema-policies.d.ts +15 -0
  90. package/dist/passes/schema-policies.js +201 -0
  91. package/dist/passes/schema-re-exports.d.ts +5 -0
  92. package/dist/passes/schema-re-exports.js +77 -0
  93. package/dist/passes/schema-relations.d.ts +8 -0
  94. package/dist/passes/schema-relations.js +182 -0
  95. package/dist/passes/schema-scope.d.ts +7 -0
  96. package/dist/passes/schema-scope.js +20 -0
  97. package/dist/passes/schema-tables.d.ts +13 -0
  98. package/dist/passes/schema-tables.js +149 -0
  99. package/dist/passes/schema.d.ts +9 -0
  100. package/dist/passes/schema.js +55 -0
  101. package/dist/passes/screen-components.d.ts +22 -0
  102. package/dist/passes/screen-components.js +143 -0
  103. package/dist/passes/screen-preview.d.ts +29 -0
  104. package/dist/passes/screen-preview.js +159 -0
  105. package/dist/passes/screens.d.ts +12 -0
  106. package/dist/passes/screens.js +278 -0
  107. package/dist/passes/static-read.d.ts +47 -0
  108. package/dist/passes/static-read.js +151 -0
  109. package/dist/passes/views.d.ts +20 -0
  110. package/dist/passes/views.js +101 -0
  111. package/dist/passes/workflow-tables.d.ts +18 -0
  112. package/dist/passes/workflow-tables.js +71 -0
  113. package/dist/passes/workflows.d.ts +8 -0
  114. package/dist/passes/workflows.js +160 -0
  115. package/dist/path-resolver.d.ts +20 -0
  116. package/dist/path-resolver.js +134 -0
  117. package/dist/revision.d.ts +2 -0
  118. package/dist/revision.js +23 -0
  119. package/dist/source.d.ts +4 -0
  120. package/dist/source.js +13 -0
  121. package/dist/static-eval.d.ts +36 -0
  122. package/dist/static-eval.js +215 -0
  123. package/dist/types-graph.d.ts +130 -0
  124. package/dist/types-graph.js +4 -0
  125. package/dist/types-jobs.d.ts +28 -0
  126. package/dist/types-jobs.js +14 -0
  127. package/dist/types-relations.d.ts +49 -0
  128. package/dist/types-relations.js +11 -0
  129. package/dist/types-workflows.d.ts +36 -0
  130. package/dist/types-workflows.js +3 -0
  131. package/dist/types.d.ts +226 -0
  132. package/dist/types.js +1 -0
  133. package/package.json +49 -4
  134. package/src/analyze.ts +119 -0
  135. package/src/ast/node.ts +102 -0
  136. package/src/ast/walk.ts +37 -0
  137. package/src/diagnostics.ts +22 -0
  138. package/src/fs.ts +59 -0
  139. package/src/graph-edges.ts +167 -0
  140. package/src/graph-ids.ts +31 -0
  141. package/src/graph.ts +190 -0
  142. package/src/index.ts +24 -0
  143. package/src/json.ts +34 -0
  144. package/src/module-graph.ts +117 -0
  145. package/src/module-info.ts +162 -0
  146. package/src/node.ts +54 -0
  147. package/src/parser.ts +19 -0
  148. package/src/passes/clients.ts +233 -0
  149. package/src/passes/config.ts +278 -0
  150. package/src/passes/file-facts.ts +142 -0
  151. package/src/passes/i18n.ts +66 -0
  152. package/src/passes/job-relations-diagnostics.ts +60 -0
  153. package/src/passes/job-relations.ts +76 -0
  154. package/src/passes/jobs.ts +181 -0
  155. package/src/passes/jsx.ts +92 -0
  156. package/src/passes/path-expression.ts +297 -0
  157. package/src/passes/route-config.ts +188 -0
  158. package/src/passes/route-description.ts +33 -0
  159. package/src/passes/route-elevation.ts +161 -0
  160. package/src/passes/route-handler.ts +105 -0
  161. package/src/passes/route-location.ts +62 -0
  162. package/src/passes/route-relation-resolve.ts +228 -0
  163. package/src/passes/route-relations-diagnostics.ts +115 -0
  164. package/src/passes/route-relations-follow.ts +146 -0
  165. package/src/passes/route-relations-usage.ts +98 -0
  166. package/src/passes/route-relations-walk.ts +255 -0
  167. package/src/passes/route-relations.ts +62 -0
  168. package/src/passes/route-table.ts +123 -0
  169. package/src/passes/route-tables.ts +50 -0
  170. package/src/passes/routes.ts +188 -0
  171. package/src/passes/schema-column-types.ts +62 -0
  172. package/src/passes/schema-columns.ts +287 -0
  173. package/src/passes/schema-dsl.ts +136 -0
  174. package/src/passes/schema-file-links-ddl.ts +125 -0
  175. package/src/passes/schema-file-links.ts +213 -0
  176. package/src/passes/schema-foreign-keys.ts +121 -0
  177. package/src/passes/schema-policies.ts +270 -0
  178. package/src/passes/schema-re-exports.ts +95 -0
  179. package/src/passes/schema-relations.ts +239 -0
  180. package/src/passes/schema-scope.ts +29 -0
  181. package/src/passes/schema-tables.ts +216 -0
  182. package/src/passes/schema.ts +67 -0
  183. package/src/passes/screen-components.ts +162 -0
  184. package/src/passes/screen-preview.ts +268 -0
  185. package/src/passes/screens.ts +291 -0
  186. package/src/passes/static-read.ts +196 -0
  187. package/src/passes/views.ts +125 -0
  188. package/src/passes/workflow-tables.ts +87 -0
  189. package/src/passes/workflows.ts +213 -0
  190. package/src/path-resolver.ts +146 -0
  191. package/src/revision.ts +25 -0
  192. package/src/source.ts +15 -0
  193. package/src/static-eval.ts +242 -0
  194. package/src/types-graph.ts +161 -0
  195. package/src/types-jobs.ts +40 -0
  196. package/src/types-relations.ts +64 -0
  197. package/src/types-workflows.ts +42 -0
  198. package/src/types.ts +299 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The wrapper config subset: permission/crossSite/dbTables/dbColumns/description/input.
3
+ *
4
+ * Split from routes.ts so both modules stay under the 300-line limit; every failure is an
5
+ * explicit unknown (never an empty value) and a diagnostic, as in the rest of the analyzer.
6
+ */
7
+ import { callArguments } from "../ast/node.js";
8
+ import { error, warning } from "../diagnostics.js";
9
+ import { descriptionDiagnostics } from "./route-description.js";
10
+ import { readStaticObject, refOf } from "./static-read.js";
11
+ const MISSING_PERMISSION = "permission is missing";
12
+ const MISSING_TABLES = "dbTables is missing; declare an array ([] means no DB)";
13
+ function stringArrayValue(value) {
14
+ if (!Array.isArray(value))
15
+ return null;
16
+ return value.every((item) => typeof item === "string") ? [...value] : null;
17
+ }
18
+ function permissionValue(value) {
19
+ return typeof value === "string" ? [value] : stringArrayValue(value);
20
+ }
21
+ function addUnknown(list, code, reason, source) {
22
+ list.push({ code, reason, source });
23
+ }
24
+ function unreadable(field, entry) {
25
+ return field + " cannot be read statically: " + (entry.reason ?? "unsupported expression");
26
+ }
27
+ /** Read the wrapper config from the static subset; every failure is explicit. */
28
+ export function routeConfigFacts(graph, file, call, config) {
29
+ const context = { graph, file: file.path };
30
+ const ref = refOf(context, call);
31
+ // Input schemas (body/query/params) are read for presence only: a Zod expression there is
32
+ // expected, so only the security/display fields below report a static-read failure.
33
+ const object = readStaticObject(callArguments(call)[0], context, {
34
+ code: "route-config-static",
35
+ report: false,
36
+ });
37
+ const unknown = [...object.unknown];
38
+ const field = (name) => object.fields.get(name);
39
+ const permissionField = field("permission");
40
+ let permission = null;
41
+ if (permissionField === undefined) {
42
+ addUnknown(unknown, "route-config-static", MISSING_PERMISSION, ref);
43
+ }
44
+ else if (!permissionField.ok) {
45
+ addUnknown(unknown, "route-config-static", unreadable("permission", permissionField), ref);
46
+ }
47
+ else {
48
+ permission = permissionValue(permissionField.value);
49
+ if (permission === null) {
50
+ addUnknown(unknown, "route-config-static", "permission must be a string or string[]", ref);
51
+ }
52
+ }
53
+ const crossSiteField = field("crossSite");
54
+ let crossSite = false;
55
+ if (crossSiteField !== undefined && !crossSiteField.ok) {
56
+ crossSite = null;
57
+ addUnknown(unknown, "route-config-static", unreadable("crossSite", crossSiteField), ref);
58
+ }
59
+ else if (crossSiteField !== undefined) {
60
+ if (typeof crossSiteField.value === "boolean")
61
+ crossSite = crossSiteField.value;
62
+ else {
63
+ crossSite = null;
64
+ addUnknown(unknown, "route-config-static", "crossSite must be a boolean", ref);
65
+ }
66
+ }
67
+ const rawSqlField = field("rawSql");
68
+ let rawSql = false;
69
+ if (rawSqlField !== undefined && !rawSqlField.ok) {
70
+ rawSql = null;
71
+ addUnknown(unknown, "route-config-static", unreadable("rawSql", rawSqlField), ref);
72
+ }
73
+ else if (rawSqlField !== undefined) {
74
+ if (typeof rawSqlField.value === "boolean")
75
+ rawSql = rawSqlField.value;
76
+ else {
77
+ // An unreadable derogation is never assumed: db.execute then fails closed (D-157).
78
+ rawSql = null;
79
+ addUnknown(unknown, "route-config-static", "rawSql must be a boolean", ref);
80
+ }
81
+ }
82
+ const tablesField = field("dbTables");
83
+ let dbTables = null;
84
+ if (tablesField === undefined) {
85
+ addUnknown(unknown, "db-tables-present", MISSING_TABLES, ref);
86
+ }
87
+ else if (!tablesField.ok) {
88
+ addUnknown(unknown, "route-config-static", unreadable("dbTables", tablesField), ref);
89
+ }
90
+ else {
91
+ dbTables = stringArrayValue(tablesField.value);
92
+ if (dbTables === null) {
93
+ addUnknown(unknown, "route-config-static", "dbTables must be a string[]", ref);
94
+ }
95
+ }
96
+ const columnsField = field("dbColumns");
97
+ let dbColumns = columnsField === undefined ? [] : null;
98
+ if (columnsField !== undefined && !columnsField.ok) {
99
+ dbColumns = null;
100
+ addUnknown(unknown, "route-config-static", unreadable("dbColumns", columnsField), ref);
101
+ }
102
+ else if (columnsField !== undefined) {
103
+ dbColumns = stringArrayValue(columnsField.value);
104
+ if (dbColumns === null) {
105
+ addUnknown(unknown, "route-config-static", "dbColumns must be a string[]", ref);
106
+ }
107
+ }
108
+ const descriptionField = field("description");
109
+ let description = null;
110
+ if (descriptionField !== undefined && !descriptionField.ok) {
111
+ addUnknown(unknown, "route-config-static", unreadable("description", descriptionField), ref);
112
+ }
113
+ else if (descriptionField !== undefined) {
114
+ if (typeof descriptionField.value === "string")
115
+ description = descriptionField.value;
116
+ else
117
+ addUnknown(unknown, "route-config-static", "description must be a string", ref);
118
+ }
119
+ const input = {
120
+ body: field("body") !== undefined,
121
+ form: field("form") !== undefined,
122
+ query: field("query") !== undefined,
123
+ params: field("params") !== undefined,
124
+ };
125
+ const diagnostics = [
126
+ ...unknown.map((item) => error(item.code, item.reason, item.source)),
127
+ ...descriptionDiagnostics(description, ref),
128
+ ];
129
+ if (permission !== null) {
130
+ for (const name of permission) {
131
+ if (!config.permissions.includes(name)) {
132
+ diagnostics.push(error("permission-declared", 'permission "' + name + '" is not declared in config.auth.permissions', ref));
133
+ }
134
+ }
135
+ }
136
+ // v3a: crossSite + a non-empty permission is legal now (the route is reached with a Bearer
137
+ // token); the warning only reminds the author that a session cookie never crosses origins.
138
+ if (crossSite === true && permission !== null && permission.length > 0) {
139
+ diagnostics.push(warning("route-cross-site-authenticated", "crossSite route requires a Bearer token: session cookies never cross origins", ref));
140
+ }
141
+ return {
142
+ permission,
143
+ crossSite,
144
+ rawSql,
145
+ dbTables,
146
+ dbColumns,
147
+ description,
148
+ input,
149
+ unknown,
150
+ diagnostics,
151
+ };
152
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Route description rules (D-129).
3
+ *
4
+ * The description is source documentation: one English sentence stored in the
5
+ * defineAuthorizedHandler config, shown under the route node and in the OpenAPI document.
6
+ * Two lines is its useful maximum, so the analyzer reports a warning when it runs long.
7
+ * The patch validator imports the same limit, so the AI and the user see one number.
8
+ */
9
+ import type { Diagnostic, SourceRef } from "../types.js";
10
+ /** About two lines in the route definition and in the graph card. */
11
+ export declare const ROUTE_DESCRIPTION_MAX_CHARS = 120;
12
+ /** A warning when the description exceed the two-line budget; nothing to report otherwise. */
13
+ export declare function descriptionDiagnostics(description: string | null, source: SourceRef | null): readonly Diagnostic[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Route description rules (D-129).
3
+ *
4
+ * The description is source documentation: one English sentence stored in the
5
+ * defineAuthorizedHandler config, shown under the route node and in the OpenAPI document.
6
+ * Two lines is its useful maximum, so the analyzer reports a warning when it runs long.
7
+ * The patch validator imports the same limit, so the AI and the user see one number.
8
+ */
9
+ import { warning } from "../diagnostics.js";
10
+ /** About two lines in the route definition and in the graph card. */
11
+ export const ROUTE_DESCRIPTION_MAX_CHARS = 120;
12
+ /** A warning when the description exceed the two-line budget; nothing to report otherwise. */
13
+ export function descriptionDiagnostics(description, source) {
14
+ if (description === null || description.length <= ROUTE_DESCRIPTION_MAX_CHARS)
15
+ return [];
16
+ return [
17
+ warning("route-description-length", "description is " +
18
+ description.length +
19
+ " characters; keep it to " +
20
+ ROUTE_DESCRIPTION_MAX_CHARS +
21
+ " (about two lines)", source),
22
+ ];
23
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Audited elevation facts from a route handler (D-099): `ctx.sudo` and `ctx.system`.
3
+ *
4
+ * Only the handler's own context can elevate: a destructured `{ sudo }`/`{ system }` binding
5
+ * or a member call on the context parameter. Any other identifier named `sudo` is ignored, so
6
+ * the graph cannot be colored by a local helper with the same name. The tables and operation
7
+ * are read through the bounded static subset; when that fails the fact is explicit (unknown
8
+ * plus an empty table list), never guessed.
9
+ */
10
+ import { type AstNode } from "../ast/node.js";
11
+ import type { ModuleGraph, ProjectFile } from "../module-graph.js";
12
+ import type { ElevationFact, UnknownFact } from "../types.js";
13
+ export interface ElevationScan {
14
+ readonly facts: readonly ElevationFact[];
15
+ readonly unknown: readonly UnknownFact[];
16
+ }
17
+ /** Scan one `defineAuthorizedHandler` call's handler for audited elevations. */
18
+ export declare function scanElevations(graph: ModuleGraph, file: ProjectFile, wrapperCall: AstNode): ElevationScan;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Audited elevation facts from a route handler (D-099): `ctx.sudo` and `ctx.system`.
3
+ *
4
+ * Only the handler's own context can elevate: a destructured `{ sudo }`/`{ system }` binding
5
+ * or a member call on the context parameter. Any other identifier named `sudo` is ignored, so
6
+ * the graph cannot be colored by a local helper with the same name. The tables and operation
7
+ * are read through the bounded static subset; when that fails the fact is explicit (unknown
8
+ * plus an empty table list), never guessed.
9
+ */
10
+ import { callArguments, identifierName, memberName, nodeArray, objectEntries, propertyName, propertyValue, unwrapExpression, } from "../ast/node.js";
11
+ import { walk } from "../ast/walk.js";
12
+ import { readStaticObject } from "./static-read.js";
13
+ function refOf(file, node) {
14
+ const offset = typeof node.start === "number" ? node.start : 0;
15
+ let line = 1;
16
+ for (let index = 0; index < offset && index < file.source.length; index += 1) {
17
+ if (file.source.charCodeAt(index) === 10)
18
+ line += 1;
19
+ }
20
+ return { file: file.path, line };
21
+ }
22
+ function elevationKind(name) {
23
+ if (name === "sudo" || name === "system")
24
+ return name;
25
+ return null;
26
+ }
27
+ /** Local identifier -> elevation kind for `{ sudo }` / `{ sudo: elevate }`. */
28
+ function handlerBindings(handler) {
29
+ const locals = new Map();
30
+ const context = nodeArray(handler, "params")[1];
31
+ if (context === undefined)
32
+ return { locals, contextName: null };
33
+ if (context.type === "Identifier") {
34
+ return { locals, contextName: identifierName(context) ?? null };
35
+ }
36
+ if (context.type !== "ObjectPattern")
37
+ return { locals, contextName: null };
38
+ for (const entry of objectEntries(context)) {
39
+ if (entry.type !== "Property")
40
+ continue;
41
+ const kind = elevationKind(propertyName(entry));
42
+ if (kind === null)
43
+ continue;
44
+ let value = propertyValue(entry);
45
+ if (value !== undefined && value.type === "AssignmentPattern") {
46
+ value = unwrapExpression(value.left);
47
+ }
48
+ const local = identifierName(unwrapExpression(value));
49
+ if (local !== undefined)
50
+ locals.set(local, kind);
51
+ }
52
+ return { locals, contextName: null };
53
+ }
54
+ function stringArray(value) {
55
+ if (!Array.isArray(value))
56
+ return null;
57
+ return value.every((entry) => typeof entry === "string") ? [...value] : null;
58
+ }
59
+ /** Scan one `defineAuthorizedHandler` call's handler for audited elevations. */
60
+ export function scanElevations(graph, file, wrapperCall) {
61
+ const handler = callArguments(wrapperCall)[1];
62
+ if (handler === undefined)
63
+ return { facts: [], unknown: [] };
64
+ const handlerNode = unwrapExpression(handler);
65
+ if (handlerNode === undefined ||
66
+ (handlerNode.type !== "ArrowFunctionExpression" && handlerNode.type !== "FunctionExpression")) {
67
+ return { facts: [], unknown: [] };
68
+ }
69
+ const bindings = handlerBindings(handlerNode);
70
+ const facts = [];
71
+ const unknown = [];
72
+ walk(handlerNode, (node) => {
73
+ if (node.type !== "CallExpression")
74
+ return;
75
+ const callee = unwrapExpression(node.callee);
76
+ if (callee === undefined)
77
+ return;
78
+ let kind = null;
79
+ if (callee.type === "Identifier") {
80
+ kind = bindings.locals.get(identifierName(callee) ?? "") ?? null;
81
+ }
82
+ else if (callee.type === "MemberExpression") {
83
+ const name = memberName(callee);
84
+ const object = unwrapExpression(callee.object);
85
+ if (name !== undefined &&
86
+ object?.type === "Identifier" &&
87
+ identifierName(object) === bindings.contextName) {
88
+ kind = elevationKind(name);
89
+ }
90
+ }
91
+ if (kind === null)
92
+ return;
93
+ const source = refOf(file, node);
94
+ const options = readStaticObject(callArguments(node)[0], { graph, file: file.path }, {
95
+ code: "route-elevation-static",
96
+ report: true,
97
+ });
98
+ const factUnknown = [...options.unknown];
99
+ const tablesField = options.fields.get("tables");
100
+ let tables = [];
101
+ if (tablesField === undefined) {
102
+ factUnknown.push({
103
+ code: "route-elevation-static",
104
+ reason: "elevation tables are missing",
105
+ source,
106
+ });
107
+ }
108
+ else if (!tablesField.ok) {
109
+ factUnknown.push({
110
+ code: "route-elevation-static",
111
+ reason: "elevation tables cannot be read statically",
112
+ source,
113
+ });
114
+ }
115
+ else {
116
+ const read = stringArray(tablesField.value);
117
+ if (read === null) {
118
+ factUnknown.push({
119
+ code: "route-elevation-static",
120
+ reason: "elevation tables must be a string[]",
121
+ source,
122
+ });
123
+ }
124
+ else {
125
+ tables = read;
126
+ }
127
+ }
128
+ const operationField = options.fields.get("operation");
129
+ const operation = operationField !== undefined && operationField.ok && typeof operationField.value === "string"
130
+ ? operationField.value
131
+ : null;
132
+ unknown.push(...factUnknown);
133
+ facts.push({ kind, tables, operation, source, unknown: factUnknown });
134
+ });
135
+ return { facts, unknown };
136
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Facts read from a route handler expression (docs/findings/ast-dsl-contract.md §3.1).
3
+ *
4
+ * The security rules need to know whether a handler reads the request input — the JSON body
5
+ * or the multipart form: the fact must come from the AST, not from a comment or a string, so
6
+ * a route cannot look validated by mentioning `body`/`form` in prose.
7
+ *
8
+ * The same reasoning drives the direct-DB warning: a handler that reaches the raw client
9
+ * through `event.context.facilis.db` or imports the `createDb` factory bypasses the
10
+ * policy-scoped client, and only the AST can tell a real access from a string mention.
11
+ */
12
+ import { type AstNode } from "../ast/node.js";
13
+ import type { ProjectFile } from "../module-graph.js";
14
+ import type { Diagnostic, SourceRef } from "../types.js";
15
+ /** Warning text for raw/direct DB access in a route file. */
16
+ export declare const ROUTE_DIRECT_DB_MESSAGE = "Direct DB access bypasses the policy-scoped client; use ctx.db for user-scoped work and ctx.system/ctx.sudo for audited system operations (not recommended).";
17
+ /** True when the handler reads `.body` or destructures a `{ body }` pattern. */
18
+ export declare function handlerReadsBody(call: AstNode): boolean;
19
+ /** True when the handler reads `.form` or destructures a `{ form }` pattern (D-053). */
20
+ export declare function handlerReadsForm(call: AstNode): boolean;
21
+ /**
22
+ * Warnings for raw/direct DB access in a route file: `event.context.facilis.db` bypasses
23
+ * the policy-scoped client, and importing the `createDb` factory is the raw-client path.
24
+ * The source points at the offending node (or the route when a node has no position), so
25
+ * the IDE can jump to it.
26
+ */
27
+ export declare function directDbWarnings(file: ProjectFile, fallback: SourceRef): Diagnostic[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Facts read from a route handler expression (docs/findings/ast-dsl-contract.md §3.1).
3
+ *
4
+ * The security rules need to know whether a handler reads the request input — the JSON body
5
+ * or the multipart form: the fact must come from the AST, not from a comment or a string, so
6
+ * a route cannot look validated by mentioning `body`/`form` in prose.
7
+ *
8
+ * The same reasoning drives the direct-DB warning: a handler that reaches the raw client
9
+ * through `event.context.facilis.db` or imports the `createDb` factory bypasses the
10
+ * policy-scoped client, and only the AST can tell a real access from a string mention.
11
+ */
12
+ import { asNode, callArguments, memberName, objectEntries, propertyName, unwrapExpression, } from "../ast/node.js";
13
+ import { walk } from "../ast/walk.js";
14
+ import { warning } from "../diagnostics.js";
15
+ import { sourceRef } from "../source.js";
16
+ const RAW_DB_SPECIFIER = "@facilis.dev/db";
17
+ const RAW_DB_FACTORY = "createDb";
18
+ /** Warning text for raw/direct DB access in a route file. */
19
+ export const ROUTE_DIRECT_DB_MESSAGE = "Direct DB access bypasses the policy-scoped client; use ctx.db for user-scoped work and ctx.system/ctx.sudo for audited system operations (not recommended).";
20
+ function nodeRef(file, node, fallback) {
21
+ return typeof node.start === "number" ? sourceRef(file.path, file.source, node.start) : fallback;
22
+ }
23
+ /** True when the object chain of `node` contains a `.facilis` property. */
24
+ function touchesFacilisContext(node) {
25
+ let current = node;
26
+ while (current !== undefined && current.type === "MemberExpression") {
27
+ if (memberName(current) === "facilis")
28
+ return true;
29
+ current = asNode(unwrapExpression(current.object));
30
+ }
31
+ return false;
32
+ }
33
+ /** `event.context.facilis.db` (or any `*.facilis.db` access): the raw policy-free client. */
34
+ function isDirectDbMember(node) {
35
+ if (node.type !== "MemberExpression" || memberName(node) !== "db")
36
+ return false;
37
+ const object = asNode(unwrapExpression(node.object));
38
+ return object !== undefined && touchesFacilisContext(object);
39
+ }
40
+ /** True when the handler reads `name` off the request: member access or destructuring. */
41
+ function handlerReads(call, name) {
42
+ const handler = callArguments(call)[1];
43
+ if (handler === undefined)
44
+ return false;
45
+ let found = false;
46
+ walk(handler, (node) => {
47
+ if (found)
48
+ return;
49
+ if (node.type === "MemberExpression" && memberName(node) === name) {
50
+ found = true;
51
+ return;
52
+ }
53
+ if (node.type === "ObjectPattern" &&
54
+ objectEntries(node).some((entry) => entry.type === "Property" && propertyName(entry) === name)) {
55
+ found = true;
56
+ }
57
+ });
58
+ return found;
59
+ }
60
+ /** True when the handler reads `.body` or destructures a `{ body }` pattern. */
61
+ export function handlerReadsBody(call) {
62
+ return handlerReads(call, "body");
63
+ }
64
+ /** True when the handler reads `.form` or destructures a `{ form }` pattern (D-053). */
65
+ export function handlerReadsForm(call) {
66
+ return handlerReads(call, "form");
67
+ }
68
+ /**
69
+ * Warnings for raw/direct DB access in a route file: `event.context.facilis.db` bypasses
70
+ * the policy-scoped client, and importing the `createDb` factory is the raw-client path.
71
+ * The source points at the offending node (or the route when a node has no position), so
72
+ * the IDE can jump to it.
73
+ */
74
+ export function directDbWarnings(file, fallback) {
75
+ const refs = [];
76
+ walk(file.program, (node) => {
77
+ if (isDirectDbMember(node))
78
+ refs.push(nodeRef(file, node, fallback));
79
+ });
80
+ for (const imported of file.module.imports) {
81
+ if (imported.specifier === RAW_DB_SPECIFIER && imported.imported === RAW_DB_FACTORY) {
82
+ refs.push(imported.source);
83
+ }
84
+ }
85
+ refs.sort((left, right) => left.line - right.line);
86
+ return refs.map((source) => warning("route-direct-db", ROUTE_DIRECT_DB_MESSAGE, source));
87
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Route file name -> method/path (Nitro's file router, SPEC §4.2).
3
+ *
4
+ * Kept apart from the route pass so the filename rules are testable and the pass stays within
5
+ * the file-length limit. Two deliberate extensions over @facilis.dev/nitro's route-path: every
6
+ * HTTP method is recognized, and a method-less file serves ALL methods (Nitro's behavior).
7
+ */
8
+ import type { HttpMethod, RouteFact } from "../types.js";
9
+ export interface RouteLocation {
10
+ readonly method: HttpMethod;
11
+ readonly path: string;
12
+ }
13
+ /** Derive the route location from its file name relative to the routes directory. */
14
+ export declare function routeLocationFromFile(relativeRouteFile: string): RouteLocation | undefined;
15
+ /** Stable route id shared by facts and the project graph. */
16
+ export declare function routeFactId(route: Pick<RouteFact, "method" | "path">): string;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Route file name -> method/path (Nitro's file router, SPEC §4.2).
3
+ *
4
+ * Kept apart from the route pass so the filename rules are testable and the pass stays within
5
+ * the file-length limit. Two deliberate extensions over @facilis.dev/nitro's route-path: every
6
+ * HTTP method is recognized, and a method-less file serves ALL methods (Nitro's behavior).
7
+ */
8
+ const METHOD_SUFFIX = /\.(get|post|put|patch|delete|head|options|trace|connect)$/;
9
+ const ENV_SUFFIX = /\.(dev|prod|prerender)$/;
10
+ const SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
11
+ function toOpenApiSegment(segment) {
12
+ // Optional params `[[id]]` and catch-all `[...slug]` keep their special form; named
13
+ // params become OpenAPI templates, matching the spec Nitro generates.
14
+ const optional = /^\[\[(.+)\]\]$/.exec(segment);
15
+ if (optional?.[1] !== undefined)
16
+ return "{" + optional[1] + "}";
17
+ const catchAll = /^\[\.\.\.(.+)\]$/.exec(segment);
18
+ if (catchAll?.[1] !== undefined)
19
+ return "{*" + catchAll[1] + "}";
20
+ const named = /^\[(.+)\]$/.exec(segment);
21
+ if (named?.[1] !== undefined)
22
+ return "{" + named[1] + "}";
23
+ return segment.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
24
+ }
25
+ function parseRouteSegment(segment) {
26
+ let name = segment;
27
+ const env = name.match(ENV_SUFFIX);
28
+ if (env !== null)
29
+ name = name.slice(0, -env[0].length);
30
+ let method = "ALL";
31
+ const methodMatch = name.match(METHOD_SUFFIX);
32
+ if (methodMatch?.[1] !== undefined) {
33
+ method = methodMatch[1].toUpperCase();
34
+ name = name.slice(0, -methodMatch[0].length);
35
+ }
36
+ return name.length === 0 ? undefined : { name, method };
37
+ }
38
+ /** Derive the route location from its file name relative to the routes directory. */
39
+ export function routeLocationFromFile(relativeRouteFile) {
40
+ const normalized = relativeRouteFile.replace(/\\/g, "/").replace(SOURCE_EXTENSION, "");
41
+ const segments = normalized.split("/").filter(Boolean);
42
+ const last = segments.length - 1;
43
+ if (last < 0)
44
+ return undefined;
45
+ const parsed = parseRouteSegment(segments[last] ?? "");
46
+ if (parsed === undefined)
47
+ return undefined;
48
+ if (parsed.name === "index")
49
+ segments.pop();
50
+ else
51
+ segments[last] = parsed.name;
52
+ const path = segments.filter((segment) => !/^\(.+\)$/.test(segment)).map(toOpenApiSegment);
53
+ return { method: parsed.method, path: path.length === 0 ? "/" : "/" + path.join("/") };
54
+ }
55
+ /** Stable route id shared by facts and the project graph. */
56
+ export function routeFactId(route) {
57
+ return "route:" + route.method + " " + route.path;
58
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Name resolution for the route-relations pass (D-157). Split from route-relations.ts so both
3
+ * files stay under the 300-line limit.
4
+ *
5
+ * Two questions are answered here, without executing project code:
6
+ * - which relation (table or view) an expression denotes, following same-file consts, project
7
+ * imports, re-exports and namespace members;
8
+ * - which db client an expression denotes, so a use can be attributed to `ctx.db` or to one
9
+ * audited elevation callback.
10
+ *
11
+ * Anything outside the static subset is explicit: "opaque" means a package binding we cannot
12
+ * inspect (no error, the runtime guard is the authority), "unresolved" means dynamic project
13
+ * code (an error, D-157: route-table-unresolved).
14
+ */
15
+ import { type AstNode } from "../ast/node.js";
16
+ import type { ModuleGraph, ProjectFile } from "../module-graph.js";
17
+ import type { TableFact, ViewFact } from "../types.js";
18
+ export interface DbClient {
19
+ /** Elevation index in RouteRelationsFact.elevations; null is the route's ctx.db. */
20
+ readonly elevation: number | null;
21
+ }
22
+ export declare const ROUTE_CLIENT: DbClient;
23
+ export type ClientEnv = ReadonlyMap<string, DbClient>;
24
+ export type RelationResolution = {
25
+ readonly kind: "resolved";
26
+ readonly name: string;
27
+ } | {
28
+ readonly kind: "unresolved";
29
+ } | {
30
+ readonly kind: "opaque";
31
+ };
32
+ export interface RelationIndex {
33
+ readonly names: ReadonlySet<string>;
34
+ resolve(graph: ModuleGraph, file: ProjectFile, node: AstNode | undefined, aliases: ReadonlyMap<string, string>, depth?: number): RelationResolution;
35
+ }
36
+ /** Table and view bindings, keyed by declaring file + binding (D-157). */
37
+ export declare function createRelationIndex(tables: readonly TableFact[], views: readonly ViewFact[]): RelationIndex;
38
+ /** The db client an expression denotes directly (a local binding or `ctx.db`), if any. */
39
+ export declare function clientOf(node: unknown, env: ClientEnv, contextName: string | null): DbClient | null;
40
+ /** The client at the root of a fluent chain (`db.select()`, `db.query.todos`). */
41
+ export declare function chainClient(node: unknown, env: ClientEnv, contextName: string | null): DbClient | null;
42
+ /** True when a callee argument carries a db client anywhere (opaque callee detection). */
43
+ export declare function carriesClient(node: AstNode, env: ClientEnv, contextName: string | null): boolean;
44
+ export interface LocalFunction {
45
+ /** Stable identity for the call stack (cycle guard). */
46
+ readonly key: string;
47
+ readonly file: ProjectFile;
48
+ readonly node: AstNode;
49
+ }
50
+ /** A project-local function (never a package one: package calls stay opaque, D-157). */
51
+ export declare function resolveLocalFunction(graph: ModuleGraph, file: ProjectFile, callee: unknown): LocalFunction | null;