vouchington-tooling 0.31.0 → 0.32.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 CHANGED
@@ -455,6 +455,8 @@ Version one recognizes literal-key `apiResponse`, `apiNoContent`, `apiOpenApiRaw
455
455
  `ctx.pipeline`, `ctx.response.empty/buffer/xml`, `ctx.request.json/buffer`, `parseJsonBody`,
456
456
  `ctx.setStatus`, and static `Content-Type` setters. The input source files and TypeScript checker
457
457
  are the source representation; the adapter does not parse or discover a repository on its own.
458
+ Reading the handler context in another object's computed assignment key preserves its binding;
459
+ assignments or updates to the context itself, including side effects inside that key, still reject it.
458
460
 
459
461
  Error-only 405 routes require a terminal throw on the handler's own context. Named handlers and
460
462
  imported wrappers are followed through compiler declarations; conditional, caught, recursive,
@@ -506,6 +508,8 @@ A status setter must dominate each body emission, including separate branches; c
506
508
  aliases are followed, assigned mutable context aliases and object-literal context wrappers fail closed,
507
509
  and helper parameters must resolve to the registered caller's unchanged response context.
508
510
  Helper-owned contracts also account for response emissions in their proven caller scopes.
511
+ Feasible assignments or deletions of response methods through canonical context aliases invalidate
512
+ that dispatch evidence; dead, ignored, or unrelated method mutations preserve the contract.
509
513
  Indirect response `call`, `apply`, and `bind` forms fail closed, including borrowed context receivers.
510
514
  SSE frames and branded content variants reject body-forbidden 1xx, 204, 205, and 304 statuses.
511
515
  Literal bracket response methods have the same emission checks as property methods. Unrelated raw emissions
@@ -8,6 +8,7 @@ import { unsupportedContextAlias } from './protocol-context-alias.mjs';
8
8
  import { expressionReceiver } from './protocol-write-receiver.mjs';
9
9
  import { visit } from './response-contract-route-analysis.mjs';
10
10
  import { isSupportedProtocolCallback } from './protocol-callback-invocation.mjs';
11
+ import { mutatesHttpResponseMethod } from './protocol-http-method-mutations.mjs';
11
12
  import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
12
13
  const responseMethods = new Set([
13
14
  'setStatus',
@@ -26,6 +27,8 @@ export function associateHttpResponse(call, response, checker, siblings) {
26
27
  const emissions = new Map();
27
28
  for (const [scope, context] of httpContextScopes(handler, checker))
28
29
  visit(scope, (node) => {
30
+ if (mutatesHttpResponseMethod(node, context, checker))
31
+ throw new Error('HTTP response context method is mutated');
29
32
  if (ts.isCallExpression(node) &&
30
33
  executableProtocolPath(node, checker) &&
31
34
  responseMethods.has(indirectHttpResponseMethod(node, context, checker) ?? ''))
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** A feasible write through a canonical context alias invalidates response dispatch evidence. */
3
+ export declare function mutatesHttpResponseMethod(node: ts.Node, context: ts.Symbol, checker: ts.TypeChecker): boolean;
@@ -0,0 +1,31 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { contextResponseMethod } from './protocol-http-context.mjs';
3
+ import { unwrapExpression } from './protocol-marker-analysis.mjs';
4
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
5
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
6
+ const responseMethods = new Set([
7
+ 'setStatus',
8
+ 'pipeline',
9
+ 'json',
10
+ 'response',
11
+ 'response.buffer',
12
+ 'response.empty',
13
+ ]);
14
+ /** A feasible write through a canonical context alias invalidates response dispatch evidence. */
15
+ export function mutatesHttpResponseMethod(node, context, checker) {
16
+ let target;
17
+ if (ts.isBinaryExpression(node) &&
18
+ node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
19
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
20
+ target = node.left;
21
+ if (ts.isDeleteExpression(node))
22
+ target = node.expression;
23
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
24
+ (node.operator === ts.SyntaxKind.PlusPlusToken ||
25
+ node.operator === ts.SyntaxKind.MinusMinusToken))
26
+ target = node.operand;
27
+ if (!target ||
28
+ !responseMethods.has(contextResponseMethod(unwrapExpression(target), context, checker, true) ?? ''))
29
+ return false;
30
+ return executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker);
31
+ }
@@ -8,6 +8,14 @@ export function hasBindingWrite(declaration, checker) {
8
8
  const binding = checker.getSymbolAtLocation(declaration.name);
9
9
  let written = false;
10
10
  const referencesBinding = (node) => {
11
+ // Index expressions and computed property names read their keys, not write them.
12
+ // inspect still visits these expressions separately to detect nested side-effect writes.
13
+ if (ts.isComputedPropertyName(node))
14
+ return;
15
+ if (ts.isElementAccessExpression(node)) {
16
+ referencesBinding(node.expression);
17
+ return;
18
+ }
11
19
  if (ts.isIdentifier(node) && checker.getSymbolAtLocation(node) === binding)
12
20
  written = true;
13
21
  node.forEachChild(referencesBinding);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {