vouchington-tooling 0.31.0 → 0.33.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 (41) hide show
  1. package/README.md +42 -0
  2. package/dist/api-contract-discovery/protocol-http-association.mjs +11 -0
  3. package/dist/api-contract-discovery/protocol-http-context-escapes.d.mts +3 -0
  4. package/dist/api-contract-discovery/protocol-http-context-escapes.mjs +95 -0
  5. package/dist/api-contract-discovery/protocol-http-context.d.mts +1 -2
  6. package/dist/api-contract-discovery/protocol-http-context.mjs +6 -29
  7. package/dist/api-contract-discovery/protocol-http-method-access.d.mts +9 -0
  8. package/dist/api-contract-discovery/protocol-http-method-access.mjs +34 -0
  9. package/dist/api-contract-discovery/protocol-http-method-mutations.d.mts +3 -0
  10. package/dist/api-contract-discovery/protocol-http-method-mutations.mjs +31 -0
  11. package/dist/api-contract-discovery/protocol-http-reflect.d.mts +5 -0
  12. package/dist/api-contract-discovery/protocol-http-reflect.mjs +45 -0
  13. package/dist/api-contract-discovery/protocol-http-response-mutations.d.mts +3 -0
  14. package/dist/api-contract-discovery/protocol-http-response-mutations.mjs +31 -0
  15. package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +2 -0
  16. package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +4 -0
  17. package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +36 -7
  18. package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +1 -0
  19. package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +21 -5
  20. package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +1 -1
  21. package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +8 -2
  22. package/dist/api-contract-discovery/registered-route-binding-writes.mjs +8 -0
  23. package/dist/basic-auth-doc-sync/index.d.mts +3 -0
  24. package/dist/basic-auth-doc-sync/index.mjs +2 -0
  25. package/dist/basic-auth-doc-sync/methods.d.mts +1 -0
  26. package/dist/basic-auth-doc-sync/methods.mjs +4 -0
  27. package/dist/basic-auth-doc-sync/runbook.d.mts +5 -0
  28. package/dist/basic-auth-doc-sync/runbook.mjs +59 -0
  29. package/dist/basic-auth-doc-sync/source.d.mts +2 -0
  30. package/dist/basic-auth-doc-sync/source.mjs +71 -0
  31. package/dist/markdown/content-facts.d.mts +6 -0
  32. package/dist/markdown/content-facts.mjs +18 -0
  33. package/dist/markdown/html-blocks.d.mts +5 -0
  34. package/dist/markdown/html-blocks.mjs +51 -0
  35. package/dist/markdown/index.d.mts +4 -1
  36. package/dist/markdown/index.mjs +3 -0
  37. package/dist/markdown/tasks.d.mts +2 -0
  38. package/dist/markdown/tasks.mjs +10 -0
  39. package/dist/markdown/types.d.mts +27 -0
  40. package/docs/basic-auth-doc-sync.md +37 -0
  41. package/package.json +12 -2
package/README.md CHANGED
@@ -366,6 +366,31 @@ summaries and quoted attributes; it is not a general HTML validator or sanitizer
366
366
  the input or infers semantic relevance, cost, schema conformance, or failure causes. The existing
367
367
  `markdownSectionBetweenHeadings` API retains its original boundary behavior.
368
368
 
369
+ `hasUncheckedMarkdownTask` reports parsed unchecked GFM task items, ignoring code and non-task text.
370
+ `markdownLiteralSpans` returns source-backed `{ kind, position, value }` facts for fenced or indented
371
+ code, inline code, and raw HTML. `markdownHtmlFacts` distinguishes inline HTML from block HTML and
372
+ labels CommonMark block types 1–7; `markdownHtmlBlocks` selects block types 1, 6, and 7, retaining
373
+ the parser's source bounds, including type 1 blocks through their matching closer or end of input.
374
+ Positions use 1-indexed line/column coordinates and JavaScript string offsets, including CRLF and
375
+ Unicode source text. These APIs return facts rather than mdast nodes so callers can apply their own
376
+ content policy without binding it to parser internals.
377
+
378
+ ```ts
379
+ import {
380
+ hasUncheckedMarkdownTask,
381
+ markdownHtmlBlocks,
382
+ markdownLiteralSpans,
383
+ } from 'vouchington-tooling/markdown'
384
+
385
+ const hasOpenTask = hasUncheckedMarkdownTask(body)
386
+ const literalSpans = markdownLiteralSpans(body)
387
+ const blockHtml = markdownHtmlBlocks(body)
388
+ ```
389
+
390
+ `markdownHtmlBlocks` reports CommonMark types 1, 6, and 7; inline tags do not appear in that result.
391
+ Use `markdownHtmlFacts` when inline/block classification and other CommonMark HTML block types are
392
+ also needed.
393
+
369
394
  Machine-wide agent configuration now belongs to [vouchington-machines](https://github.com/vouchington/vouchington-machines/blob/main/docs/agent-config.md).
370
395
  Use its `configure-agents.sh` and `diagnose-agents.sh`; project hooks and MCP configuration remain in their checkouts.
371
396
 
@@ -455,6 +480,8 @@ Version one recognizes literal-key `apiResponse`, `apiNoContent`, `apiOpenApiRaw
455
480
  `ctx.pipeline`, `ctx.response.empty/buffer/xml`, `ctx.request.json/buffer`, `parseJsonBody`,
456
481
  `ctx.setStatus`, and static `Content-Type` setters. The input source files and TypeScript checker
457
482
  are the source representation; the adapter does not parse or discover a repository on its own.
483
+ Reading the handler context in another object's computed assignment key preserves its binding;
484
+ assignments or updates to the context itself, including side effects inside that key, still reject it.
458
485
 
459
486
  Error-only 405 routes require a terminal throw on the handler's own context. Named handlers and
460
487
  imported wrappers are followed through compiler declarations; conditional, caught, recursive,
@@ -502,11 +529,19 @@ Caller-owned `apiOpenApiHttpResponse(key, response)` markers bind a constant opa
502
529
  carrying optional readonly `apiHttpResponseVariants` metadata. Each concrete union variant has
503
530
  `status` and `bodyKind`; content variants also have `mediaType` and `body`. Discovery associates
504
531
  the marked variable's status and body emissions without reading or replacing response bytes.
532
+ Feasible assignments, deletions, or updates to its body or status, including cast and constant
533
+ aliases, invalidate the selected contracts because their metadata no longer proves the dispatch.
505
534
  A status setter must dominate each body emission, including separate branches; constant context
506
535
  aliases are followed, assigned mutable context aliases and object-literal context wrappers fail closed,
507
536
  and helper parameters must resolve to the registered caller's unchanged response context.
508
537
  Helper-owned contracts also account for response emissions in their proven caller scopes.
538
+ Opaque callees receiving the canonical HTTP context or its response object invalidate its contract;
539
+ implemented helpers and consumed callbacks retain their proven dispatch behavior.
540
+ Feasible assignments or deletions of response methods through canonical context aliases invalidate
541
+ that dispatch evidence; dead, ignored, or unrelated method mutations preserve the contract.
509
542
  Indirect response `call`, `apply`, and `bind` forms fail closed, including borrowed context receivers.
543
+ Compiler-resolved standard `Reflect.apply` uses its target and actual receiver to reject unrepresented
544
+ HTTP emissions; project implementations, unrelated contexts, and non-executed calls are preserved.
510
545
  SSE frames and branded content variants reject body-forbidden 1xx, 204, 205, and 304 statuses.
511
546
  Literal bracket response methods have the same emission checks as property methods. Unrelated raw emissions
512
547
  remain unavailable. Separate branded response branches are checked together, and the nearest
@@ -675,3 +710,10 @@ block, escaped and length-bounded like CI failures, and other entries are ignore
675
710
  `Status: none observed` or `Status: events observed`, each followed by
676
711
  `(journal entries only; no friction log observed)`; a truncated scan reports
677
712
  `unavailable (journal scan incomplete)` with the entries read so far.
713
+
714
+ ## Basic Auth exemption facts
715
+
716
+ `vouchington-tooling/basic-auth-doc-sync` extracts literal exemption collections from
717
+ TypeScript and route rows from Markdown. Callers configure declaration names and
718
+ runbook headings, then compare the returned data using their own route policy.
719
+ See the [API contract](docs/basic-auth-doc-sync.md).
@@ -8,7 +8,10 @@ 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';
13
+ import { opaqueHttpContextArgument } from './protocol-http-context-escapes.mjs';
14
+ import { mutatedHttpResponse } from './protocol-http-response-mutations.mjs';
12
15
  const responseMethods = new Set([
13
16
  'setStatus',
14
17
  'pipeline',
@@ -26,6 +29,12 @@ export function associateHttpResponse(call, response, checker, siblings) {
26
29
  const emissions = new Map();
27
30
  for (const [scope, context] of httpContextScopes(handler, checker))
28
31
  visit(scope, (node) => {
32
+ if (mutatedHttpResponse(node, response, checker))
33
+ throw new Error('HTTP branded response body or status is mutated');
34
+ if (ts.isCallExpression(node) && opaqueHttpContextArgument(node, checker, context))
35
+ throw new Error('HTTP response context escapes through an opaque argument');
36
+ if (mutatesHttpResponseMethod(node, context, checker))
37
+ throw new Error('HTTP response context method is mutated');
29
38
  if (ts.isCallExpression(node) &&
30
39
  executableProtocolPath(node, checker) &&
31
40
  responseMethods.has(indirectHttpResponseMethod(node, context, checker) ?? ''))
@@ -65,6 +74,8 @@ export function associateHttpResponse(call, response, checker, siblings) {
65
74
  }
66
75
  /** Opaque callbacks may emit through an independently proven handler's context. */
67
76
  export function opaqueHttpResponse(call, checker, context) {
77
+ if (opaqueHttpContextArgument(call, checker, context))
78
+ return true;
68
79
  if (context)
69
80
  return (responseMethods.has(contextResponseMethod(call.expression, context, checker, true) ??
70
81
  indirectHttpResponseMethod(call, context, checker) ??
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Opaque consumers of the canonical context may emit undocumented status or bodies. */
3
+ export declare function opaqueHttpContextArgument(call: ts.CallExpression, checker: ts.TypeChecker, context?: ts.Symbol, active?: Set<ts.FunctionLikeDeclaration>): boolean;
@@ -0,0 +1,95 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { expressionReceiver } from './protocol-write-receiver.mjs';
3
+ import { httpHandlerContext, contextResponseMethod } from './protocol-http-context.mjs';
4
+ import { httpContextInvocations } from './protocol-http-context-callers.mjs';
5
+ import { callbackArgumentBindings } from './protocol-callback-argument-bindings.mjs';
6
+ import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
7
+ import { enclosingFunction, unwrapExpression } from './protocol-marker-analysis.mjs';
8
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
9
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
10
+ import { visit } from './response-contract-route-analysis.mjs';
11
+ import { standardReflectApply } from './protocol-http-reflect.mjs';
12
+ import { createProtocolCallbackValueResolver, isProtocolCallbackFunction, } from './protocol-callback-values.mjs';
13
+ /** Opaque consumers of the canonical context may emit undocumented status or bodies. */
14
+ export function opaqueHttpContextArgument(call, checker, context, active = new Set()) {
15
+ const matches = (symbol) => call.arguments.some((argument) => contextArgument(argument, symbol, checker));
16
+ if (context && !matches(context))
17
+ return false;
18
+ if (!(executableProtocolPath(call, checker) || opaqueProtocolCallbackPath(call, checker)))
19
+ return false;
20
+ if (context)
21
+ return !implementedCallee(call, context, checker, active);
22
+ for (let owner = enclosingFunction(call); owner; owner = enclosingFunction(owner)) {
23
+ const name = runtimeParameters(owner)[0]?.name;
24
+ const symbol = name && ts.isIdentifier(name) ? checker.getSymbolAtLocation(name) : undefined;
25
+ if (symbol && matches(symbol) && httpHandlerContext(owner, checker) === symbol)
26
+ return !implementedCallee(call, symbol, checker, active);
27
+ }
28
+ return false;
29
+ }
30
+ function contextArgument(expression, context, checker) {
31
+ const receiver = expressionReceiver(expression, checker);
32
+ return ((receiver?.root === context &&
33
+ (receiver.path.length === 0 ||
34
+ (receiver.path.length === 1 && receiver.path[0] === 'response'))) ||
35
+ contextResponseMethod(unwrapExpression(expression), context, checker, true) === 'response');
36
+ }
37
+ /** Actual caller bindings retain the concrete implementations of consumed callback parameters. */
38
+ function implementedCallee(call, context, checker, active) {
39
+ const resolver = createProtocolCallbackValueResolver(checker);
40
+ if (standardReflectApply(call, checker)) {
41
+ const target = call.arguments[0] && resolver.resolve(call.arguments[0], new Map());
42
+ return !!(target &&
43
+ isProtocolCallbackFunction(target.node) &&
44
+ target.node.body &&
45
+ ts.isBlock(target.node.body) &&
46
+ target.node.body.statements.length === 0);
47
+ }
48
+ const implemented = (env) => {
49
+ const target = resolver.resolve(call.expression, env);
50
+ return !!(target &&
51
+ isProtocolCallbackFunction(target.node) &&
52
+ target.node.body &&
53
+ !calleeEscapes(target.node, call, context, checker, active));
54
+ };
55
+ if (implemented(new Map()))
56
+ return true;
57
+ const expression = unwrapExpression(call.expression);
58
+ const declaration = ts.isIdentifier(expression)
59
+ ? checker.getSymbolAtLocation(expression)?.valueDeclaration
60
+ : undefined;
61
+ const owner = declaration && ts.isParameter(declaration) ? enclosingFunction(declaration) : undefined;
62
+ if (!owner || !runtimeParameters(owner).includes(declaration))
63
+ return false;
64
+ const callers = httpContextInvocations(owner, checker).callers;
65
+ return (callers.length > 0 &&
66
+ callers.every((invocation) => {
67
+ const target = resolver.resolve(invocation.expression, new Map());
68
+ if (target?.node !== owner)
69
+ return false;
70
+ const env = callbackArgumentBindings(owner, invocation, new Map(), target.env, checker, resolver);
71
+ return !!env && implemented(env);
72
+ }));
73
+ }
74
+ /** The current proven call binds canonical arguments; other foreign callers do not hide escapes. */
75
+ function calleeEscapes(fn, call, context, checker, active) {
76
+ if (active.has(fn))
77
+ return true;
78
+ const next = new Set(active).add(fn);
79
+ const contexts = [context];
80
+ for (const [index, parameter] of runtimeParameters(fn).entries()) {
81
+ const argument = call.arguments[index];
82
+ if (!argument || !contextArgument(argument, context, checker))
83
+ continue;
84
+ if (!ts.isIdentifier(parameter.name))
85
+ return true;
86
+ contexts.push(checker.getSymbolAtLocation(parameter.name));
87
+ }
88
+ let escaped = false;
89
+ visit(fn.body, (node) => {
90
+ if (ts.isCallExpression(node) &&
91
+ contexts.some((symbol) => opaqueHttpContextArgument(node, checker, symbol, next)))
92
+ escaped = true;
93
+ });
94
+ return escaped;
95
+ }
@@ -1,8 +1,7 @@
1
1
  import ts from '../contract-schema/typescript-api.mts';
2
+ export { contextResponseMethod } from './protocol-http-method-access.mts';
2
3
  /** A helper parameter is a response context only when every executable caller passes one. */
3
4
  export declare function httpHandlerContext(fn: ts.FunctionLikeDeclaration, checker: ts.TypeChecker, active?: Set<ts.FunctionLikeDeclaration>): ts.Symbol | undefined;
4
- /** Literal bracket access is equivalent to property access for response methods. */
5
- export declare function contextResponseMethod(expression: ts.Expression, context: ts.Symbol, checker: ts.TypeChecker, allowMutable?: boolean): string | undefined;
6
5
  /** Follow only caller contexts already admitted by the actual-argument proof. */
7
6
  export declare function httpContextScopes(fn: ts.FunctionLikeDeclaration, checker: ts.TypeChecker): Map<ts.FunctionLikeDeclaration, ts.Symbol>;
8
7
  /** Indirect methods cannot prove the declared status or body, including borrowed this receivers. */
@@ -4,6 +4,9 @@ import { enclosingFunction } from './protocol-marker-analysis.mjs';
4
4
  import { expressionReceiver } from './protocol-write-receiver.mjs';
5
5
  import { hasBindingWrite } from './registered-route-binding-writes.mjs';
6
6
  import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
7
+ import { contextResponseMethod, methodAccess } from './protocol-http-method-access.mjs';
8
+ import { reflectHttpResponseMethod } from './protocol-http-reflect.mjs';
9
+ export { contextResponseMethod } from './protocol-http-method-access.mjs';
7
10
  /** A helper parameter is a response context only when every executable caller passes one. */
8
11
  export function httpHandlerContext(fn, checker, active = new Set()) {
9
12
  if (active.has(fn))
@@ -31,35 +34,6 @@ export function httpHandlerContext(fn, checker, active = new Set()) {
31
34
  const result = valid ? context : undefined;
32
35
  return result;
33
36
  }
34
- /** Literal bracket access is equivalent to property access for response methods. */
35
- export function contextResponseMethod(expression, context, checker, allowMutable = false) {
36
- const access = methodAccess(expression);
37
- if (!access)
38
- return undefined;
39
- const receiver = expressionReceiver(access.receiver, checker) ??
40
- bracketResponseReceiver(access.receiver, checker);
41
- if (receiver?.root !== context || (receiver.mutableAlias && !allowMutable))
42
- return undefined;
43
- if (receiver.path.length === 0)
44
- return access.name;
45
- if (receiver.path.length === 1 && receiver.path[0] === 'response')
46
- return `response.${access.name}`;
47
- return undefined;
48
- }
49
- function methodAccess(expression) {
50
- if (ts.isPropertyAccessExpression(expression))
51
- return { receiver: expression.expression, name: expression.name.text };
52
- if (ts.isElementAccessExpression(expression) && ts.isStringLiteral(expression.argumentExpression))
53
- return { receiver: expression.expression, name: expression.argumentExpression.text };
54
- return undefined;
55
- }
56
- function bracketResponseReceiver(expression, checker) {
57
- const access = methodAccess(expression);
58
- if (!access || access.name !== 'response')
59
- return undefined;
60
- const receiver = expressionReceiver(access.receiver, checker);
61
- return receiver && { ...receiver, path: [...receiver.path, 'response'] };
62
- }
63
37
  /** Follow only caller contexts already admitted by the actual-argument proof. */
64
38
  export function httpContextScopes(fn, checker) {
65
39
  const scopes = new Map();
@@ -84,6 +58,9 @@ export function httpContextScopes(fn, checker) {
84
58
  }
85
59
  /** Indirect methods cannot prove the declared status or body, including borrowed this receivers. */
86
60
  export function indirectHttpResponseMethod(call, context, checker) {
61
+ const reflected = reflectHttpResponseMethod(call, context, checker);
62
+ if (reflected)
63
+ return reflected;
87
64
  const outer = methodAccess(call.expression);
88
65
  if (!outer || !['call', 'apply', 'bind'].includes(outer.name))
89
66
  return undefined;
@@ -0,0 +1,9 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Literal bracket access is equivalent to property access for response methods. */
3
+ export declare function contextResponseMethod(expression: ts.Expression, context: ts.Symbol, checker: ts.TypeChecker, allowMutable?: boolean): string | undefined;
4
+ export declare function methodAccess(expression: ts.Expression): {
5
+ receiver: ts.LeftHandSideExpression;
6
+ name: string;
7
+ } | undefined;
8
+ /** Uses the existing canonical receiver plus literal response-property access. */
9
+ export declare function httpContextReceiver(expression: ts.Expression, checker: ts.TypeChecker): import("./protocol-write-receiver.mts").WriteReceiver | undefined;
@@ -0,0 +1,34 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { expressionReceiver } from './protocol-write-receiver.mjs';
3
+ /** Literal bracket access is equivalent to property access for response methods. */
4
+ export function contextResponseMethod(expression, context, checker, allowMutable = false) {
5
+ const access = methodAccess(expression);
6
+ if (!access)
7
+ return undefined;
8
+ const receiver = httpContextReceiver(access.receiver, checker);
9
+ if (receiver?.root !== context || (receiver.mutableAlias && !allowMutable))
10
+ return undefined;
11
+ if (receiver.path.length === 0)
12
+ return access.name;
13
+ if (receiver.path.length === 1 && receiver.path[0] === 'response')
14
+ return `response.${access.name}`;
15
+ return undefined;
16
+ }
17
+ export function methodAccess(expression) {
18
+ if (ts.isPropertyAccessExpression(expression))
19
+ return { receiver: expression.expression, name: expression.name.text };
20
+ if (ts.isElementAccessExpression(expression) && ts.isStringLiteral(expression.argumentExpression))
21
+ return { receiver: expression.expression, name: expression.argumentExpression.text };
22
+ return undefined;
23
+ }
24
+ /** Uses the existing canonical receiver plus literal response-property access. */
25
+ export function httpContextReceiver(expression, checker) {
26
+ const receiver = expressionReceiver(expression, checker);
27
+ if (receiver)
28
+ return receiver;
29
+ const access = methodAccess(expression);
30
+ if (!access || access.name !== 'response')
31
+ return undefined;
32
+ const parent = expressionReceiver(access.receiver, checker);
33
+ return parent && { ...parent, path: [...parent.path, 'response'] };
34
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Both the actual invoked value and signature must belong to the compiler library. */
3
+ export declare function standardReflectApply(call: ts.CallExpression, checker: ts.TypeChecker): boolean;
4
+ /** Reflected context emissions cannot prove the marked status or response body. */
5
+ export declare function reflectHttpResponseMethod(call: ts.CallExpression, context: ts.Symbol, checker: ts.TypeChecker): string | undefined;
@@ -0,0 +1,45 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { standardCompilerDeclaration } from './protocol-platform-callbacks.mjs';
3
+ import { expressionReceiver } from './protocol-write-receiver.mjs';
4
+ import { unwrapExpression } from './protocol-marker-analysis.mjs';
5
+ import { contextResponseMethod, httpContextReceiver, methodAccess, } from './protocol-http-method-access.mjs';
6
+ /** Both the actual invoked value and signature must belong to the compiler library. */
7
+ export function standardReflectApply(call, checker) {
8
+ const access = methodAccess(unwrapExpression(call.expression));
9
+ const receiver = expressionReceiver(access?.receiver ?? call.expression, checker);
10
+ if (!receiver)
11
+ return false;
12
+ const path = [...receiver.path, ...(access ? [access.name] : [])];
13
+ if (path.join('.') !== 'apply')
14
+ return false;
15
+ if (!receiver.root.declarations?.every((value) => standardCompilerDeclaration(value, checker)))
16
+ return false;
17
+ const declaration = checker.getResolvedSignature(call)?.declaration;
18
+ if (!declaration || !ts.isFunctionDeclaration(declaration) || declaration.name?.text !== 'apply')
19
+ return false;
20
+ const namespace = declaration.parent.parent;
21
+ return (ts.isModuleDeclaration(namespace) &&
22
+ namespace.name.text === 'Reflect' &&
23
+ standardCompilerDeclaration(declaration, checker));
24
+ }
25
+ /** Reflected context emissions cannot prove the marked status or response body. */
26
+ export function reflectHttpResponseMethod(call, context, checker) {
27
+ if (!standardReflectApply(call, checker))
28
+ return undefined;
29
+ const target = call.arguments[0];
30
+ const direct = target && contextResponseMethod(target, context, checker, true);
31
+ if (direct)
32
+ return direct;
33
+ const method = target && expressionReceiver(target, checker);
34
+ if (method?.root === context)
35
+ return method.path[0] === 'response' ? method.path.slice(0, 2).join('.') : method.path[0];
36
+ const receiver = call.arguments[1] && httpContextReceiver(call.arguments[1], checker);
37
+ const name = (target && methodAccess(target)?.name) ?? method?.path.at(-1);
38
+ if (receiver?.root !== context || !name)
39
+ return undefined;
40
+ return receiver.path.length === 0
41
+ ? name
42
+ : receiver.path.length === 1 && receiver.path[0] === 'response'
43
+ ? `response.${name}`
44
+ : undefined;
45
+ }
@@ -0,0 +1,3 @@
1
+ import ts from '../contract-schema/typescript-api.mts';
2
+ /** Branded metadata requires the dispatched response's body and status to remain intact. */
3
+ export declare function mutatedHttpResponse(node: ts.Node, response: 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 { expressionReceiver } from './protocol-write-receiver.mjs';
5
+ import { executableProtocolPath } from './protocol-execution-path.mjs';
6
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
7
+ /** Branded metadata requires the dispatched response's body and status to remain intact. */
8
+ export function mutatedHttpResponse(node, response, checker) {
9
+ const target = mutationTarget(node);
10
+ if (!target)
11
+ return false;
12
+ const receiver = expressionReceiver(target, checker);
13
+ const field = receiver?.root === response
14
+ ? receiver.path[0]
15
+ : contextResponseMethod(unwrapExpression(target), response, checker, true);
16
+ return ((field === 'body' || field === 'status') &&
17
+ (executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker)));
18
+ }
19
+ function mutationTarget(node) {
20
+ if (ts.isBinaryExpression(node) &&
21
+ node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
22
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
23
+ return node.left;
24
+ if (ts.isDeleteExpression(node))
25
+ return node.expression;
26
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
27
+ (node.operator === ts.SyntaxKind.PlusPlusToken ||
28
+ node.operator === ts.SyntaxKind.MinusMinusToken))
29
+ return node.operand;
30
+ return undefined;
31
+ }
@@ -1,5 +1,7 @@
1
1
  import ts from '../contract-schema/typescript-api.mts';
2
2
  /** Records the actual caller compiler's library identities, including another installation. */
3
3
  export declare function registerPlatformCompilerLibraries(program: ts.Program): void;
4
+ /** Shares actual compiler-library identity with other standard API guards. */
5
+ export declare function standardCompilerDeclaration(declaration: ts.Declaration, checker: ts.TypeChecker): boolean;
4
6
  /** Recognizes only callback APIs supplied by the compiler's standard libraries or Node typings. */
5
7
  export declare function platformCallbackArgument(node: ts.CallExpression | ts.NewExpression, checker: ts.TypeChecker): ts.Expression | undefined;
@@ -27,6 +27,10 @@ function standardDeclaration(declaration, timer, checker) {
27
27
  return (timer &&
28
28
  /\/node_modules\/@types\/node\/(?:web-globals\/)?timers\.d\.ts$/.test(file.replaceAll('\\', '/')));
29
29
  }
30
+ /** Shares actual compiler-library identity with other standard API guards. */
31
+ export function standardCompilerDeclaration(declaration, checker) {
32
+ return standardDeclaration(declaration, false, checker);
33
+ }
30
34
  function platformSymbol(expression, checker, seen = new Set()) {
31
35
  const value = unwrapExpression(expression);
32
36
  if (!ts.isIdentifier(value))
@@ -6,7 +6,8 @@ import { enclosingRouteBinding, visit, } from './response-contract-route-analysi
6
6
  import { expressionReceiver, sameWriteReceiver, } from './protocol-write-receiver.mjs';
7
7
  import { sseWriteInvocation } from './protocol-sse-write-access.mjs';
8
8
  import { isSourceLevelMutation, mutationAffectsSelectedStream, sseWriteMutation, } from './protocol-sse-write-mutations.mjs';
9
- import { actualReceivers, helperBindings, routeKey } from './protocol-sse-write-helpers.mjs';
9
+ import { actualReceivers, helperBindings, opaqueCallReceivesSelectedStream, routeKey, } from './protocol-sse-write-helpers.mjs';
10
+ import { writeAccess } from './protocol-sse-write-resolution.mjs';
10
11
  /** Reports unmarked writes on executable or opaque escaping paths to a selected frame's stream. */
11
12
  export function rejectRawSseWrites(files, checker, bindings, framedWrites, routes, reject) {
12
13
  const calls = [];
@@ -30,8 +31,14 @@ export function rejectRawSseWrites(files, checker, bindings, framedWrites, route
30
31
  if (mutationNode.getSourceFile() === source && isSourceLevelMutation(mutationNode))
31
32
  return true;
32
33
  const candidate = enclosingRouteBinding(mutationNode, checker, bindings, false);
33
- return !!candidate && routeKey(candidate) === routeKey(binding);
34
- }, (expression) => {
34
+ if (candidate && routeKey(candidate) === routeKey(binding))
35
+ return true;
36
+ if (!candidate &&
37
+ mutationNode.getSourceFile() === source &&
38
+ opaqueProtocolCallbackPath(mutationNode, checker))
39
+ return true;
40
+ return helperBindings(mutationNode, calls, checker, bindings).some((helper) => routeKey(helper) === routeKey(binding));
41
+ }, (mutationNode) => helperBindings(mutationNode, calls, checker, bindings).length > 0, (expression) => {
35
42
  const receiver = expressionReceiver(expression, checker);
36
43
  return receiver && actualReceivers(receiver, binding, calls, checker, bindings);
37
44
  }, (receiver) => actualReceivers(receiver, binding, calls, checker, bindings)))
@@ -39,7 +46,7 @@ export function rejectRawSseWrites(files, checker, bindings, framedWrites, route
39
46
  continue;
40
47
  }
41
48
  const access = sseWriteInvocation(node, checker);
42
- if (!access || !access.rawBytes || !potentiallyExecuted(node))
49
+ if (!potentiallyExecuted(node))
43
50
  continue;
44
51
  const proven = executableProtocolPath(node, checker) || opaqueProtocolCallbackPath(node, checker);
45
52
  const helpers = helperBindings(node, calls, checker, bindings);
@@ -51,17 +58,39 @@ export function rejectRawSseWrites(files, checker, bindings, framedWrites, route
51
58
  const route = routes.get(routeKey(candidate));
52
59
  if (!route)
53
60
  continue;
61
+ const receiver = access?.receiver && expressionReceiver(access.receiver, checker);
62
+ const actual = receiver ? actualReceivers(receiver, candidate, calls, checker, bindings) : [];
63
+ const framed = route.receivers.flatMap((value) => actualReceivers(value, candidate, calls, checker, bindings));
64
+ const bound = writeAccess(node.expression);
65
+ const boundReceiver = bound?.method === 'bind' && expressionReceiver(bound.receiver, checker);
66
+ const boundActual = boundReceiver
67
+ ? actualReceivers(boundReceiver, candidate, calls, checker, bindings)
68
+ : [];
69
+ const accessTargetsSelected = (access?.method === 'write' || access?.method === 'end') &&
70
+ actual.some((value) => value !== undefined &&
71
+ framed.some((frame) => frame !== undefined && sameWriteReceiver(frame, value)));
72
+ const bindsSelectedMember = bound?.method === 'bind' &&
73
+ boundActual.some((value) => value !== undefined &&
74
+ framed.some((frame) => frame !== undefined &&
75
+ frame.root === value.root &&
76
+ value.path.length > frame.path.length &&
77
+ value.path.slice(0, frame.path.length).join('.') === frame.path.join('.')));
78
+ if (!accessTargetsSelected &&
79
+ !bindsSelectedMember &&
80
+ opaqueCallReceivesSelectedStream(node, route.receivers, candidate, calls, checker, bindings)) {
81
+ reject(node, candidate, route.keys);
82
+ continue;
83
+ }
84
+ if (!access || !access.rawBytes)
85
+ continue;
54
86
  if (!access.receiver) {
55
87
  reject(node, candidate, route.keys);
56
88
  continue;
57
89
  }
58
- const receiver = expressionReceiver(access.receiver, checker);
59
90
  if (!receiver) {
60
91
  reject(node, candidate, route.keys);
61
92
  continue;
62
93
  }
63
- const actual = actualReceivers(receiver, candidate, calls, checker, bindings);
64
- const framed = route.receivers.flatMap((value) => actualReceivers(value, candidate, calls, checker, bindings));
65
94
  if (actual.some((value) => value === undefined ||
66
95
  framed.some((frame) => frame === undefined || sameWriteReceiver(frame, value))))
67
96
  reject(node, candidate, route.keys);
@@ -2,5 +2,6 @@ import ts from '../contract-schema/typescript-api.mts';
2
2
  import { type HandlerBindings, type RouteBinding } from './response-contract-route-analysis.mts';
3
3
  import { type WriteReceiver } from './protocol-write-receiver.mts';
4
4
  export declare function routeKey(binding: RouteBinding): string;
5
+ export declare function opaqueCallReceivesSelectedStream(call: ts.CallExpression, selectedReceivers: readonly WriteReceiver[], binding: RouteBinding, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings): boolean;
5
6
  export declare function helperBindings(node: ts.Node, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings): RouteBinding[];
6
7
  export declare function actualReceivers(receiver: WriteReceiver, binding: RouteBinding, calls: readonly ts.CallExpression[], checker: ts.TypeChecker, bindings: HandlerBindings, active?: Set<ts.Symbol>): (WriteReceiver | undefined)[];
@@ -1,10 +1,11 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
2
  import { hasBindingWrite } from './registered-route-binding-writes.mjs';
3
+ import { createProtocolCallbackValueResolver } from './protocol-callback-values.mjs';
3
4
  import { runtimeParameters } from './registered-route-runtime-parameters.mjs';
4
5
  import { enclosingFunction } from './protocol-marker-analysis.mjs';
5
6
  import { executableProtocolPath } from './protocol-execution-path.mjs';
6
7
  import { enclosingRouteBinding, } from './response-contract-route-analysis.mjs';
7
- import { expressionReceiver } from './protocol-write-receiver.mjs';
8
+ import { expressionReceiver, sameWriteReceiver, } from './protocol-write-receiver.mjs';
8
9
  export function routeKey(binding) {
9
10
  return `${binding.method}:${binding.routeTemplate}`;
10
11
  }
@@ -22,13 +23,29 @@ function implementationDeclaration(declaration, checker) {
22
23
  }
23
24
  return undefined;
24
25
  }
26
+ function implementationCall(call, checker) {
27
+ return (implementationDeclaration(checker.getResolvedSignature(call)?.declaration, checker) ??
28
+ createProtocolCallbackValueResolver(checker).resolve(call.expression, new Map())?.node);
29
+ }
30
+ export function opaqueCallReceivesSelectedStream(call, selectedReceivers, binding, calls, checker, bindings) {
31
+ const implementation = implementationCall(call, checker);
32
+ if (implementation && ts.isFunctionLike(implementation) && 'body' in implementation)
33
+ return false;
34
+ const framed = selectedReceivers.flatMap((receiver) => actualReceivers(receiver, binding, calls, checker, bindings));
35
+ return call.arguments.some((argument) => {
36
+ const receiver = expressionReceiver(argument, checker);
37
+ if (!receiver)
38
+ return false;
39
+ return actualReceivers(receiver, binding, calls, checker, bindings).some((value) => value === undefined ||
40
+ framed.some((frame) => frame === undefined || sameWriteReceiver(frame, value)));
41
+ });
42
+ }
25
43
  export function helperBindings(node, calls, checker, bindings) {
26
44
  const fn = enclosingFunction(node);
27
45
  if (!fn || fn.asteriskToken)
28
46
  return [];
29
47
  return calls.flatMap((call) => {
30
- if (implementationDeclaration(checker.getResolvedSignature(call)?.declaration, checker) !== fn ||
31
- !executableProtocolPath(call, checker))
48
+ if (implementationCall(call, checker) !== fn || !executableProtocolPath(call, checker))
32
49
  return [];
33
50
  const binding = enclosingRouteBinding(call, checker, bindings, false);
34
51
  return binding ? [binding] : [];
@@ -50,8 +67,7 @@ export function actualReceivers(receiver, binding, calls, checker, bindings, act
50
67
  return [undefined];
51
68
  const index = runtimeParameters(fn).indexOf(declaration);
52
69
  const actuals = calls.flatMap((call) => {
53
- if (implementationDeclaration(checker.getResolvedSignature(call)?.declaration, checker) !== fn ||
54
- !executableProtocolPath(call, checker))
70
+ if (implementationCall(call, checker) !== fn || !executableProtocolPath(call, checker))
55
71
  return [];
56
72
  const callBinding = enclosingRouteBinding(call, checker, bindings, false);
57
73
  if (!callBinding || routeKey(callBinding) !== routeKey(binding))
@@ -5,5 +5,5 @@ export type SseWriteMutation = {
5
5
  receiver: ts.Expression;
6
6
  };
7
7
  export declare function sseWriteMutation(node: ts.Node): SseWriteMutation | undefined;
8
- export declare function mutationAffectsSelectedStream(mutations: readonly SseWriteMutation[], frameReceivers: readonly WriteReceiver[], checker: ts.TypeChecker, isSameRoute: (node: ts.Node) => boolean, resolveActual: (expression: ts.Expression) => readonly (WriteReceiver | undefined)[] | undefined, resolveFrame: (receiver: WriteReceiver) => readonly (WriteReceiver | undefined)[]): boolean;
8
+ export declare function mutationAffectsSelectedStream(mutations: readonly SseWriteMutation[], frameReceivers: readonly WriteReceiver[], checker: ts.TypeChecker, isSameRoute: (node: ts.Node) => boolean, isInvokedHelper: (node: ts.Node) => boolean, resolveActual: (expression: ts.Expression) => readonly (WriteReceiver | undefined)[] | undefined, resolveFrame: (receiver: WriteReceiver) => readonly (WriteReceiver | undefined)[]): boolean;
9
9
  export declare function isSourceLevelMutation(node: ts.Node): boolean;
@@ -1,5 +1,7 @@
1
1
  import ts from '../contract-schema/typescript-api.mjs';
2
+ import { potentiallyExecuted } from './protocol-executable-path.mjs';
2
3
  import { executableProtocolPath } from './protocol-execution-path.mjs';
4
+ import { opaqueProtocolCallbackPath } from './protocol-opaque-callback.mjs';
3
5
  import { sameWriteReceiver } from './protocol-write-receiver.mjs';
4
6
  import { writeAccess } from './protocol-sse-write-resolution.mjs';
5
7
  export function sseWriteMutation(node) {
@@ -18,10 +20,14 @@ export function sseWriteMutation(node) {
18
20
  function isAssignment(kind) {
19
21
  return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
20
22
  }
21
- export function mutationAffectsSelectedStream(mutations, frameReceivers, checker, isSameRoute, resolveActual, resolveFrame) {
23
+ export function mutationAffectsSelectedStream(mutations, frameReceivers, checker, isSameRoute, isInvokedHelper, resolveActual, resolveFrame) {
22
24
  const framed = frameReceivers.flatMap(resolveFrame);
23
25
  return mutations.some(({ node, receiver }) => {
24
- if ((!isSourceLevelMutation(node) && !executableProtocolPath(node, checker)) ||
26
+ if (!potentiallyExecuted(node) ||
27
+ (!isSourceLevelMutation(node) &&
28
+ !executableProtocolPath(node, checker) &&
29
+ !opaqueProtocolCallbackPath(node, checker) &&
30
+ !isInvokedHelper(node)) ||
25
31
  !isSameRoute(node))
26
32
  return false;
27
33
  const actual = resolveActual(receiver);
@@ -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);
@@ -0,0 +1,3 @@
1
+ export { findBasicAuthExemptPaths, findBasicAuthExemptMethodsByPath } from './source.mts';
2
+ export { findRunbookExemptRoutes } from './runbook.mts';
3
+ export type { ExemptRoute } from './runbook.mts';
@@ -0,0 +1,2 @@
1
+ export { findBasicAuthExemptPaths, findBasicAuthExemptMethodsByPath } from './source.mjs';
2
+ export { findRunbookExemptRoutes } from './runbook.mjs';
@@ -0,0 +1 @@
1
+ export declare function isCanonicalHttpMethod(method: string): boolean;
@@ -0,0 +1,4 @@
1
+ const HTTP_METHOD_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Z]+$/;
2
+ export function isCanonicalHttpMethod(method) {
3
+ return method === method.toUpperCase() && HTTP_METHOD_TOKEN_RE.test(method);
4
+ }
@@ -0,0 +1,5 @@
1
+ export type ExemptRoute = {
2
+ methods: string[];
3
+ path: string;
4
+ };
5
+ export declare function findRunbookExemptRoutes(markdown: string, heading: string): ExemptRoute[] | null;
@@ -0,0 +1,59 @@
1
+ import { parseGfmMarkdown } from '../markdown/ast.mjs';
2
+ import { markdownNodeText } from '../markdown/text.mjs';
3
+ import { parseMarkdownTables } from '../markdown/tables.mjs';
4
+ import { isCanonicalHttpMethod } from './methods.mjs';
5
+ const METHODS_CELL_RE = /^`[^`]+`(?:\s*,\s*`[^`]+`)*$/;
6
+ function codeText(cell) {
7
+ const match = /^`([^`]+)`$/.exec(cell);
8
+ return match?.[1] ?? null;
9
+ }
10
+ function methodTexts(cell) {
11
+ if (!METHODS_CELL_RE.test(cell))
12
+ return null;
13
+ const methods = Array.from(cell.matchAll(/`([^`]+)`/g), (match) => match[1]).filter((value) => value !== undefined);
14
+ return methods.every(isCanonicalHttpMethod) ? methods : null;
15
+ }
16
+ function tableHeaderColumns(cells) {
17
+ const columns = new Map();
18
+ for (const [index, cell] of cells.entries()) {
19
+ const name = cell.toLowerCase();
20
+ if ((name === 'path' || name === 'methods') && columns.has(name))
21
+ return null;
22
+ columns.set(name, index);
23
+ }
24
+ return columns;
25
+ }
26
+ export function findRunbookExemptRoutes(markdown, heading) {
27
+ const children = parseGfmMarkdown(markdown).children;
28
+ const headingIndex = children.findIndex((node) => node.type === 'heading' &&
29
+ node.depth === 3 &&
30
+ markdownNodeText(node, { whitespace: 'collapse' }).toLowerCase() === heading.toLowerCase());
31
+ if (headingIndex === -1)
32
+ return null;
33
+ const endIndex = children.findIndex((node, index) => index > headingIndex && node.type === 'heading' && node.depth <= 3);
34
+ const startOffset = children[headingIndex].position.end.offset;
35
+ const endOffset = endIndex === -1 ? markdown.length : children[endIndex].position.start.offset;
36
+ const table = parseMarkdownTables(markdown.slice(startOffset, endOffset), {
37
+ preserveInlineCodeMarkers: true,
38
+ })[0];
39
+ if (!table || table.length < 2)
40
+ return null;
41
+ const routes = [];
42
+ const columns = tableHeaderColumns(table[0].cells);
43
+ if (!columns)
44
+ return null;
45
+ const pathColumn = columns.get('path') ?? -1;
46
+ const methodsColumn = columns.get('methods') ?? -1;
47
+ if (pathColumn === -1 || methodsColumn === -1)
48
+ return null;
49
+ for (const row of table.slice(1)) {
50
+ const path = codeText(row.cells[pathColumn] ?? '');
51
+ const methods = methodTexts(row.cells[methodsColumn] ?? '');
52
+ if (!path)
53
+ return null;
54
+ if (!methods || methods.length === 0)
55
+ return null;
56
+ routes.push({ methods, path });
57
+ }
58
+ return routes;
59
+ }
@@ -0,0 +1,2 @@
1
+ export declare function findBasicAuthExemptPaths(code: string, variableName: string): string[] | null;
2
+ export declare function findBasicAuthExemptMethodsByPath(code: string, variableName: string): Map<string, string[]> | null;
@@ -0,0 +1,71 @@
1
+ import ts from '../contract-schema/typescript-api.mjs';
2
+ import { isCanonicalHttpMethod } from './methods.mjs';
3
+ export function findBasicAuthExemptPaths(code, variableName) {
4
+ const declaration = findTopLevelConstInitializer(code, variableName);
5
+ if (!declaration)
6
+ return null;
7
+ return stringSetValues(declaration);
8
+ }
9
+ export function findBasicAuthExemptMethodsByPath(code, variableName) {
10
+ const declaration = findTopLevelConstInitializer(code, variableName);
11
+ if (!declaration || !isNewCollectionExpression(declaration, 'Map'))
12
+ return null;
13
+ const [entriesArg] = declaration.arguments ?? [];
14
+ if (!entriesArg || !ts.isArrayLiteralExpression(entriesArg))
15
+ return null;
16
+ const entries = new Map();
17
+ for (const entry of entriesArg.elements) {
18
+ if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2)
19
+ return null;
20
+ const path = stringLiteralValue(entry.elements[0]);
21
+ const methods = stringSetValues(entry.elements[1]);
22
+ if (!path || !methods || methods.length === 0)
23
+ return null;
24
+ if (!methods.every(isCanonicalHttpMethod))
25
+ return null;
26
+ entries.set(path, methods);
27
+ }
28
+ return entries.size > 0 ? entries : null;
29
+ }
30
+ function findTopLevelConstInitializer(code, name) {
31
+ const source = ts.createSourceFile('basic-auth.mts', code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
32
+ for (const statement of source.statements) {
33
+ if (!ts.isVariableStatement(statement))
34
+ continue;
35
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
36
+ continue;
37
+ for (const declaration of statement.declarationList.declarations) {
38
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
39
+ continue;
40
+ return declaration.initializer ?? null;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function stringSetValues(expression) {
46
+ if (!expression || !isNewCollectionExpression(expression, 'Set'))
47
+ return null;
48
+ const [valuesArg] = expression.arguments ?? [];
49
+ if (!valuesArg || !ts.isArrayLiteralExpression(valuesArg))
50
+ return null;
51
+ const values = [];
52
+ for (const element of valuesArg.elements) {
53
+ const value = stringLiteralValue(element);
54
+ if (!value)
55
+ return null;
56
+ values.push(value);
57
+ }
58
+ return values.length > 0 ? values : null;
59
+ }
60
+ function stringLiteralValue(expression) {
61
+ if (expression &&
62
+ (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression))) {
63
+ return expression.text;
64
+ }
65
+ return null;
66
+ }
67
+ function isNewCollectionExpression(expression, collectionName) {
68
+ return (ts.isNewExpression(expression) &&
69
+ ts.isIdentifier(expression.expression) &&
70
+ expression.expression.text === collectionName);
71
+ }
@@ -0,0 +1,6 @@
1
+ import type { MarkdownSourceSpan } from './types.mts';
2
+ /**
3
+ * Returns source spans for fenced/indented code, inline code, and raw HTML. Offsets are JavaScript
4
+ * string offsets; positions retain the parser's 1-indexed line and column coordinates.
5
+ */
6
+ export declare function markdownLiteralSpans(markdown: string): MarkdownSourceSpan[];
@@ -0,0 +1,18 @@
1
+ import { parseGfmMarkdown, walkMarkdown } from './ast.mjs';
2
+ /**
3
+ * Returns source spans for fenced/indented code, inline code, and raw HTML. Offsets are JavaScript
4
+ * string offsets; positions retain the parser's 1-indexed line and column coordinates.
5
+ */
6
+ export function markdownLiteralSpans(markdown) {
7
+ const spans = [];
8
+ walkMarkdown(parseGfmMarkdown(markdown), (node) => {
9
+ if (node.type !== 'code' && node.type !== 'inlineCode' && node.type !== 'html')
10
+ return;
11
+ spans.push({
12
+ kind: node.type === 'inlineCode' ? 'inline-code' : node.type,
13
+ position: node.position,
14
+ value: node.value,
15
+ });
16
+ });
17
+ return spans;
18
+ }
@@ -0,0 +1,5 @@
1
+ import type { MarkdownHtmlFact } from './types.mts';
2
+ /** Returns source-backed HTML facts, classifying inline occurrences separately from block nodes. */
3
+ export declare function markdownHtmlFacts(markdown: string): MarkdownHtmlFact[];
4
+ /** Returns block HTML facts for CommonMark types 1, 6, and 7, preserving their parser bounds. */
5
+ export declare function markdownHtmlBlocks(markdown: string): MarkdownHtmlFact[];
@@ -0,0 +1,51 @@
1
+ import { parseGfmMarkdown } from './ast.mjs';
2
+ const BLOCK_CONTAINERS = new Set(['root', 'blockquote', 'list', 'listItem', 'footnoteDefinition']);
3
+ const TYPE1 = /^ {0,3}<(script|style|pre|textarea)(?:[\s>]|$)/i;
4
+ const TYPE6_NAMES = 'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|' +
5
+ 'dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|' +
6
+ 'hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|' +
7
+ 'search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul';
8
+ const TYPE6 = new RegExp(`^ {0,3}</?(?:${TYPE6_NAMES})(?:[\\s>]|/>|$)`, 'i');
9
+ function htmlType(value) {
10
+ if (TYPE1.test(value))
11
+ return 1;
12
+ if (/^ {0,3}<!--/.test(value))
13
+ return 2;
14
+ if (/^ {0,3}<\?/.test(value))
15
+ return 3;
16
+ if (/^ {0,3}<![A-Za-z]/.test(value))
17
+ return 4;
18
+ if (/^ {0,3}<!\[CDATA\[/i.test(value))
19
+ return 5;
20
+ if (TYPE6.test(value))
21
+ return 6;
22
+ // Parsed block HTML outside types 1–6 is CommonMark type 7 by definition.
23
+ return 7;
24
+ }
25
+ /** Returns source-backed HTML facts, classifying inline occurrences separately from block nodes. */
26
+ export function markdownHtmlFacts(markdown) {
27
+ const facts = [];
28
+ const visit = (node, inBlockContainer) => {
29
+ if (node.type === 'html') {
30
+ const kind = inBlockContainer ? 'block' : 'inline';
31
+ facts.push({
32
+ kind,
33
+ commonMarkType: kind === 'block' ? htmlType(node.value) : null,
34
+ position: node.position,
35
+ value: node.value,
36
+ });
37
+ }
38
+ if (!('children' in node))
39
+ return;
40
+ const childIsBlockContainer = BLOCK_CONTAINERS.has(node.type);
41
+ for (const child of node.children)
42
+ visit(child, childIsBlockContainer);
43
+ };
44
+ visit(parseGfmMarkdown(markdown), true);
45
+ return facts;
46
+ }
47
+ /** Returns block HTML facts for CommonMark types 1, 6, and 7, preserving their parser bounds. */
48
+ export function markdownHtmlBlocks(markdown) {
49
+ return markdownHtmlFacts(markdown).filter((fact) => fact.kind === 'block' &&
50
+ (fact.commonMarkType === 1 || fact.commonMarkType === 6 || fact.commonMarkType === 7));
51
+ }
@@ -1,4 +1,6 @@
1
1
  export { findMarkdownNode, findMarkdownNodes, parseGfmMarkdown, walkMarkdown } from './ast.mts';
2
+ export { markdownHtmlBlocks, markdownHtmlFacts } from './html-blocks.mts';
3
+ export { markdownLiteralSpans } from './content-facts.mts';
2
4
  export { extractLooseMarkdownTableRows } from './loose-pipe-rows.mts';
3
5
  export { parseMarkdownSections } from './document-sections.mts';
4
6
  export { validateMarkdownSections } from './validate-sections.mts';
@@ -6,4 +8,5 @@ export type { MarkdownVisibleSection, MarkdownSectionsDiagnostic, MarkdownSectio
6
8
  export { markdownSectionBetweenHeadings } from './sections.mts';
7
9
  export { extractMarkdownTables, parseMarkdownTables } from './tables.mts';
8
10
  export { markdownNodeText } from './text.mts';
9
- export type { LooseMarkdownTableRow, LooseMarkdownTableRowsOptions, MarkdownNode, MarkdownSection, MarkdownSectionOptions, MarkdownTableRow, MarkdownTextOptions, ParseMarkdownTablesOptions, PositionedMarkdownTable, } from './types.mts';
11
+ export { hasUncheckedMarkdownTask } from './tasks.mts';
12
+ export type { LooseMarkdownTableRow, LooseMarkdownTableRowsOptions, MarkdownNode, MarkdownHtmlBlockType, MarkdownHtmlFact, MarkdownSourcePosition, MarkdownSourceSpan, MarkdownSection, MarkdownSectionOptions, MarkdownTableRow, MarkdownTextOptions, ParseMarkdownTablesOptions, PositionedMarkdownTable, } from './types.mts';
@@ -1,7 +1,10 @@
1
1
  export { findMarkdownNode, findMarkdownNodes, parseGfmMarkdown, walkMarkdown } from './ast.mjs';
2
+ export { markdownHtmlBlocks, markdownHtmlFacts } from './html-blocks.mjs';
3
+ export { markdownLiteralSpans } from './content-facts.mjs';
2
4
  export { extractLooseMarkdownTableRows } from './loose-pipe-rows.mjs';
3
5
  export { parseMarkdownSections } from './document-sections.mjs';
4
6
  export { validateMarkdownSections } from './validate-sections.mjs';
5
7
  export { markdownSectionBetweenHeadings } from './sections.mjs';
6
8
  export { extractMarkdownTables, parseMarkdownTables } from './tables.mjs';
7
9
  export { markdownNodeText } from './text.mjs';
10
+ export { hasUncheckedMarkdownTask } from './tasks.mjs';
@@ -0,0 +1,2 @@
1
+ /** True when the GFM parse contains at least one unchecked task-list item. */
2
+ export declare function hasUncheckedMarkdownTask(markdown: string): boolean;
@@ -0,0 +1,10 @@
1
+ import { parseGfmMarkdown, walkMarkdown } from './ast.mjs';
2
+ /** True when the GFM parse contains at least one unchecked task-list item. */
3
+ export function hasUncheckedMarkdownTask(markdown) {
4
+ let unchecked = false;
5
+ walkMarkdown(parseGfmMarkdown(markdown), (node) => {
6
+ if (node.type === 'listItem' && node.checked === false)
7
+ unchecked = true;
8
+ });
9
+ return unchecked;
10
+ }
@@ -1,6 +1,33 @@
1
1
  import type { Heading, Root, RootContent } from 'mdast';
2
2
  import type { Position } from 'unist';
3
3
  export type MarkdownNode = Root | RootContent;
4
+ /** A source range measured in JavaScript string offsets and 1-indexed source coordinates. */
5
+ export type MarkdownSourcePosition = {
6
+ start: {
7
+ line: number;
8
+ column: number;
9
+ offset: number;
10
+ };
11
+ end: {
12
+ line: number;
13
+ column: number;
14
+ offset: number;
15
+ };
16
+ };
17
+ /** A source-backed Markdown span with no dependency on mdast node types. */
18
+ export type MarkdownSourceSpan = {
19
+ kind: 'code' | 'inline-code' | 'html';
20
+ position: MarkdownSourcePosition;
21
+ value: string;
22
+ };
23
+ export type MarkdownHtmlBlockType = 1 | 2 | 3 | 4 | 5 | 6 | 7;
24
+ /** HTML parser facts distinguish block parsing from inline HTML in a phrasing container. */
25
+ export type MarkdownHtmlFact = {
26
+ commonMarkType: MarkdownHtmlBlockType | null;
27
+ kind: 'block' | 'inline';
28
+ position: MarkdownSourcePosition;
29
+ value: string;
30
+ };
4
31
  export type MarkdownTableRow = {
5
32
  cells: string[];
6
33
  line: number;
@@ -0,0 +1,37 @@
1
+ # Basic Auth exemption facts
2
+
3
+ Import from `vouchington-tooling/basic-auth-doc-sync`. TypeScript source parsing uses
4
+ the optional `@typescript/typescript6` dependency. Markdown-only callers can import
5
+ `findRunbookExemptRoutes` from `vouchington-tooling/basic-auth-doc-sync/runbook`
6
+ without installing that optional compiler.
7
+
8
+ - `findBasicAuthExemptPaths(source, variableName)` returns the literal strings in a
9
+ top-level `const` initialized with `new Set([...])`.
10
+ - `findBasicAuthExemptMethodsByPath(source, variableName)` returns a map of literal
11
+ paths to literal uppercase HTTP method tokens from `new Map([[path, new Set([...])]])`.
12
+ - `findRunbookExemptRoutes(markdown, heading)` reads the first GFM table following
13
+ the configured real GFM level-three heading (case insensitive), stopping at the
14
+ next same-or-higher-level heading. Duplicate `Path` or `Methods` columns are rejected. Columns are named `Path`
15
+ and `Methods`; paths and comma-separated method tokens must be inline code.
16
+
17
+ All functions return `null` for absent or unsupported declarations/tables. Source
18
+ collections must be nonempty and contain only nonempty string literals or static
19
+ templates; dynamic expressions are rejected. Literal order and duplicate methods
20
+ or documentation rows are preserved for caller validation; repeated map keys follow
21
+ Map replacement semantics. The runbook reader accepts short table separators and CRLF.
22
+
23
+ Callers own symbol names, route inventory, document locations, comparisons, and
24
+ diagnostics. These functions return data without exposing AST nodes.
25
+
26
+ ## Source provenance
27
+
28
+ The fact readers were extracted from `vouchington/vouchington` at
29
+ `99a0948e4e3cc0c5b9791062df6e9de37e4cf3f1`:
30
+
31
+ - `static-code-analysis/targeted-guardrails/basic-auth-doc-sync-parsers.mts`
32
+ - `static-code-analysis/targeted-guardrails/basic-auth-doc-sync.test.mts`
33
+ - `static-code-analysis/targeted-guardrails/basic-auth-doc-sync-parser-edge.test.mts`
34
+
35
+ The original parser and its characterization tests informed the extraction. The
36
+ first extraction commit misspelled the edge-test basename in its provenance body;
37
+ the paths above are the authoritative source list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.31.0",
3
+ "version": "0.33.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": {
@@ -331,7 +331,17 @@
331
331
  "import": "./dist/gh-api-shell-quoting/index.mjs",
332
332
  "default": "./dist/gh-api-shell-quoting/index.mjs"
333
333
  },
334
- "./package.json": "./package.json"
334
+ "./package.json": "./package.json",
335
+ "./basic-auth-doc-sync": {
336
+ "types": "./dist/basic-auth-doc-sync/index.d.mts",
337
+ "import": "./dist/basic-auth-doc-sync/index.mjs",
338
+ "default": "./dist/basic-auth-doc-sync/index.mjs"
339
+ },
340
+ "./basic-auth-doc-sync/runbook": {
341
+ "types": "./dist/basic-auth-doc-sync/runbook.d.mts",
342
+ "import": "./dist/basic-auth-doc-sync/runbook.mjs",
343
+ "default": "./dist/basic-auth-doc-sync/runbook.mjs"
344
+ }
335
345
  },
336
346
  "publishConfig": {
337
347
  "access": "public"