vouchington-tooling 0.33.0 → 0.35.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 (182) hide show
  1. package/README.md +181 -1
  2. package/dist/api-contract-discovery/index.d.mts +4 -1
  3. package/dist/api-contract-discovery/index.mjs +1 -0
  4. package/dist/api-contract-discovery/protocol-analysis-cache.d.mts +21 -0
  5. package/dist/api-contract-discovery/protocol-analysis-cache.mjs +7 -0
  6. package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +2 -1
  7. package/dist/api-contract-discovery/protocol-callback-invocation.mjs +12 -4
  8. package/dist/api-contract-discovery/protocol-callback-values.d.mts +2 -1
  9. package/dist/api-contract-discovery/protocol-callback-values.mjs +3 -3
  10. package/dist/api-contract-discovery/protocol-execution-path.d.mts +2 -1
  11. package/dist/api-contract-discovery/protocol-execution-path.mjs +2 -2
  12. package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +2 -1
  13. package/dist/api-contract-discovery/protocol-http-context-callers.mjs +13 -7
  14. package/dist/api-contract-discovery/query-contract-extraction.mjs +20 -3
  15. package/dist/api-contract-discovery/query-contract-literals.d.mts +5 -0
  16. package/dist/api-contract-discovery/query-contract-literals.mjs +29 -0
  17. package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +2 -1
  18. package/dist/api-contract-discovery/registered-route-binding-writes.mjs +20 -11
  19. package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +7 -1
  20. package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +27 -26
  21. package/dist/api-contract-discovery/registered-route-handler-helpers.d.mts +3 -0
  22. package/dist/api-contract-discovery/registered-route-handler-helpers.mjs +16 -0
  23. package/dist/api-contract-discovery/request-validation-callbacks.d.mts +9 -0
  24. package/dist/api-contract-discovery/request-validation-callbacks.mjs +27 -0
  25. package/dist/api-contract-discovery/request-validation-checker.d.mts +7 -0
  26. package/dist/api-contract-discovery/request-validation-checker.mjs +25 -0
  27. package/dist/api-contract-discovery/request-validation-conditional.d.mts +6 -0
  28. package/dist/api-contract-discovery/request-validation-conditional.mjs +31 -0
  29. package/dist/api-contract-discovery/request-validation-factory-calls.d.mts +11 -0
  30. package/dist/api-contract-discovery/request-validation-factory-calls.mjs +23 -0
  31. package/dist/api-contract-discovery/request-validation-facts.d.mts +16 -0
  32. package/dist/api-contract-discovery/request-validation-facts.mjs +64 -0
  33. package/dist/api-contract-discovery/request-validation-follow.d.mts +24 -0
  34. package/dist/api-contract-discovery/request-validation-follow.mjs +38 -0
  35. package/dist/api-contract-discovery/request-validation-handler-state.d.mts +8 -0
  36. package/dist/api-contract-discovery/request-validation-handler-state.mjs +35 -0
  37. package/dist/api-contract-discovery/request-validation-input.d.mts +12 -0
  38. package/dist/api-contract-discovery/request-validation-input.mjs +43 -0
  39. package/dist/api-contract-discovery/request-validation-keys.d.mts +25 -0
  40. package/dist/api-contract-discovery/request-validation-keys.mjs +119 -0
  41. package/dist/api-contract-discovery/request-validation-match.d.mts +11 -0
  42. package/dist/api-contract-discovery/request-validation-match.mjs +47 -0
  43. package/dist/api-contract-discovery/request-validation-mutation.d.mts +7 -0
  44. package/dist/api-contract-discovery/request-validation-mutation.mjs +80 -0
  45. package/dist/api-contract-discovery/request-validation-origin.d.mts +20 -0
  46. package/dist/api-contract-discovery/request-validation-origin.mjs +71 -0
  47. package/dist/api-contract-discovery/request-validation-reads.d.mts +13 -0
  48. package/dist/api-contract-discovery/request-validation-reads.mjs +88 -0
  49. package/dist/api-contract-discovery/request-validation-registration.d.mts +9 -0
  50. package/dist/api-contract-discovery/request-validation-registration.mjs +32 -0
  51. package/dist/api-contract-discovery/request-validation-report.d.mts +3 -0
  52. package/dist/api-contract-discovery/request-validation-report.mjs +21 -0
  53. package/dist/api-contract-discovery/request-validation-request-origin.d.mts +5 -0
  54. package/dist/api-contract-discovery/request-validation-request-origin.mjs +13 -0
  55. package/dist/api-contract-discovery/request-validation-select.d.mts +9 -0
  56. package/dist/api-contract-discovery/request-validation-select.mjs +36 -0
  57. package/dist/api-contract-discovery/request-validation-sites.d.mts +9 -0
  58. package/dist/api-contract-discovery/request-validation-sites.mjs +77 -0
  59. package/dist/api-contract-discovery/request-validation-state-key.d.mts +13 -0
  60. package/dist/api-contract-discovery/request-validation-state-key.mjs +30 -0
  61. package/dist/api-contract-discovery/request-validation-trace-helpers.d.mts +14 -0
  62. package/dist/api-contract-discovery/request-validation-trace-helpers.mjs +80 -0
  63. package/dist/api-contract-discovery/request-validation-trace-select.d.mts +23 -0
  64. package/dist/api-contract-discovery/request-validation-trace-select.mjs +45 -0
  65. package/dist/api-contract-discovery/request-validation-trace.d.mts +8 -0
  66. package/dist/api-contract-discovery/request-validation-trace.mjs +130 -0
  67. package/dist/api-contract-discovery/request-validation-types.d.mts +84 -0
  68. package/dist/api-contract-discovery/request-validation-types.mjs +1 -0
  69. package/dist/api-contract-discovery/request-validation-walker.d.mts +26 -0
  70. package/dist/api-contract-discovery/request-validation-walker.mjs +143 -0
  71. package/dist/api-contract-discovery/request-validation.test-helpers.d.mts +39 -0
  72. package/dist/api-contract-discovery/request-validation.test-helpers.mjs +108 -0
  73. package/dist/api-contract-discovery/response-contract-attribution-facts.d.mts +2 -0
  74. package/dist/api-contract-discovery/response-contract-attribution-facts.mjs +18 -0
  75. package/dist/api-contract-discovery/response-contract-attribution.d.mts +3 -0
  76. package/dist/api-contract-discovery/response-contract-attribution.mjs +23 -0
  77. package/dist/api-contract-discovery/response-contract-call-classification.d.mts +3 -0
  78. package/dist/api-contract-discovery/response-contract-call-classification.mjs +37 -0
  79. package/dist/api-contract-discovery/response-contract-error-branch.d.mts +1 -1
  80. package/dist/api-contract-discovery/response-contract-error-branch.mjs +109 -6
  81. package/dist/api-contract-discovery/response-contract-handler-bindings.d.mts +3 -0
  82. package/dist/api-contract-discovery/response-contract-handler-bindings.mjs +126 -0
  83. package/dist/api-contract-discovery/response-contract-handler-calls.d.mts +3 -0
  84. package/dist/api-contract-discovery/response-contract-handler-calls.mjs +81 -0
  85. package/dist/api-contract-discovery/response-contract-implicit-variants.d.mts +2 -0
  86. package/dist/api-contract-discovery/response-contract-implicit-variants.mjs +6 -0
  87. package/dist/api-contract-discovery/response-contract-implicit.d.mts +2 -1
  88. package/dist/api-contract-discovery/response-contract-implicit.mjs +9 -17
  89. package/dist/api-contract-discovery/response-contract-lenient.d.mts +18 -0
  90. package/dist/api-contract-discovery/response-contract-object-symbols.d.mts +3 -0
  91. package/dist/api-contract-discovery/response-contract-object-symbols.mjs +80 -0
  92. package/dist/api-contract-discovery/response-contract-property-mutations.d.mts +5 -0
  93. package/dist/api-contract-discovery/response-contract-property-mutations.mjs +84 -0
  94. package/dist/api-contract-discovery/response-contract-registration.d.mts +1 -1
  95. package/dist/api-contract-discovery/response-contract-registration.mjs +7 -3
  96. package/dist/api-contract-discovery/response-contract-registry.mjs +43 -8
  97. package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +3 -4
  98. package/dist/api-contract-discovery/response-contract-route-analysis.mjs +4 -101
  99. package/dist/api-contract-discovery/response-contract-route-syntax.d.mts +7 -0
  100. package/dist/api-contract-discovery/response-contract-route-syntax.mjs +41 -0
  101. package/dist/api-contract-discovery/response-contract-symbols.d.mts +5 -0
  102. package/dist/api-contract-discovery/response-contract-symbols.mjs +82 -0
  103. package/dist/compiler-build/index.d.mts +2 -0
  104. package/dist/compiler-build/index.mjs +1 -0
  105. package/dist/compiler-build/program-configuration.d.mts +9 -0
  106. package/dist/compiler-build/program-configuration.mjs +28 -0
  107. package/dist/compiler-build/program-types.d.mts +29 -0
  108. package/dist/compiler-build/program-types.mjs +1 -0
  109. package/dist/compiler-build/program.d.mts +7 -0
  110. package/dist/compiler-build/program.mjs +78 -0
  111. package/dist/contract-schema/index.d.mts +4 -0
  112. package/dist/contract-schema/index.mjs +2 -0
  113. package/dist/contract-schema/type-query-api.d.mts +44 -0
  114. package/dist/contract-schema/type-query-api.mjs +10 -0
  115. package/dist/contract-schema/type-query-call.d.mts +16 -0
  116. package/dist/contract-schema/type-query-call.mjs +52 -0
  117. package/dist/contract-schema/type-query-export.d.mts +10 -0
  118. package/dist/contract-schema/type-query-export.mjs +91 -0
  119. package/dist/contract-schema/type-query.d.mts +22 -0
  120. package/dist/contract-schema/type-query.mjs +39 -0
  121. package/dist/finite-enum-ripple/ast.d.mts +9 -0
  122. package/dist/finite-enum-ripple/ast.mjs +94 -0
  123. package/dist/finite-enum-ripple/check.d.mts +3 -0
  124. package/dist/finite-enum-ripple/check.mjs +62 -0
  125. package/dist/finite-enum-ripple/compare.d.mts +20 -0
  126. package/dist/finite-enum-ripple/compare.mjs +88 -0
  127. package/dist/finite-enum-ripple/component-paths.d.mts +8 -0
  128. package/dist/finite-enum-ripple/component-paths.mjs +30 -0
  129. package/dist/finite-enum-ripple/configured-object-mutations.d.mts +1 -0
  130. package/dist/finite-enum-ripple/configured-object-mutations.mjs +78 -0
  131. package/dist/finite-enum-ripple/create-page-jsx.d.mts +3 -0
  132. package/dist/finite-enum-ripple/create-page-jsx.mjs +28 -0
  133. package/dist/finite-enum-ripple/create-page-literals.d.mts +2 -0
  134. package/dist/finite-enum-ripple/create-page-literals.mjs +83 -0
  135. package/dist/finite-enum-ripple/create-page-object-visitor.d.mts +2 -0
  136. package/dist/finite-enum-ripple/create-page-object-visitor.mjs +57 -0
  137. package/dist/finite-enum-ripple/create-page-properties.d.mts +4 -0
  138. package/dist/finite-enum-ripple/create-page-properties.mjs +35 -0
  139. package/dist/finite-enum-ripple/index.d.mts +2 -0
  140. package/dist/finite-enum-ripple/index.mjs +1 -0
  141. package/dist/finite-enum-ripple/model.d.mts +78 -0
  142. package/dist/finite-enum-ripple/model.mjs +1 -0
  143. package/dist/finite-enum-ripple/parser-support.d.mts +11 -0
  144. package/dist/finite-enum-ripple/parser-support.mjs +99 -0
  145. package/dist/finite-enum-ripple/parsers.d.mts +32 -0
  146. package/dist/finite-enum-ripple/parsers.mjs +115 -0
  147. package/dist/finite-enum-ripple/pattern-captures.d.mts +2 -0
  148. package/dist/finite-enum-ripple/pattern-captures.mjs +45 -0
  149. package/dist/finite-enum-ripple/structured-route-check.d.mts +3 -0
  150. package/dist/finite-enum-ripple/structured-route-check.mjs +94 -0
  151. package/dist/finite-enum-ripple/types.d.mts +2 -0
  152. package/dist/finite-enum-ripple/types.mjs +60 -0
  153. package/dist/finite-enum-ripple/union-factory-check.d.mts +2 -0
  154. package/dist/finite-enum-ripple/union-factory-check.mjs +34 -0
  155. package/dist/finite-enum-ripple/union-types.d.mts +2 -0
  156. package/dist/finite-enum-ripple/union-types.mjs +129 -0
  157. package/dist/openapi-document/build-openapi-query.mjs +1 -1
  158. package/dist/openapi-document/openapi-types.d.mts +1 -1
  159. package/dist/openapi-document/query-types.d.mts +8 -2
  160. package/dist/post-publication-inventory/index.d.mts +5 -0
  161. package/dist/post-publication-inventory/index.mjs +2 -0
  162. package/dist/post-publication-inventory/readers/sql-executors.d.mts +3 -0
  163. package/dist/post-publication-inventory/readers/sql-executors.mjs +32 -0
  164. package/dist/post-publication-inventory/readers/sql-source.d.mts +7 -0
  165. package/dist/post-publication-inventory/readers/sql-source.mjs +87 -0
  166. package/dist/post-publication-inventory/readers/sql-template.d.mts +12 -0
  167. package/dist/post-publication-inventory/readers/sql-template.mjs +119 -0
  168. package/dist/post-publication-inventory/writers/source.d.mts +9 -0
  169. package/dist/post-publication-inventory/writers/source.mjs +128 -0
  170. package/dist/post-publication-inventory/writers/types.d.mts +19 -0
  171. package/dist/post-publication-inventory/writers/types.mjs +1 -0
  172. package/dist/workflow-policy/index.d.mts +1 -3
  173. package/dist/workflow-policy/index.mjs +0 -2
  174. package/dist/workflow-policy/types.d.mts +2 -28
  175. package/docs/workflow-and-compiler.md +7 -16
  176. package/package.json +13 -2
  177. package/dist/workflow-policy/concurrency-scope.d.mts +0 -4
  178. package/dist/workflow-policy/concurrency-scope.mjs +0 -66
  179. package/dist/workflow-policy/concurrency.d.mts +0 -10
  180. package/dist/workflow-policy/concurrency.mjs +0 -77
  181. package/dist/workflow-policy/graph.d.mts +0 -2
  182. package/dist/workflow-policy/graph.mjs +0 -138
@@ -0,0 +1,143 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { handlerBindings } from './request-validation-handler-state.mjs';
3
+ import { httpContextInvocations } from './protocol-http-context-callers.mjs';
4
+ import { potentiallyExecuted } from './protocol-executable-path.mjs';
5
+ import { executedCallbackFunctions, invokedUnconditionally, } from './request-validation-callbacks.mjs';
6
+ import { isConditionalPosition } from './request-validation-conditional.mjs';
7
+ import { followedImplementations, inlineFunction, } from './request-validation-follow.mjs';
8
+ import { calledFactory } from './request-validation-factory-calls.mjs';
9
+ import { findConfig } from './request-validation-match.mjs';
10
+ import { rawReadsAt } from './request-validation-reads.mjs';
11
+ import { factorySite, sourceOf, validatorSite } from './request-validation-sites.mjs';
12
+ import { createReporter } from './request-validation-report.mjs';
13
+ import { createRegistrationScanner } from './request-validation-registration.mjs';
14
+ import { createStateKey, emptyState, } from './request-validation-state-key.mjs';
15
+ import { bindArguments } from './request-validation-trace.mjs';
16
+ import { unwrapTransparentExpression } from './response-contract-route-syntax.mjs';
17
+ /** Walks handlers and the helpers they run, collecting validation facts for one route. */
18
+ export function createRouteWalker(config) {
19
+ const { checker, sourceFiles, validators, factories, executedCallbacks, protocolCache } = config;
20
+ const configured = [...validators, ...factories, ...executedCallbacks];
21
+ const facts = { validatorSites: [], factorySites: [], carrierReads: [] };
22
+ const report = createReporter();
23
+ const visited = new Map();
24
+ const scopeOf = (state) => ({
25
+ checker,
26
+ sourceFiles,
27
+ configured,
28
+ roots: state.roots,
29
+ keys: state.keys,
30
+ cache: protocolCache,
31
+ });
32
+ const stateKey = createStateKey();
33
+ const inputs = new Set();
34
+ const pendingReads = [];
35
+ const isInput = (node) => {
36
+ for (let current = node; current; current = current.parent)
37
+ if (inputs.has(current))
38
+ return true;
39
+ return false;
40
+ };
41
+ function walkFunction(fn, state) {
42
+ const seen = visited.get(fn) ?? new Set();
43
+ const key = stateKey(state);
44
+ if (seen.has(key))
45
+ return;
46
+ visited.set(fn, seen.add(key));
47
+ walk(fn.body, state);
48
+ }
49
+ /** Records a configured validator call; its input arguments are not raw reads. */
50
+ function reportValidator(call, state) {
51
+ const validator = findConfig(call.expression, validators, checker);
52
+ if (!validator)
53
+ return false;
54
+ const { site, inputNodes } = validatorSite(call, validator, scopeOf(state), state.conditional || isConditionalPosition(call));
55
+ inputNodes.forEach((node) => inputs.add(node));
56
+ const { conditional, ...identity } = site;
57
+ // A site reached both conditionally and unconditionally is unconditional.
58
+ report('validator', facts.validatorSites, site, call, identity).conditional &&= conditional;
59
+ return true;
60
+ }
61
+ /** Records a factory site once per call node; one reached both ways is unconditional. */
62
+ function recordFactory(call, factory, state, conditional) {
63
+ const site = factorySite(call, factory, scopeOf(state), conditional);
64
+ const { conditional: merged, ...identity } = site;
65
+ report('factory', facts.factorySites, site, call, identity).conditional &&= merged;
66
+ }
67
+ /** Records a configured factory call whose returned handler is a registration-time value. */
68
+ function reportFactory(call, state) {
69
+ const factory = findConfig(call.expression, factories, checker);
70
+ if (!factory)
71
+ return false;
72
+ recordFactory(call, factory, state, state.conditional || isConditionalPosition(call));
73
+ return true;
74
+ }
75
+ function visitCall(call, state) {
76
+ const conditional = state.conditional || isConditionalPosition(call);
77
+ // A handler built by a configured factory at module level and invoked here.
78
+ const built = calledFactory(call, factories, checker);
79
+ if (built)
80
+ recordFactory(built.call, built.config, emptyState, conditional);
81
+ const inline = inlineFunction(unwrapTransparentExpression(call.expression));
82
+ const callee = inline?.asteriskToken ? undefined : inline;
83
+ const via = new Set(state.via).add(call);
84
+ const runners = new Set(callee ? [callee] : []);
85
+ if (callee) {
86
+ const { keys, roots } = bindArguments(callee, call, scopeOf(state));
87
+ walkFunction(callee, { keys, roots, conditional, via });
88
+ }
89
+ for (const implementation of followedImplementations(call, scopeOf(state))) {
90
+ const { keys, roots } = bindArguments(implementation, call, scopeOf(state));
91
+ runners.add(implementation);
92
+ walkFunction(implementation, { keys, roots, conditional, via });
93
+ }
94
+ // Listed callbacks of a configured host take only the call's own condition.
95
+ for (const callback of executedCallbackFunctions(call, executedCallbacks, checker))
96
+ walkFunction(callback, { ...state, conditional });
97
+ for (const argument of call.arguments) {
98
+ const callback = unwrapTransparentExpression(argument);
99
+ if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))
100
+ continue;
101
+ const { callers } = httpContextInvocations(callback, checker, protocolCache);
102
+ // A helper that runs the callback under its own condition makes the sites conditional.
103
+ if (callers.length > 0)
104
+ walkFunction(callback, {
105
+ ...state,
106
+ conditional: conditional || !invokedUnconditionally(callers, runners),
107
+ });
108
+ }
109
+ }
110
+ function walk(node, state) {
111
+ if (!potentiallyExecuted(node))
112
+ return;
113
+ if (ts.isCallExpression(node))
114
+ reportValidator(node, state);
115
+ for (const read of rawReadsAt(node, scopeOf(state)))
116
+ pendingReads.push({ node, read: { ...read, source: sourceOf(node) }, via: state.via });
117
+ if (ts.isCallExpression(node))
118
+ visitCall(node, state);
119
+ ts.forEachChild(node, (child) => {
120
+ if (!ts.isFunctionLike(child))
121
+ walk(child, state);
122
+ });
123
+ }
124
+ const scanRegistration = createRegistrationScanner(scopeOf, reportFactory);
125
+ return {
126
+ /** The collected facts; reads that only build a validator's input are left out. */
127
+ get facts() {
128
+ for (const { node, read, via } of pendingReads)
129
+ if (!isInput(node) && ![...via].some(isInput))
130
+ report('read', facts.carrierReads, read);
131
+ pendingReads.length = 0;
132
+ return facts;
133
+ },
134
+ scanRegistration: (node) => scanRegistration(node),
135
+ walkHandler(proof) {
136
+ walkFunction(proof.node, {
137
+ ...handlerBindings(proof, checker),
138
+ conditional: proof.conditional ?? false,
139
+ via: emptyState.via,
140
+ });
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,39 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import type { ExecutedCallbackConfig, FactoryConfig, ValidatorConfig } from './request-validation-types.mts';
3
+ /** Synthetic library modules shared by the request-validation fixtures. */
4
+ export declare const librarySources: {
5
+ readonly 'lib/validation.ts': `
6
+ export function validateInput(ctx: any, operation: string, input?: any): boolean {
7
+ if (ctx.query.limit && input && operation) ctx.throw(400)
8
+ return true
9
+ }
10
+ export function validatePage(ctx: any, operation: string, options?: any): boolean {
11
+ if (ctx.query.limit && operation && options) ctx.throw(400)
12
+ return true
13
+ }
14
+ `;
15
+ readonly 'lib/factory.ts': `
16
+ import { validateInput } from './validation'
17
+ export function createThingHandler(options: any) {
18
+ return (ctx: any) => {
19
+ if (options.mode) validateInput(ctx, options.operation, { path: ctx.params })
20
+ ctx.status = 204
21
+ }
22
+ }
23
+ `;
24
+ readonly 'lib/reexport.ts': `export { validateInput as checkInput } from './validation'`;
25
+ readonly 'lib/keys.ts': `export const LIST_KEY = 'GET:/api/items'`;
26
+ };
27
+ export declare const validators: readonly ValidatorConfig[];
28
+ /** Builds a program whose files can import each other, relatively or through `@lib/*`. */
29
+ export declare function buildModuleProgram(files: Readonly<Record<string, string>>): {
30
+ program: ts.Program;
31
+ sourceFile(path: string): ts.SourceFile;
32
+ };
33
+ export type ModuleProgram = ReturnType<typeof buildModuleProgram>;
34
+ /** Runs discovery over the given files with the shared configuration. */
35
+ export declare function discover(built: ModuleProgram, paths: readonly string[], overrides?: {
36
+ validators?: readonly ValidatorConfig[];
37
+ factories?: readonly FactoryConfig[];
38
+ executedCallbacks?: readonly ExecutedCallbackConfig[];
39
+ }): Record<string, import("./request-validation-types.mts").RouteValidationFacts>;
@@ -0,0 +1,108 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { discoverRequestValidationFacts } from './request-validation-facts.mjs';
3
+ /** Synthetic library modules shared by the request-validation fixtures. */
4
+ export const librarySources = {
5
+ 'lib/validation.ts': `
6
+ export function validateInput(ctx: any, operation: string, input?: any): boolean {
7
+ if (ctx.query.limit && input && operation) ctx.throw(400)
8
+ return true
9
+ }
10
+ export function validatePage(ctx: any, operation: string, options?: any): boolean {
11
+ if (ctx.query.limit && operation && options) ctx.throw(400)
12
+ return true
13
+ }
14
+ `,
15
+ 'lib/factory.ts': `
16
+ import { validateInput } from './validation'
17
+ export function createThingHandler(options: any) {
18
+ return (ctx: any) => {
19
+ if (options.mode) validateInput(ctx, options.operation, { path: ctx.params })
20
+ ctx.status = 204
21
+ }
22
+ }
23
+ `,
24
+ 'lib/reexport.ts': `export { validateInput as checkInput } from './validation'`,
25
+ 'lib/keys.ts': `export const LIST_KEY = 'GET:/api/items'`,
26
+ };
27
+ export const validators = [
28
+ {
29
+ module: 'lib/validation.ts',
30
+ exportName: 'validateInput',
31
+ operationArgument: 1,
32
+ carriers: { kind: 'input-object', argument: 2 },
33
+ },
34
+ {
35
+ module: 'lib/validation',
36
+ exportName: 'validatePage',
37
+ operationArgument: 1,
38
+ carriers: {
39
+ kind: 'fixed',
40
+ carriers: ['query'],
41
+ optionCarriers: [{ argument: 2, property: 'path', carrier: 'path' }],
42
+ },
43
+ },
44
+ ];
45
+ const factories = [
46
+ {
47
+ module: 'lib/factory.ts',
48
+ exportName: 'createThingHandler',
49
+ optionsArgument: 0,
50
+ operationProperty: 'operation',
51
+ carriers: ['path', 'body'],
52
+ },
53
+ ];
54
+ const ROOT = '/virtual/';
55
+ /** Builds a program whose files can import each other, relatively or through `@lib/*`. */
56
+ export function buildModuleProgram(files) {
57
+ const sources = new Map(Object.entries(files).map(([path, text]) => [
58
+ `${ROOT}${path}`,
59
+ path.startsWith('global/') ? text : `${text}\nexport {}\n`,
60
+ ]));
61
+ const options = {
62
+ module: ts.ModuleKind.ESNext,
63
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
64
+ baseUrl: ROOT,
65
+ paths: { '@lib/*': ['lib/*'] },
66
+ skipLibCheck: true,
67
+ strict: true,
68
+ target: ts.ScriptTarget.ESNext,
69
+ };
70
+ const host = ts.createCompilerHost(options, true);
71
+ const getSourceFile = host.getSourceFile.bind(host);
72
+ host.getSourceFile = (requested, languageVersion, ...rest) => {
73
+ const text = sources.get(requested);
74
+ return text === undefined
75
+ ? getSourceFile(requested, languageVersion, ...rest)
76
+ : ts.createSourceFile(requested, text, languageVersion, true, ts.ScriptKind.TS);
77
+ };
78
+ host.fileExists = (requested) => sources.has(requested) || ts.sys.fileExists(requested);
79
+ host.directoryExists = (requested) => [...sources.keys()].some((path) => path.startsWith(`${requested.replace(/\/$/, '')}/`)) ||
80
+ !!ts.sys.directoryExists?.(requested);
81
+ host.readFile = (requested) => sources.get(requested) ?? ts.sys.readFile(requested);
82
+ const program = ts.createProgram([...sources.keys()], options, host);
83
+ return {
84
+ program,
85
+ sourceFile(path) {
86
+ const sourceFile = program.getSourceFile(`${ROOT}${path}`);
87
+ const diagnostics = [
88
+ ...program.getSyntacticDiagnostics(sourceFile),
89
+ ...program.getSemanticDiagnostics(sourceFile),
90
+ ];
91
+ if (diagnostics.length > 0)
92
+ throw new Error(diagnostics
93
+ .map((item) => ts.flattenDiagnosticMessageText(item.messageText, '\n'))
94
+ .join('\n'));
95
+ return sourceFile;
96
+ },
97
+ };
98
+ }
99
+ /** Runs discovery over the given files with the shared configuration. */
100
+ export function discover(built, paths, overrides = {}) {
101
+ return discoverRequestValidationFacts({
102
+ program: built.program,
103
+ sourceFiles: paths.map((path) => built.sourceFile(path)),
104
+ validators,
105
+ factories,
106
+ ...overrides,
107
+ });
108
+ }
@@ -0,0 +1,2 @@
1
+ import type { AmbiguousAttributionFact } from './response-contract-lenient.mts';
2
+ export declare function sortAttributionFacts(facts: readonly AmbiguousAttributionFact[]): AmbiguousAttributionFact[];
@@ -0,0 +1,18 @@
1
+ export function sortAttributionFacts(facts) {
2
+ return [...facts].toSorted((left, right) => {
3
+ const first = parseSourceLocation(left.sourceLocation);
4
+ const second = parseSourceLocation(right.sourceLocation);
5
+ if (first.path !== second.path)
6
+ return first.path < second.path ? -1 : 1;
7
+ return first.line - second.line || first.column - second.column;
8
+ });
9
+ }
10
+ function parseSourceLocation(location) {
11
+ const columnSeparator = location.lastIndexOf(':');
12
+ const lineSeparator = location.lastIndexOf(':', columnSeparator - 1);
13
+ return {
14
+ path: location.slice(0, lineSeparator),
15
+ line: Number(location.slice(lineSeparator + 1, columnSeparator)),
16
+ column: Number(location.slice(columnSeparator + 1)),
17
+ };
18
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import { type HandlerBindings, type AmbiguousHandlerBindings } from './response-contract-route-analysis.mts';
3
+ export declare function ambiguousRoutesForCall(node: ts.Node, checker: ts.TypeChecker, handlerBindings: HandlerBindings, ambiguousBindings: AmbiguousHandlerBindings): readonly string[] | undefined;
@@ -0,0 +1,23 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
3
+ import { attributionFunctionSymbol, attributionSymbol } from './response-contract-symbols.mjs';
4
+ import { isInlineRouteHandler } from './response-contract-route-syntax.mjs';
5
+ import {} from './response-contract-route-analysis.mjs';
6
+ export function ambiguousRoutesForCall(node, checker, handlerBindings, ambiguousBindings) {
7
+ let current = node;
8
+ while (current) {
9
+ if (isInlineRouteHandler(current))
10
+ return undefined;
11
+ const symbol = attributionFunctionSymbol(current, checker);
12
+ if (symbol) {
13
+ const resolved = attributionSymbol(symbol, checker);
14
+ const routes = ambiguousBindings.get(resolved);
15
+ if (routes)
16
+ return executableProtocolPath(node, checker, current) ? routes : undefined;
17
+ if (handlerBindings.has(resolved))
18
+ return undefined;
19
+ }
20
+ current = current.parent;
21
+ }
22
+ return undefined;
23
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ export type ImplicitResponseCallLabel = 'ctx.json()' | 'ctx.pipeline()' | 'ctx.response.xml()' | 'ctx.response.buffer()' | 'ctx.response.empty()';
3
+ export declare function implicitResponseCallLabel(call: ts.CallExpression, excludeDynamicErrorObjects?: boolean, checker?: ts.TypeChecker): ImplicitResponseCallLabel | undefined;
@@ -0,0 +1,37 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { isInErrorBranch } from './response-contract-error-branch.mjs';
3
+ import { isXmlResponseCall } from './response-contract-media.mjs';
4
+ import { containsResponseMarker } from './response-contract-registration.mjs';
5
+ import { isContextMethod, isContextResponseBufferCall, isContextResponseEmptyCall, } from './response-contract-route-analysis.mjs';
6
+ import { unwrapTransparentExpression } from './response-contract-route-syntax.mjs';
7
+ export function implicitResponseCallLabel(call, excludeDynamicErrorObjects = false, checker) {
8
+ if (isContextMethod(call.expression, 'json')) {
9
+ if (!call.arguments[0] || isInErrorBranch(call, excludeDynamicErrorObjects, checker))
10
+ return undefined;
11
+ return 'ctx.json()';
12
+ }
13
+ if (isStreamJsonPipeline(call, excludeDynamicErrorObjects))
14
+ return isInErrorBranch(call, excludeDynamicErrorObjects, checker) ? undefined : 'ctx.pipeline()';
15
+ if (excludeDynamicErrorObjects && isInErrorBranch(call, true, checker))
16
+ return undefined;
17
+ if (isXmlResponseCall(call))
18
+ return call.arguments[0] ? 'ctx.response.xml()' : undefined;
19
+ if (isContextResponseBufferCall(call.expression))
20
+ return 'ctx.response.buffer()';
21
+ if (isContextResponseEmptyCall(call.expression))
22
+ return 'ctx.response.empty()';
23
+ if (isContextMethod(call.expression, 'pipeline') && !containsResponseMarker(call))
24
+ return 'ctx.pipeline()';
25
+ return undefined;
26
+ }
27
+ function isStreamJsonPipeline(call, unwrapArguments) {
28
+ if (!isContextMethod(call.expression, 'pipeline'))
29
+ return false;
30
+ const originalBody = call.arguments[0];
31
+ const pipelineBody = originalBody && (unwrapArguments ? unwrapTransparentExpression(originalBody) : originalBody);
32
+ return (!!pipelineBody &&
33
+ ts.isCallExpression(pipelineBody) &&
34
+ ts.isIdentifier(pipelineBody.expression) &&
35
+ pipelineBody.expression.text === 'streamJsonObject' &&
36
+ !!pipelineBody.arguments[0]);
37
+ }
@@ -12,4 +12,4 @@ import ts from '../contract-schema/typescript-api.mts';
12
12
  * two independent, self-contained early-return guards, even though neither guard's error status
13
13
  * is in effect on the path that reaches the trailing statement.
14
14
  */
15
- export declare function isInErrorBranch(call: ts.CallExpression): boolean;
15
+ export declare function isInErrorBranch(call: ts.CallExpression, excludeDynamicErrorObjects?: boolean, checker?: ts.TypeChecker): boolean;
@@ -1,5 +1,8 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
2
  import { isContextMethod } from './response-contract-route-analysis.mjs';
3
+ import { potentiallyExecuted } from './protocol-executable-path.mjs';
4
+ import { unwrapTransparentExpression } from './response-contract-route-syntax.mjs';
5
+ import { responseBodyExpression } from './response-contract-registration.mjs';
3
6
  /**
4
7
  * Detects a hand-rolled early-return error guard: `ctx.setStatus(NNN >= 400)` followed by a
5
8
  * response body call in the same block (e.g. `auth-me.mts`'s `ctx.setStatus(401); return
@@ -13,11 +16,18 @@ import { isContextMethod } from './response-contract-route-analysis.mjs';
13
16
  * two independent, self-contained early-return guards, even though neither guard's error status
14
17
  * is in effect on the path that reaches the trailing statement.
15
18
  */
16
- export function isInErrorBranch(call) {
19
+ export function isInErrorBranch(call, excludeDynamicErrorObjects = false, checker) {
17
20
  let statement = call;
18
21
  while (!ts.isStatement(statement))
19
22
  statement = statement.parent;
20
- return precededByErrorStatus(statement);
23
+ if (!excludeDynamicErrorObjects)
24
+ return precededByErrorStatus(statement);
25
+ const latestStatus = latestStatusSetter(statement);
26
+ if (!latestStatus)
27
+ return false;
28
+ if (isBareErrorStatusStatement(latestStatus, true))
29
+ return true;
30
+ return isErrorObjectJson(call, checker) && isDynamicStatusStatement(latestStatus);
21
31
  }
22
32
  /**
23
33
  * Walks outward through enclosing blocks so a `ctx.setStatus(>=400)` set before the branch's own
@@ -27,21 +37,114 @@ export function isInErrorBranch(call) {
27
37
  * bodies nested one block deeper.
28
38
  */
29
39
  function precededByErrorStatus(statement) {
40
+ return precededByStatus(statement, isBareErrorStatusStatement);
41
+ }
42
+ function precededByStatus(statement, matches) {
30
43
  const block = statement.parent;
31
44
  if (!ts.isBlock(block))
32
45
  return false;
33
46
  const index = block.statements.indexOf(statement);
34
- if (block.statements.slice(0, index).some(isBareErrorStatusStatement))
47
+ if (block.statements.slice(0, index).some(matches))
35
48
  return true;
36
49
  const enclosing = block.parent;
37
- return ts.isStatement(enclosing) ? precededByErrorStatus(enclosing) : false;
50
+ return ts.isStatement(enclosing) ? precededByStatus(enclosing, matches) : false;
51
+ }
52
+ function latestStatusSetter(statement) {
53
+ const block = statement.parent;
54
+ if (!ts.isBlock(block))
55
+ return undefined;
56
+ const index = block.statements.indexOf(statement);
57
+ for (let previous = index - 1; previous >= 0; previous--) {
58
+ const candidate = block.statements[previous];
59
+ if (isStatusSetterStatement(candidate)) {
60
+ if (potentiallyExecuted(candidate))
61
+ return candidate;
62
+ continue;
63
+ }
64
+ if (mayConditionallySetStatus(candidate))
65
+ return undefined;
66
+ }
67
+ const enclosing = block.parent;
68
+ return ts.isStatement(enclosing) ? latestStatusSetter(enclosing) : undefined;
69
+ }
70
+ function mayConditionallySetStatus(statement) {
71
+ if (ts.isFunctionDeclaration(statement) ||
72
+ ts.isInterfaceDeclaration(statement) ||
73
+ ts.isTypeAliasDeclaration(statement) ||
74
+ ts.isImportDeclaration(statement))
75
+ return false;
76
+ let found = false;
77
+ const inspect = (node) => {
78
+ if (node !== statement && isNestedFunction(node))
79
+ return;
80
+ if (ts.isCallExpression(node) &&
81
+ isContextMethod(node.expression, 'setStatus') &&
82
+ potentiallyExecuted(node))
83
+ found = true;
84
+ ts.forEachChild(node, inspect);
85
+ };
86
+ inspect(statement);
87
+ return found;
88
+ }
89
+ function isNestedFunction(node) {
90
+ return (ts.isFunctionDeclaration(node) ||
91
+ ts.isFunctionExpression(node) ||
92
+ ts.isArrowFunction(node) ||
93
+ ts.isMethodDeclaration(node) ||
94
+ ts.isGetAccessorDeclaration(node) ||
95
+ ts.isSetAccessorDeclaration(node) ||
96
+ ts.isConstructorDeclaration(node));
97
+ }
98
+ function isStatusSetterStatement(statement) {
99
+ return (ts.isExpressionStatement(statement) &&
100
+ ts.isCallExpression(statement.expression) &&
101
+ isContextMethod(statement.expression.expression, 'setStatus'));
102
+ }
103
+ function isDynamicStatusStatement(statement) {
104
+ const status = statement.expression.arguments[0];
105
+ return !!status && !ts.isNumericLiteral(unwrapTransparentExpression(status));
106
+ }
107
+ function isErrorObjectJson(call, checker) {
108
+ const body = responseBodyExpression(call, true);
109
+ const errorObject = body && resolveConstObjectAlias(unwrapTransparentExpression(body), checker);
110
+ return (!!errorObject &&
111
+ ts.isObjectLiteralExpression(errorObject) &&
112
+ errorObject.properties.some((property) => {
113
+ if (ts.isSpreadAssignment(property) || ts.isMethodDeclaration(property))
114
+ return false;
115
+ // Every remaining object-literal member has a name; spread members were excluded above.
116
+ const name = property.name;
117
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name))
118
+ return name.text === 'error';
119
+ if (!ts.isComputedPropertyName(name))
120
+ return false;
121
+ const key = unwrapTransparentExpression(name.expression);
122
+ return ts.isStringLiteral(key) && key.text === 'error';
123
+ }));
124
+ }
125
+ function resolveConstObjectAlias(expression, checker, seen = new Set()) {
126
+ if (!checker || !ts.isIdentifier(expression))
127
+ return expression;
128
+ const symbol = checker.getSymbolAtLocation(expression);
129
+ if (!symbol || seen.has(symbol))
130
+ return expression;
131
+ seen.add(symbol);
132
+ const declaration = symbol.valueDeclaration;
133
+ if (!declaration ||
134
+ !ts.isVariableDeclaration(declaration) ||
135
+ !declaration.initializer ||
136
+ !ts.isVariableDeclarationList(declaration.parent) ||
137
+ !(declaration.parent.flags & ts.NodeFlags.Const))
138
+ return expression;
139
+ return resolveConstObjectAlias(unwrapTransparentExpression(declaration.initializer), checker, seen);
38
140
  }
39
- function isBareErrorStatusStatement(statement) {
141
+ function isBareErrorStatusStatement(statement, unwrapStatus = false) {
40
142
  if (!ts.isExpressionStatement(statement))
41
143
  return false;
42
144
  const expression = statement.expression;
43
145
  if (!ts.isCallExpression(expression) || !isContextMethod(expression.expression, 'setStatus'))
44
146
  return false;
45
147
  const statusArgument = expression.arguments[0];
46
- return (!!statusArgument && ts.isNumericLiteral(statusArgument) && Number(statusArgument.text) >= 400);
148
+ const status = statusArgument && (unwrapStatus ? unwrapTransparentExpression(statusArgument) : statusArgument);
149
+ return !!status && ts.isNumericLiteral(status) && Number(status.text) >= 400;
47
150
  }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ import type { AmbiguousHandlerBindings, HandlerBindings } from './response-contract-route-analysis.mts';
3
+ export declare function collectHandlerBindings(sourceFiles: readonly ts.SourceFile[], checker: ts.TypeChecker, ambiguousBindings?: AmbiguousHandlerBindings): HandlerBindings;