vouchington-tooling 0.29.0 → 0.31.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.
- package/README.md +69 -19
- package/bin/node-guard.mjs +9 -0
- package/bin/vouchington-mcp.mjs +20 -0
- package/bin/vouchington.mjs +18 -0
- package/dist/agent-blackboard/feedback-outbox-counts.d.mts +5 -0
- package/dist/agent-blackboard/feedback-outbox-counts.mjs +11 -0
- package/dist/agent-blackboard/feedback-outbox-directory.d.mts +2 -0
- package/dist/agent-blackboard/feedback-outbox-directory.mjs +16 -0
- package/dist/agent-blackboard/feedback-outbox.mjs +5 -7
- package/dist/agent-blackboard/index.d.mts +1 -0
- package/dist/agent-blackboard/index.mjs +1 -0
- package/dist/api-contract-discovery/header-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/protocol-body-status.d.mts +2 -0
- package/dist/api-contract-discovery/protocol-body-status.mjs +4 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-callback-argument-bindings.mjs +33 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-invocation.mjs +103 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-callback-mutations.mjs +42 -0
- package/dist/api-contract-discovery/protocol-callback-registration.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-callback-registration.mjs +27 -0
- package/dist/api-contract-discovery/protocol-callback-values.d.mts +17 -0
- package/dist/api-contract-discovery/protocol-callback-values.mjs +114 -0
- package/dist/api-contract-discovery/protocol-context-alias.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-context-alias.mjs +20 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-contract-extraction.mjs +87 -0
- package/dist/api-contract-discovery/protocol-contract-keys.d.mts +1 -0
- package/dist/api-contract-discovery/protocol-contract-keys.mjs +8 -0
- package/dist/api-contract-discovery/protocol-contract-registry.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-contract-registry.mjs +133 -0
- package/dist/api-contract-discovery/protocol-executable-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-executable-path.mjs +64 -0
- package/dist/api-contract-discovery/protocol-execution-path.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-execution-path.mjs +16 -0
- package/dist/api-contract-discovery/protocol-http-association.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-association.mjs +97 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-body-kinds.mjs +19 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-http-context-callers.mjs +34 -0
- package/dist/api-contract-discovery/protocol-http-context.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-http-context.mjs +108 -0
- package/dist/api-contract-discovery/protocol-http-emission.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-emission.mjs +76 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-http-implicit-taint.mjs +26 -0
- package/dist/api-contract-discovery/protocol-http-registration.d.mts +12 -0
- package/dist/api-contract-discovery/protocol-http-registration.mjs +53 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-marker-analysis.mjs +50 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-opaque-callback.mjs +107 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +72 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-platform-mutation-targets.mjs +89 -0
- package/dist/api-contract-discovery/protocol-platform-writes.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-platform-writes.mjs +92 -0
- package/dist/api-contract-discovery/protocol-requested-keys.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-requested-keys.mjs +45 -0
- package/dist/api-contract-discovery/protocol-sse-callers.d.mts +7 -0
- package/dist/api-contract-discovery/protocol-sse-callers.mjs +67 -0
- package/dist/api-contract-discovery/protocol-sse-emission.d.mts +11 -0
- package/dist/api-contract-discovery/protocol-sse-emission.mjs +61 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-feasible-status.mjs +24 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +70 -0
- package/dist/api-contract-discovery/protocol-sse-status.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-sse-status.mjs +84 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-access.mjs +42 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +6 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +66 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +36 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.d.mts +14 -0
- package/dist/api-contract-discovery/protocol-sse-write-resolution.mjs +79 -0
- package/dist/api-contract-discovery/protocol-status-dominance.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-status-dominance.mjs +35 -0
- package/dist/api-contract-discovery/protocol-write-receiver.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-write-receiver.mjs +79 -0
- package/dist/api-contract-discovery/query-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-binding-writes.mjs +30 -0
- package/dist/api-contract-discovery/registered-route-catalog.mjs +11 -7
- package/dist/api-contract-discovery/registered-route-factory-returns.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-factory-returns.mjs +29 -0
- package/dist/api-contract-discovery/registered-route-handler-analysis.d.mts +5 -1
- package/dist/api-contract-discovery/registered-route-handler-analysis.mjs +79 -35
- package/dist/api-contract-discovery/registered-route-runtime-parameters.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-runtime-parameters.mjs +5 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.d.mts +3 -0
- package/dist/api-contract-discovery/registered-route-terminal-error.mjs +98 -0
- package/dist/api-contract-discovery/request-contract-registry.mjs +2 -0
- package/dist/api-contract-discovery/response-contract-implicit.mjs +28 -10
- package/dist/api-contract-discovery/response-contract-registry.mjs +14 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-route-analysis.mjs +22 -4
- package/dist/api-contract-discovery/response-contract-status.d.mts +1 -1
- package/dist/api-contract-discovery/response-contract-status.mjs +10 -8
- package/dist/api-contract-discovery/response-contract-types.d.mts +7 -0
- package/dist/cli/commands/agent-blackboard-flags.d.mts +4 -0
- package/dist/cli/commands/agent-blackboard-flags.mjs +34 -0
- package/dist/cli/commands/agent-blackboard.mjs +27 -35
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.mjs +12 -15
- package/dist/cli/parse-forwarded-commands.d.mts +0 -3
- package/dist/cli/parse-forwarded-commands.mjs +0 -2
- package/dist/cli/parse.d.mts +0 -3
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +2 -6
- package/dist/contract-schema/contract-schema-bounded-array.d.mts +1 -3
- package/dist/contract-schema/contract-schema-bounded-array.mjs +5 -0
- package/dist/contract-schema/contract-schema-object-tuple.mjs +7 -5
- package/dist/contract-schema/contract-schema-type-extractor.mjs +13 -0
- package/dist/contract-schema/contract-schema-validator.mjs +2 -2
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -1
- package/dist/mcp-server/dispatch.d.mts +4 -1
- package/dist/mcp-server/dispatch.mjs +6 -4
- package/dist/mcp-server/handlers-outbox.mjs +2 -7
- package/dist/mcp-server/harness.test-helpers.d.mts +1 -0
- package/dist/mcp-server/harness.test-helpers.mjs +1 -1
- package/dist/mcp-server/main.d.mts +5 -0
- package/dist/mcp-server/main.mjs +8 -0
- package/dist/mcp-server/run.d.mts +2 -0
- package/dist/mcp-server/run.mjs +4 -1
- package/dist/mcp-server/sdk.d.mts +11 -2
- package/dist/mcp-server/sdk.mjs +37 -5
- package/dist/mcp-server/server.mjs +3 -2
- package/dist/mcp-server/tools.mjs +3 -2
- package/dist/mcp-server/worktree.d.mts +17 -10
- package/dist/mcp-server/worktree.mjs +48 -31
- package/dist/openapi-document/build-openapi-catalog-sse.d.mts +4 -0
- package/dist/openapi-document/build-openapi-catalog-sse.mjs +27 -0
- package/dist/openapi-document/build-openapi-document.mjs +5 -2
- package/dist/openapi-document/build-openapi-response.d.mts +2 -2
- package/dist/openapi-document/build-openapi-response.mjs +33 -3
- package/dist/openapi-document/build-openapi-sse-events.d.mts +11 -0
- package/dist/openapi-document/build-openapi-sse-events.mjs +46 -0
- package/dist/openapi-document/contract-schema-intersection-merge.mjs +11 -11
- package/dist/openapi-document/contract-schema-to-openapi.mjs +3 -3
- package/dist/openapi-document/openapi-route-helpers.mjs +1 -3
- package/dist/openapi-document/openapi-types.d.mts +3 -0
- package/dist/openapi-document/operation-types.d.mts +7 -0
- package/docs/mcp-server.md +83 -25
- package/package.json +11 -9
- package/scripts/build.mjs +2 -0
- package/skills/blackboard/SKILL.md +66 -4
- package/skills/manifest.json +19 -19
- package/dist/agent-harness-config/apply.d.mts +0 -4
- package/dist/agent-harness-config/apply.mjs +0 -72
- package/dist/agent-harness-config/cli.d.mts +0 -10
- package/dist/agent-harness-config/cli.mjs +0 -100
- package/dist/agent-harness-config/index.d.mts +0 -4
- package/dist/agent-harness-config/index.mjs +0 -3
- package/dist/agent-harness-config/merge-json.d.mts +0 -5
- package/dist/agent-harness-config/merge-json.mjs +0 -62
- package/dist/agent-harness-config/merge-toml.d.mts +0 -6
- package/dist/agent-harness-config/merge-toml.mjs +0 -132
- package/dist/agent-harness-config/paths.d.mts +0 -3
- package/dist/agent-harness-config/paths.mjs +0 -117
- package/dist/agent-harness-config/policy.d.mts +0 -14
- package/dist/agent-harness-config/policy.mjs +0 -51
- package/dist/agent-harness-config/prerequisites.d.mts +0 -2
- package/dist/agent-harness-config/prerequisites.mjs +0 -67
- package/dist/agent-harness-config/toml-value.d.mts +0 -6
- package/dist/agent-harness-config/toml-value.mjs +0 -69
- package/dist/agent-harness-config/types.d.mts +0 -90
- package/dist/agent-harness-config/types.mjs +0 -2
- package/docs/agent-harness-config.md +0 -134
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
2
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
3
|
+
import { unwrapExpression } from './protocol-marker-analysis.mjs';
|
|
4
|
+
const standardLibrary = dirname(resolvePath(ts.getDefaultLibFilePath({})));
|
|
5
|
+
function isStandardDeclaration(declaration, libraries) {
|
|
6
|
+
const source = declaration.getSourceFile();
|
|
7
|
+
const file = resolvePath(source.fileName);
|
|
8
|
+
return libraries
|
|
9
|
+
? libraries.has(source)
|
|
10
|
+
: source.isDeclarationFile &&
|
|
11
|
+
dirname(file) === standardLibrary &&
|
|
12
|
+
/^lib(?:\.[\w.-]+)?\.d\.ts$/.test(basename(file));
|
|
13
|
+
}
|
|
14
|
+
export function literalStrings(expression, checker) {
|
|
15
|
+
if (!expression)
|
|
16
|
+
return undefined;
|
|
17
|
+
const strings = new Set();
|
|
18
|
+
function visit(type) {
|
|
19
|
+
if (type.isUnion())
|
|
20
|
+
return type.types.every(visit);
|
|
21
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
22
|
+
strings.add(type.value);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (type.flags &
|
|
26
|
+
(ts.TypeFlags.String | ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter))
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return visit(checker.getTypeAtLocation(expression)) ? [...strings] : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function globalTarget(expression, checker, seen = new Set()) {
|
|
33
|
+
const value = unwrapExpression(expression);
|
|
34
|
+
if (!ts.isIdentifier(value))
|
|
35
|
+
return undefined;
|
|
36
|
+
const symbol = checker.getSymbolAtLocation(value);
|
|
37
|
+
if (!symbol || seen.has(symbol))
|
|
38
|
+
return undefined;
|
|
39
|
+
seen.add(symbol);
|
|
40
|
+
if (value.text === 'globalThis')
|
|
41
|
+
return symbol.declarations?.length ? undefined : value;
|
|
42
|
+
if (symbol.name === 'global' &&
|
|
43
|
+
symbol.declarations?.some((declaration) => {
|
|
44
|
+
const source = declaration.getSourceFile();
|
|
45
|
+
return (ts.isVariableDeclaration(declaration) &&
|
|
46
|
+
source.isDeclarationFile &&
|
|
47
|
+
resolvePath(source.fileName)
|
|
48
|
+
.replaceAll('\\', '/')
|
|
49
|
+
.endsWith('/node_modules/@types/node/globals.d.ts'));
|
|
50
|
+
}))
|
|
51
|
+
return value;
|
|
52
|
+
const declaration = symbol.valueDeclaration;
|
|
53
|
+
return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer
|
|
54
|
+
? globalTarget(declaration.initializer, checker, seen)
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
export function mutatorKind(call, checker, libraries) {
|
|
58
|
+
const callee = call.expression;
|
|
59
|
+
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression))
|
|
60
|
+
return undefined;
|
|
61
|
+
const owner = callee.expression.text;
|
|
62
|
+
const method = callee.name.text;
|
|
63
|
+
if (!((owner === 'Object' && ['defineProperty', 'assign'].includes(method)) ||
|
|
64
|
+
(owner === 'Reflect' && method === 'set')))
|
|
65
|
+
return undefined;
|
|
66
|
+
const ownerSymbol = checker.getSymbolAtLocation(callee.expression);
|
|
67
|
+
const methodSymbol = checker.getSymbolAtLocation(callee.name);
|
|
68
|
+
if (!ownerSymbol?.declarations?.length ||
|
|
69
|
+
!methodSymbol?.declarations?.length ||
|
|
70
|
+
!ownerSymbol.declarations.every((declaration) => isStandardDeclaration(declaration, libraries)) ||
|
|
71
|
+
!methodSymbol.declarations.every((declaration) => isStandardDeclaration(declaration, libraries)))
|
|
72
|
+
return undefined;
|
|
73
|
+
return method;
|
|
74
|
+
}
|
|
75
|
+
export function sourceKeys(source, checker) {
|
|
76
|
+
const keys = new Set();
|
|
77
|
+
function visit(type) {
|
|
78
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter))
|
|
79
|
+
return false;
|
|
80
|
+
if (type.isUnion())
|
|
81
|
+
return type.types.every(visit);
|
|
82
|
+
if (type.getStringIndexType())
|
|
83
|
+
return false;
|
|
84
|
+
for (const property of type.getProperties())
|
|
85
|
+
keys.add(property.name);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return visit(checker.getTypeAtLocation(unwrapExpression(source))) ? [...keys] : undefined;
|
|
89
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
/** Records global timer and Promise writes, including calls to standard property mutators. */
|
|
3
|
+
export declare function writtenPlatformSymbols(files: readonly ts.SourceFile[], checker: ts.TypeChecker, libraries?: ReadonlySet<ts.SourceFile>): Set<ts.Symbol>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { unwrapExpression } from './protocol-marker-analysis.mjs';
|
|
3
|
+
import { globalTarget, literalStrings, mutatorKind, sourceKeys, } from './protocol-platform-mutation-targets.mjs';
|
|
4
|
+
const platformNames = ['setTimeout', 'setInterval', 'Promise'];
|
|
5
|
+
/** Records global timer and Promise writes, including calls to standard property mutators. */
|
|
6
|
+
export function writtenPlatformSymbols(files, checker, libraries) {
|
|
7
|
+
const written = new Set();
|
|
8
|
+
function mark(target, keys) {
|
|
9
|
+
const root = globalTarget(target, checker);
|
|
10
|
+
if (!root)
|
|
11
|
+
return;
|
|
12
|
+
const members = keys
|
|
13
|
+
? keys.map((key) => checker.getPropertyOfType(checker.getTypeAtLocation(root), key))
|
|
14
|
+
: platformNames.map((name) => checker.getPropertyOfType(checker.getTypeAtLocation(root), name));
|
|
15
|
+
for (const member of members)
|
|
16
|
+
if (member && platformNames.includes(member.name))
|
|
17
|
+
written.add(member);
|
|
18
|
+
}
|
|
19
|
+
function target(node) {
|
|
20
|
+
if (ts.isElementAccessExpression(node)) {
|
|
21
|
+
const keys = literalStrings(node.argumentExpression, checker);
|
|
22
|
+
mark(node.expression, keys);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
26
|
+
const symbol = checker.getSymbolAtLocation(node.name);
|
|
27
|
+
if (globalTarget(node.expression, checker) && symbol && platformNames.includes(symbol.name))
|
|
28
|
+
written.add(symbol);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
32
|
+
if (symbol && platformNames.includes(symbol.name))
|
|
33
|
+
written.add(symbol);
|
|
34
|
+
if (ts.isObjectLiteralExpression(node) ||
|
|
35
|
+
ts.isArrayLiteralExpression(node) ||
|
|
36
|
+
ts.isPropertyAssignment(node) ||
|
|
37
|
+
ts.isSpreadAssignment(node) ||
|
|
38
|
+
ts.isSpreadElement(node))
|
|
39
|
+
ts.forEachChild(node, target);
|
|
40
|
+
}
|
|
41
|
+
function visit(node) {
|
|
42
|
+
if (ts.isBinaryExpression(node) &&
|
|
43
|
+
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
44
|
+
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
45
|
+
target(node.left);
|
|
46
|
+
if (ts.isDeleteExpression(node))
|
|
47
|
+
target(node.expression);
|
|
48
|
+
if (ts.isCallExpression(node)) {
|
|
49
|
+
const kind = mutatorKind(node, checker, libraries);
|
|
50
|
+
if (kind && node.arguments[0] && ts.isSpreadElement(node.arguments[0]))
|
|
51
|
+
for (const file of files)
|
|
52
|
+
for (const binding of checker.getSymbolsInScope(file, ts.SymbolFlags.Value | ts.SymbolFlags.Alias)) {
|
|
53
|
+
const symbol = binding.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(binding) : binding;
|
|
54
|
+
if (platformNames.includes(symbol.name))
|
|
55
|
+
written.add(symbol);
|
|
56
|
+
}
|
|
57
|
+
if (kind === 'defineProperty' || kind === 'set') {
|
|
58
|
+
const receiver = node.arguments[0];
|
|
59
|
+
if (receiver && !ts.isSpreadElement(receiver))
|
|
60
|
+
mark(receiver, literalStrings(node.arguments[1] && !ts.isSpreadElement(node.arguments[1])
|
|
61
|
+
? node.arguments[1]
|
|
62
|
+
: undefined, checker));
|
|
63
|
+
}
|
|
64
|
+
else if (kind === 'assign') {
|
|
65
|
+
const receiver = node.arguments[0];
|
|
66
|
+
if (receiver && !ts.isSpreadElement(receiver) && globalTarget(receiver, checker)) {
|
|
67
|
+
for (const source of node.arguments.slice(1)) {
|
|
68
|
+
if (ts.isSpreadElement(source)) {
|
|
69
|
+
const values = unwrapExpression(source.expression);
|
|
70
|
+
if (ts.isArrayLiteralExpression(values)) {
|
|
71
|
+
for (const value of values.elements)
|
|
72
|
+
if (ts.isSpreadElement(value))
|
|
73
|
+
mark(receiver, undefined);
|
|
74
|
+
else
|
|
75
|
+
mark(receiver, sourceKeys(value, checker));
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
mark(receiver, undefined);
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
mark(receiver, sourceKeys(source, checker));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
}
|
|
88
|
+
for (const file of files)
|
|
89
|
+
if (!file.isDeclarationFile)
|
|
90
|
+
visit(file);
|
|
91
|
+
return written;
|
|
92
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RouteBinding } from './response-contract-route-analysis.mts';
|
|
2
|
+
/** Whether requested protocol keys include a route binding, independent of its protocol variants. */
|
|
3
|
+
export declare function protocolBindingRequested(binding: RouteBinding, requestedKeys?: ReadonlySet<string>): boolean;
|
|
4
|
+
/** Maps one generated protocol key to its requested route spelling and exact variant suffix. */
|
|
5
|
+
export declare function requestedProtocolKey(key: string, binding: RouteBinding, requestedKeys?: ReadonlySet<string>): string | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { routeShape } from './registered-route-catalog.mjs';
|
|
2
|
+
/** Whether requested protocol keys include a route binding, independent of its protocol variants. */
|
|
3
|
+
export function protocolBindingRequested(binding, requestedKeys) {
|
|
4
|
+
if (!requestedKeys)
|
|
5
|
+
return true;
|
|
6
|
+
return [...requestedKeys].some((key) => {
|
|
7
|
+
const requested = parseProtocolKey(key);
|
|
8
|
+
return requested && sameRoute(requested, binding);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
/** Maps one generated protocol key to its requested route spelling and exact variant suffix. */
|
|
12
|
+
export function requestedProtocolKey(key, binding, requestedKeys) {
|
|
13
|
+
if (!requestedKeys)
|
|
14
|
+
return key;
|
|
15
|
+
if (requestedKeys.size === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
const generated = parseProtocolKey(key);
|
|
18
|
+
if (!generated || !sameRoute(generated, binding))
|
|
19
|
+
return undefined;
|
|
20
|
+
const candidates = [];
|
|
21
|
+
for (const requestedKey of requestedKeys) {
|
|
22
|
+
const parsed = parseProtocolKey(requestedKey);
|
|
23
|
+
if (parsed?.suffix === generated.suffix && sameRoute(parsed, binding))
|
|
24
|
+
candidates.push({ key: requestedKey, parsed });
|
|
25
|
+
}
|
|
26
|
+
const exact = candidates.find((candidate) => candidate.parsed.base === generated.base);
|
|
27
|
+
return (exact ?? candidates.toSorted((left, right) => left.key.localeCompare(right.key))[0])?.key;
|
|
28
|
+
}
|
|
29
|
+
function parseProtocolKey(key) {
|
|
30
|
+
const suffixStart = key.indexOf('#');
|
|
31
|
+
const base = suffixStart < 0 ? key : key.slice(0, suffixStart);
|
|
32
|
+
const separator = base.indexOf(':');
|
|
33
|
+
if (separator <= 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
return {
|
|
36
|
+
method: base.slice(0, separator),
|
|
37
|
+
routeTemplate: base.slice(separator + 1),
|
|
38
|
+
suffix: suffixStart < 0 ? '' : key.slice(suffixStart),
|
|
39
|
+
base,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sameRoute(key, binding) {
|
|
43
|
+
return (key.method === binding.method &&
|
|
44
|
+
routeShape(key.routeTemplate) === routeShape(binding.routeTemplate));
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
export declare function sseCallerScope(functions: readonly ts.FunctionLikeDeclaration[], calls: readonly ts.CallExpression[], binding: RouteBinding, bindings: HandlerBindings, checker: ts.TypeChecker): {
|
|
4
|
+
paths: (ts.CallExpression | ts.NewExpression)[][];
|
|
5
|
+
contexts: Set<ts.Symbol>;
|
|
6
|
+
functions: ts.FunctionLikeDeclaration[];
|
|
7
|
+
} | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { createProtocolCallbackValueResolver } from './protocol-callback-values.mjs';
|
|
3
|
+
import { registeredHandler } from './protocol-callback-registration.mjs';
|
|
4
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
5
|
+
import { enclosingFunction } from './protocol-marker-analysis.mjs';
|
|
6
|
+
import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
|
|
7
|
+
import { expressionReceiver } from './protocol-write-receiver.mjs';
|
|
8
|
+
import { enclosingRouteBinding, } from './response-contract-route-analysis.mjs';
|
|
9
|
+
export function sseCallerScope(functions, calls, binding, bindings, checker) {
|
|
10
|
+
const resolver = createProtocolCallbackValueResolver(checker);
|
|
11
|
+
const matches = calls.flatMap((call) => {
|
|
12
|
+
const fn = resolver.resolve(call.expression, new Map())?.node;
|
|
13
|
+
if (!fn ||
|
|
14
|
+
!functions.includes(fn) ||
|
|
15
|
+
!executableProtocolPath(call, checker))
|
|
16
|
+
return [];
|
|
17
|
+
const route = enclosingRouteBinding(call, checker, bindings, false);
|
|
18
|
+
return route?.method === binding.method && route.routeTemplate === binding.routeTemplate
|
|
19
|
+
? [{ call, fn: fn }]
|
|
20
|
+
: [];
|
|
21
|
+
});
|
|
22
|
+
if (!matches.length)
|
|
23
|
+
return undefined;
|
|
24
|
+
const contexts = new Set();
|
|
25
|
+
const callerFunctions = new Set();
|
|
26
|
+
const paths = matches.map(({ call }) => {
|
|
27
|
+
const anchors = [call];
|
|
28
|
+
let fn = enclosingFunction(call);
|
|
29
|
+
let outer = fn;
|
|
30
|
+
while (fn) {
|
|
31
|
+
callerFunctions.add(fn);
|
|
32
|
+
let value = fn;
|
|
33
|
+
while (ts.isPropertyAssignment(value.parent) || ts.isObjectLiteralExpression(value.parent))
|
|
34
|
+
value = value.parent;
|
|
35
|
+
if (ts.isCallExpression(value.parent) || ts.isNewExpression(value.parent))
|
|
36
|
+
anchors.push(value.parent);
|
|
37
|
+
outer = fn;
|
|
38
|
+
fn = enclosingFunction(fn);
|
|
39
|
+
}
|
|
40
|
+
const name = outer && runtimeParameters(outer)[0]?.name;
|
|
41
|
+
const context = name && ts.isIdentifier(name) ? checker.getSymbolAtLocation(name) : undefined;
|
|
42
|
+
if (!outer ||
|
|
43
|
+
(name && !context) ||
|
|
44
|
+
!calls.some((node) => registeredHandler(node) &&
|
|
45
|
+
node.arguments.some((argument) => resolver.resolve(argument, new Map())?.node === outer)))
|
|
46
|
+
throw new Error('SSE helper must bind a registered caller context');
|
|
47
|
+
if (context)
|
|
48
|
+
contexts.add(context);
|
|
49
|
+
return anchors;
|
|
50
|
+
});
|
|
51
|
+
if (contexts.size > 1)
|
|
52
|
+
throw new Error('SSE helper has ambiguous caller contexts');
|
|
53
|
+
const context = [...contexts][0];
|
|
54
|
+
for (const { call, fn } of matches)
|
|
55
|
+
runtimeParameters(fn).forEach((parameter, index) => {
|
|
56
|
+
const argument = call.arguments[index];
|
|
57
|
+
const receiver = argument && expressionReceiver(argument, checker);
|
|
58
|
+
if (context &&
|
|
59
|
+
receiver?.root === context &&
|
|
60
|
+
receiver.path.length === 0 &&
|
|
61
|
+
!receiver.mutableAlias &&
|
|
62
|
+
ts.isIdentifier(parameter.name)) {
|
|
63
|
+
contexts.add(checker.getSymbolAtLocation(parameter.name));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return { paths, contexts, functions: [...callerFunctions] };
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import type { HandlerBindings, RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
export declare function sseEmission(call: ts.CallExpression, checker: ts.TypeChecker, calls: readonly ts.CallExpression[], binding: RouteBinding, bindings: HandlerBindings): {
|
|
4
|
+
write: ts.CallExpression;
|
|
5
|
+
receiver: import("./protocol-write-receiver.mts").WriteReceiver;
|
|
6
|
+
status: {
|
|
7
|
+
statusCodes?: readonly [number, ...number[]];
|
|
8
|
+
statusKnowledge: 'default' | 'explicit' | 'unknown';
|
|
9
|
+
unavailableReason?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
|
|
3
|
+
import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
|
|
4
|
+
import { potentiallyExecuted } from './protocol-executable-path.mjs';
|
|
5
|
+
import { enclosingFunction } from './protocol-marker-analysis.mjs';
|
|
6
|
+
import { sseCallerScope } from './protocol-sse-callers.mjs';
|
|
7
|
+
import { resolveSseStatus } from './protocol-sse-status.mjs';
|
|
8
|
+
import { writeReceiver } from './protocol-write-receiver.mjs';
|
|
9
|
+
export function sseEmission(call, checker, calls, binding, bindings) {
|
|
10
|
+
const write = call.parent;
|
|
11
|
+
if (!ts.isCallExpression(write) ||
|
|
12
|
+
!ts.isPropertyAccessExpression(write.expression) ||
|
|
13
|
+
write.expression.name.text !== 'write' ||
|
|
14
|
+
write.arguments[0] !== call)
|
|
15
|
+
throw new Error('apiSseFrame must be the frame written by its route');
|
|
16
|
+
const receiver = writeReceiver(write, checker);
|
|
17
|
+
if (!receiver)
|
|
18
|
+
throw new Error('SSE frame requires a bound stream receiver');
|
|
19
|
+
if (receiver.mutableAlias)
|
|
20
|
+
throw new Error('SSE frame uses a mutable stream alias');
|
|
21
|
+
let fn = enclosingFunction(call);
|
|
22
|
+
let context;
|
|
23
|
+
const callbacks = [];
|
|
24
|
+
const functions = [];
|
|
25
|
+
while (fn) {
|
|
26
|
+
functions.push(fn);
|
|
27
|
+
if (!potentiallyExecuted(fn) || !potentiallyExecuted(call))
|
|
28
|
+
throw new Error('SSE frame is not on an executable callback path');
|
|
29
|
+
if (fn.asteriskToken)
|
|
30
|
+
throw new Error('SSE frame is in a deferred generator');
|
|
31
|
+
if (!isSupportedProtocolCallback(fn, checker))
|
|
32
|
+
throw new Error('SSE frame is in an uninvoked callback');
|
|
33
|
+
if (ts.isFunctionDeclaration(fn)) {
|
|
34
|
+
const name = runtimeParameters(fn)[0]?.name;
|
|
35
|
+
if (name && ts.isIdentifier(name))
|
|
36
|
+
context = checker.getSymbolAtLocation(name);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
let value = fn;
|
|
40
|
+
while (ts.isPropertyAssignment(value.parent) || ts.isObjectLiteralExpression(value.parent))
|
|
41
|
+
value = value.parent;
|
|
42
|
+
if (ts.isCallExpression(value.parent) || ts.isNewExpression(value.parent))
|
|
43
|
+
callbacks.push(value.parent);
|
|
44
|
+
if ((ts.isCallExpression(value.parent) &&
|
|
45
|
+
ts.isPropertyAccessExpression(value.parent.expression) &&
|
|
46
|
+
['get', 'post', 'put', 'patch', 'delete'].includes(value.parent.expression.name.text)) ||
|
|
47
|
+
(ts.isVariableDeclaration(fn.parent) && !enclosingFunction(fn))) {
|
|
48
|
+
const name = runtimeParameters(fn)[0]?.name;
|
|
49
|
+
if (name && ts.isIdentifier(name))
|
|
50
|
+
context = checker.getSymbolAtLocation(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
fn = enclosingFunction(fn);
|
|
54
|
+
}
|
|
55
|
+
const caller = sseCallerScope(functions, calls, binding, bindings, checker);
|
|
56
|
+
const anchors = [write, ...callbacks];
|
|
57
|
+
const paths = caller ? caller.paths.map((path) => [...anchors, ...path]) : [anchors];
|
|
58
|
+
const contexts = caller?.contexts ?? new Set(context ? [context] : []);
|
|
59
|
+
const status = resolveSseStatus(paths, [...new Set([...functions, ...(caller?.functions ?? [])])], contexts, checker);
|
|
60
|
+
return { write, receiver, status };
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
/** Excludes a sibling branch or a statement that cannot precede this anchor. */
|
|
3
|
+
export function statusCanPrecede(setter, anchor) {
|
|
4
|
+
const ancestors = new Set();
|
|
5
|
+
for (let node = anchor; node; node = node.parent)
|
|
6
|
+
ancestors.add(node);
|
|
7
|
+
for (let node = setter; node.parent; node = node.parent) {
|
|
8
|
+
const parent = node.parent;
|
|
9
|
+
if (ts.isIfStatement(parent) && parent.elseStatement) {
|
|
10
|
+
const opposite = parent.thenStatement === node ? parent.elseStatement : parent.thenStatement;
|
|
11
|
+
if (ancestors.has(opposite))
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (ts.isBlock(parent) && ancestors.has(parent)) {
|
|
15
|
+
let statement = anchor;
|
|
16
|
+
while (statement.parent !== parent)
|
|
17
|
+
statement = statement.parent;
|
|
18
|
+
if (parent.statements.indexOf(node) >
|
|
19
|
+
parent.statements.indexOf(statement))
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
import { type WriteReceiver } from './protocol-write-receiver.mts';
|
|
4
|
+
export type SseRouteWrites = {
|
|
5
|
+
receivers: WriteReceiver[];
|
|
6
|
+
keys: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Reports unmarked writes on executable or opaque escaping paths to a selected frame's stream. */
|
|
9
|
+
export declare function rejectRawSseWrites(files: readonly ts.SourceFile[], checker: ts.TypeChecker, bindings: HandlerBindings, framedWrites: ReadonlySet<ts.CallExpression>, routes: ReadonlyMap<string, SseRouteWrites>, reject: (node: ts.CallExpression, binding: RouteBinding, keys: readonly string[]) => void): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { potentiallyExecuted } from './protocol-executable-path.mjs';
|
|
3
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
4
|
+
import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
|
|
5
|
+
import { enclosingRouteBinding, visit, } from './response-contract-route-analysis.mjs';
|
|
6
|
+
import { expressionReceiver, sameWriteReceiver, } from './protocol-write-receiver.mjs';
|
|
7
|
+
import { sseWriteInvocation } from './protocol-sse-write-access.mjs';
|
|
8
|
+
import { isSourceLevelMutation, mutationAffectsSelectedStream, sseWriteMutation, } from './protocol-sse-write-mutations.mjs';
|
|
9
|
+
import { actualReceivers, helperBindings, routeKey } from './protocol-sse-write-helpers.mjs';
|
|
10
|
+
/** Reports unmarked writes on executable or opaque escaping paths to a selected frame's stream. */
|
|
11
|
+
export function rejectRawSseWrites(files, checker, bindings, framedWrites, routes, reject) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const mutations = [];
|
|
14
|
+
for (const file of files)
|
|
15
|
+
visit(file, (node) => {
|
|
16
|
+
if (ts.isCallExpression(node))
|
|
17
|
+
calls.push(node);
|
|
18
|
+
const mutation = sseWriteMutation(node);
|
|
19
|
+
if (mutation)
|
|
20
|
+
mutations.push(mutation);
|
|
21
|
+
});
|
|
22
|
+
for (const node of calls) {
|
|
23
|
+
if (framedWrites.has(node)) {
|
|
24
|
+
const binding = enclosingRouteBinding(node, checker, bindings, false);
|
|
25
|
+
const route = binding && routes.get(routeKey(binding));
|
|
26
|
+
const source = node.getSourceFile();
|
|
27
|
+
if (binding &&
|
|
28
|
+
route &&
|
|
29
|
+
mutationAffectsSelectedStream(mutations, route.receivers, checker, (mutationNode) => {
|
|
30
|
+
if (mutationNode.getSourceFile() === source && isSourceLevelMutation(mutationNode))
|
|
31
|
+
return true;
|
|
32
|
+
const candidate = enclosingRouteBinding(mutationNode, checker, bindings, false);
|
|
33
|
+
return !!candidate && routeKey(candidate) === routeKey(binding);
|
|
34
|
+
}, (expression) => {
|
|
35
|
+
const receiver = expressionReceiver(expression, checker);
|
|
36
|
+
return receiver && actualReceivers(receiver, binding, calls, checker, bindings);
|
|
37
|
+
}, (receiver) => actualReceivers(receiver, binding, calls, checker, bindings)))
|
|
38
|
+
reject(node, binding, route.keys);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const access = sseWriteInvocation(node, checker);
|
|
42
|
+
if (!access || !access.rawBytes || !potentiallyExecuted(node))
|
|
43
|
+
continue;
|
|
44
|
+
const proven = executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker);
|
|
45
|
+
const helpers = helperBindings(node, calls, checker, bindings);
|
|
46
|
+
if (!proven && !helpers.length)
|
|
47
|
+
continue;
|
|
48
|
+
const binding = proven ? enclosingRouteBinding(node, checker, bindings, false) : undefined;
|
|
49
|
+
const candidates = binding ? [binding] : helpers;
|
|
50
|
+
for (const candidate of candidates) {
|
|
51
|
+
const route = routes.get(routeKey(candidate));
|
|
52
|
+
if (!route)
|
|
53
|
+
continue;
|
|
54
|
+
if (!access.receiver) {
|
|
55
|
+
reject(node, candidate, route.keys);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const receiver = expressionReceiver(access.receiver, checker);
|
|
59
|
+
if (!receiver) {
|
|
60
|
+
reject(node, candidate, route.keys);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const actual = actualReceivers(receiver, candidate, calls, checker, bindings);
|
|
64
|
+
const framed = route.receivers.flatMap((value) => actualReceivers(value, candidate, calls, checker, bindings));
|
|
65
|
+
if (actual.some((value) => value === undefined ||
|
|
66
|
+
framed.some((frame) => frame === undefined || sameWriteReceiver(frame, value))))
|
|
67
|
+
reject(node, candidate, route.keys);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { resolveEmissionStatus } from './response-contract-status.mts';
|
|
3
|
+
export declare function resolveSseStatus(paths: readonly (readonly (ts.CallExpression | ts.NewExpression)[])[], functions: readonly ts.FunctionLikeDeclaration[], contexts: ReadonlySet<ts.Symbol>, checker: ts.TypeChecker): ReturnType<typeof resolveEmissionStatus>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { executableProtocolPath } from './protocol-execution-path.mjs';
|
|
3
|
+
import { unsupportedContextAlias, unsupportedContextAssignment } from './protocol-context-alias.mjs';
|
|
4
|
+
import { contextResponseMethod } from './protocol-http-emission.mjs';
|
|
5
|
+
import { enclosingFunction, unwrapExpression } from './protocol-marker-analysis.mjs';
|
|
6
|
+
import { expressionReceiver } from './protocol-write-receiver.mjs';
|
|
7
|
+
import { statusCanPrecede } from './protocol-sse-feasible-status.mjs';
|
|
8
|
+
import { statusDominatesEmission, unconditionalStatusSetter } from './protocol-status-dominance.mjs';
|
|
9
|
+
import { visit } from './response-contract-route-analysis.mjs';
|
|
10
|
+
import { resolveEmissionStatus } from './response-contract-status.mjs';
|
|
11
|
+
import { statusForbidsBody } from './protocol-body-status.mjs';
|
|
12
|
+
export function resolveSseStatus(paths, functions, contexts, checker) {
|
|
13
|
+
const matches = (expression) => [...contexts].some((context) => contextResponseMethod(expression, context, checker) === 'setStatus');
|
|
14
|
+
const setters = new Set();
|
|
15
|
+
for (const fn of functions)
|
|
16
|
+
visit(fn, (node) => {
|
|
17
|
+
if (!executableProtocolPath(node, checker))
|
|
18
|
+
return;
|
|
19
|
+
if ([...contexts].some((context) => (ts.isVariableDeclaration(node) && unsupportedContextAlias(node, context, checker)) ||
|
|
20
|
+
unsupportedContextAssignment(node, context, checker)))
|
|
21
|
+
throw new Error('SSE context has an unsupported mutable or destructured alias');
|
|
22
|
+
if (ts.isCallExpression(node) && indirectSetter(node.expression, contexts, checker))
|
|
23
|
+
throw new Error('SSE context uses an unsupported indirect status setter');
|
|
24
|
+
if (ts.isCallExpression(node) && matches(node.expression))
|
|
25
|
+
setters.add(node);
|
|
26
|
+
});
|
|
27
|
+
const statuses = paths.map((path) => {
|
|
28
|
+
for (const setter of setters) {
|
|
29
|
+
const anchors = path.filter((anchor) => enclosingFunction(anchor) === enclosingFunction(setter));
|
|
30
|
+
if ((!anchors.length || anchors.some((anchor) => statusCanPrecede(setter, anchor))) &&
|
|
31
|
+
!anchors.some((anchor) => statusDominatesEmission(anchor, new Set([setter]))))
|
|
32
|
+
throw new Error('SSE status does not dominate its frame emission');
|
|
33
|
+
}
|
|
34
|
+
for (const anchor of path) {
|
|
35
|
+
const setter = nearestSetter(anchor, setters);
|
|
36
|
+
if (setter) {
|
|
37
|
+
const status = resolveEmissionStatus(setter, matches);
|
|
38
|
+
if (status.unavailableReason)
|
|
39
|
+
throw new Error(status.unavailableReason);
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { statusKnowledge: 'default' };
|
|
44
|
+
});
|
|
45
|
+
if (statuses.every((status) => status.statusKnowledge === 'default'))
|
|
46
|
+
return { statusKnowledge: 'default' };
|
|
47
|
+
const codes = [...new Set(statuses.flatMap((status) => status.statusCodes ?? [200]))].toSorted((a, b) => a - b);
|
|
48
|
+
if (codes.some((code) => !Number.isInteger(code) || code < 100 || code > 599))
|
|
49
|
+
throw new Error('SSE status must be an integer from 100 through 599');
|
|
50
|
+
if (codes.some(statusForbidsBody))
|
|
51
|
+
throw new Error('SSE HTTP status forbids response content');
|
|
52
|
+
return { statusKnowledge: 'explicit', statusCodes: codes };
|
|
53
|
+
}
|
|
54
|
+
function indirectSetter(expression, contexts, checker) {
|
|
55
|
+
const access = unwrapExpression(expression);
|
|
56
|
+
const name = ts.isPropertyAccessExpression(access)
|
|
57
|
+
? access.name.text
|
|
58
|
+
: ts.isElementAccessExpression(access) && ts.isStringLiteral(access.argumentExpression)
|
|
59
|
+
? access.argumentExpression.text
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!name || !['call', 'apply', 'bind'].includes(name))
|
|
62
|
+
return false;
|
|
63
|
+
const target = access.expression;
|
|
64
|
+
if ([...contexts].some((context) => contextResponseMethod(target, context, checker) === 'setStatus'))
|
|
65
|
+
return true;
|
|
66
|
+
const receiver = expressionReceiver(target, checker);
|
|
67
|
+
return !!receiver && contexts.has(receiver.root) && receiver.path[0] === 'setStatus';
|
|
68
|
+
}
|
|
69
|
+
function nearestSetter(anchor, setters) {
|
|
70
|
+
let current = anchor;
|
|
71
|
+
while (current.parent && !ts.isFunctionLike(current.parent)) {
|
|
72
|
+
const parent = current.parent;
|
|
73
|
+
if (ts.isBlock(parent)) {
|
|
74
|
+
const index = parent.statements.indexOf(current);
|
|
75
|
+
for (const statement of parent.statements.slice(0, index).toReversed()) {
|
|
76
|
+
const setter = unconditionalStatusSetter(statement, setters);
|
|
77
|
+
if (setter)
|
|
78
|
+
return setter;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
current = parent;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
type WriteInvocation = {
|
|
3
|
+
receiver: ts.Expression | undefined;
|
|
4
|
+
method: string | undefined;
|
|
5
|
+
rawBytes: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Resolves direct and indirect SSE writes without losing their actual receiver. */
|
|
8
|
+
export declare function sseWriteInvocation(call: ts.CallExpression, checker: ts.TypeChecker): WriteInvocation | undefined;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mjs';
|
|
2
|
+
import { hasRawBytes, indirectPayload, reflectApplyInvocation, writeAccess, } from './protocol-sse-write-resolution.mjs';
|
|
3
|
+
/** Resolves direct and indirect SSE writes without losing their actual receiver. */
|
|
4
|
+
export function sseWriteInvocation(call, checker) {
|
|
5
|
+
const reflected = reflectApplyInvocation(call, checker);
|
|
6
|
+
if (reflected)
|
|
7
|
+
return reflected;
|
|
8
|
+
const access = writeAccess(call.expression);
|
|
9
|
+
if (!access)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (access.method === 'pipe')
|
|
12
|
+
return call.arguments[0]
|
|
13
|
+
? { receiver: call.arguments[0], method: 'write', rawBytes: true }
|
|
14
|
+
: undefined;
|
|
15
|
+
if (access.method === 'bind') {
|
|
16
|
+
const target = writeAccess(access.receiver);
|
|
17
|
+
if (target?.method !== 'write' && target?.method !== 'end')
|
|
18
|
+
return undefined;
|
|
19
|
+
return { receiver: call.arguments[0], method: target.method, rawBytes: true };
|
|
20
|
+
}
|
|
21
|
+
if (access.method !== 'call' && access.method !== 'apply')
|
|
22
|
+
return { ...access, rawBytes: hasRawBytes(access.method, call.arguments, checker, false) };
|
|
23
|
+
const target = writeAccess(access.receiver);
|
|
24
|
+
const receiver = call.arguments[0];
|
|
25
|
+
if (!receiver)
|
|
26
|
+
return undefined;
|
|
27
|
+
const payload = indirectPayload(access.method, call, checker);
|
|
28
|
+
if (target?.method === 'pipe') {
|
|
29
|
+
const destination = payload?.[0];
|
|
30
|
+
return {
|
|
31
|
+
receiver: destination,
|
|
32
|
+
method: 'write',
|
|
33
|
+
rawBytes: payload === undefined || destination !== undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const method = target?.method === 'write' || target?.method === 'end' ? target.method : undefined;
|
|
37
|
+
return {
|
|
38
|
+
receiver,
|
|
39
|
+
method,
|
|
40
|
+
rawBytes: payload === undefined || hasRawBytes(method, payload, checker, true),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import ts from '../contract-schema/typescript-api.mts';
|
|
2
|
+
import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
|
|
3
|
+
import { type WriteReceiver } from './protocol-write-receiver.mts';
|
|
4
|
+
export declare function routeKey(binding: RouteBinding): string;
|
|
5
|
+
export declare function helperBindings(node: ts.Node, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings): RouteBinding[];
|
|
6
|
+
export declare function actualReceivers(receiver: WriteReceiver, binding: RouteBinding, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings, active?: Set<ts.Symbol>): (WriteReceiver | undefined)[];
|