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.
- package/README.md +42 -0
- package/dist/api-contract-discovery/protocol-http-association.mjs +11 -0
- package/dist/api-contract-discovery/protocol-http-context-escapes.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-context-escapes.mjs +95 -0
- package/dist/api-contract-discovery/protocol-http-context.d.mts +1 -2
- package/dist/api-contract-discovery/protocol-http-context.mjs +6 -29
- package/dist/api-contract-discovery/protocol-http-method-access.d.mts +9 -0
- package/dist/api-contract-discovery/protocol-http-method-access.mjs +34 -0
- package/dist/api-contract-discovery/protocol-http-method-mutations.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-method-mutations.mjs +31 -0
- package/dist/api-contract-discovery/protocol-http-reflect.d.mts +5 -0
- package/dist/api-contract-discovery/protocol-http-reflect.mjs +45 -0
- package/dist/api-contract-discovery/protocol-http-response-mutations.d.mts +3 -0
- package/dist/api-contract-discovery/protocol-http-response-mutations.mjs +31 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.d.mts +2 -0
- package/dist/api-contract-discovery/protocol-platform-callbacks.mjs +4 -0
- package/dist/api-contract-discovery/protocol-sse-raw-writes.mjs +36 -7
- package/dist/api-contract-discovery/protocol-sse-write-helpers.d.mts +1 -0
- package/dist/api-contract-discovery/protocol-sse-write-helpers.mjs +21 -5
- package/dist/api-contract-discovery/protocol-sse-write-mutations.d.mts +1 -1
- package/dist/api-contract-discovery/protocol-sse-write-mutations.mjs +8 -2
- package/dist/api-contract-discovery/registered-route-binding-writes.mjs +8 -0
- package/dist/basic-auth-doc-sync/index.d.mts +3 -0
- package/dist/basic-auth-doc-sync/index.mjs +2 -0
- package/dist/basic-auth-doc-sync/methods.d.mts +1 -0
- package/dist/basic-auth-doc-sync/methods.mjs +4 -0
- package/dist/basic-auth-doc-sync/runbook.d.mts +5 -0
- package/dist/basic-auth-doc-sync/runbook.mjs +59 -0
- package/dist/basic-auth-doc-sync/source.d.mts +2 -0
- package/dist/basic-auth-doc-sync/source.mjs +71 -0
- package/dist/markdown/content-facts.d.mts +6 -0
- package/dist/markdown/content-facts.mjs +18 -0
- package/dist/markdown/html-blocks.d.mts +5 -0
- package/dist/markdown/html-blocks.mjs +51 -0
- package/dist/markdown/index.d.mts +4 -1
- package/dist/markdown/index.mjs +3 -0
- package/dist/markdown/tasks.d.mts +2 -0
- package/dist/markdown/tasks.mjs +10 -0
- package/dist/markdown/types.d.mts +27 -0
- package/docs/basic-auth-doc-sync.md +37 -0
- 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,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
|
-
|
|
34
|
-
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 @@
|
|
|
1
|
+
export declare function isCanonicalHttpMethod(method: string): boolean;
|
|
@@ -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,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
|
|
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';
|
package/dist/markdown/index.mjs
CHANGED
|
@@ -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,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.
|
|
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"
|