@molecule/api-ai-tools 1.9.11 → 1.9.12
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 +23 -1
- package/dist/trace-artifacts.d.ts +31 -0
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +141 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-18T07:
|
|
6
|
+
Generated: 2026-10-18T07:36:48.788Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -404,6 +404,28 @@ interface TraceRequest {
|
|
|
404
404
|
* fixture), in which case only recording order is available.
|
|
405
405
|
*/
|
|
406
406
|
startedAt?: number
|
|
407
|
+
/**
|
|
408
|
+
* Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
|
|
409
|
+
*
|
|
410
|
+
* A 401 cannot be read without this: the API's own body said
|
|
411
|
+
* `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
|
|
412
|
+
* when the header's absence proved the CLIENT never attached the token
|
|
413
|
+
* (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
|
|
414
|
+
* refused a token it was sent.
|
|
415
|
+
*/
|
|
416
|
+
sentAuthorization?: boolean
|
|
417
|
+
/**
|
|
418
|
+
* The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
|
|
419
|
+
* no header or its first word does not parse as a scheme (a value pasted without one is a
|
|
420
|
+
* credential, and is never copied here).
|
|
421
|
+
*/
|
|
422
|
+
authorization?: string
|
|
423
|
+
/**
|
|
424
|
+
* Whether the request carried a `Cookie` header — never the cookies themselves. An app that
|
|
425
|
+
* signs in with a session cookie has no `Authorization` header by design, so this tells a
|
|
426
|
+
* credential-less request from one whose credential is a cookie.
|
|
427
|
+
*/
|
|
428
|
+
sentCookie?: boolean
|
|
407
429
|
}
|
|
408
430
|
```
|
|
409
431
|
|
|
@@ -67,6 +67,28 @@ export interface TraceRequest {
|
|
|
67
67
|
* fixture), in which case only recording order is available.
|
|
68
68
|
*/
|
|
69
69
|
startedAt?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
|
|
72
|
+
*
|
|
73
|
+
* A 401 cannot be read without this: the API's own body said
|
|
74
|
+
* `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
|
|
75
|
+
* when the header's absence proved the CLIENT never attached the token
|
|
76
|
+
* (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
|
|
77
|
+
* refused a token it was sent.
|
|
78
|
+
*/
|
|
79
|
+
sentAuthorization?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
|
|
82
|
+
* no header or its first word does not parse as a scheme (a value pasted without one is a
|
|
83
|
+
* credential, and is never copied here).
|
|
84
|
+
*/
|
|
85
|
+
authorization?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the request carried a `Cookie` header — never the cookies themselves. An app that
|
|
88
|
+
* signs in with a session cookie has no `Authorization` header by design, so this tells a
|
|
89
|
+
* credential-less request from one whose credential is a cookie.
|
|
90
|
+
*/
|
|
91
|
+
sentCookie?: boolean;
|
|
70
92
|
}
|
|
71
93
|
/**
|
|
72
94
|
* The test's LAST user action before the failing assertion, and the requests it caused.
|
|
@@ -313,6 +335,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
|
|
|
313
335
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
314
336
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
315
337
|
*
|
|
338
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
339
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
340
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
341
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
342
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
343
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
344
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
345
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
346
|
+
*
|
|
316
347
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
317
348
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
318
349
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
|
package/dist/trace-artifacts.js
CHANGED
|
@@ -243,12 +243,34 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
243
243
|
const response = snapshot.response || {}
|
|
244
244
|
const type = String(snapshot._resourceType || '')
|
|
245
245
|
if (!origin) origin = type === 'document' ? originOf(String(request.url)) : ''
|
|
246
|
+
// What the request carried for CREDENTIALS — never the secret itself. A 401 on the app's own
|
|
247
|
+
// API cannot be read without this: m148's favorite POST answered 401 with no Authorization
|
|
248
|
+
// header at all, so the client never attached the token, and the note sent the run to the
|
|
249
|
+
// API handler instead (xr-molecule-flash-m148 +29:18). Presence is recorded as a boolean and
|
|
250
|
+
// only the scheme WORD (\`Bearer\`, \`Token\`) is kept; the value is a live credential and never
|
|
251
|
+
// leaves the trace. A first word that does not parse as a scheme (\`Authorization: <raw jwt>\`)
|
|
252
|
+
// is recorded as presence only, so a pasted token can never ride along as a "scheme".
|
|
253
|
+
const headerList = Array.isArray(request.headers) ? request.headers : []
|
|
254
|
+
const authHeader = headerList.find(function (h) {
|
|
255
|
+
return h && typeof h.name === 'string' && /^authorization$/i.test(h.name)
|
|
256
|
+
})
|
|
257
|
+
const authValue =
|
|
258
|
+
authHeader && typeof authHeader.value === 'string' ? String(authHeader.value).trim() : ''
|
|
259
|
+
const authSpace = authValue.search(/\\s/)
|
|
260
|
+
const authWord = authSpace > 0 ? authValue.slice(0, authSpace) : ''
|
|
261
|
+
const authScheme = /^[A-Za-z][A-Za-z0-9._~+/-]{0,23}$/.test(authWord) ? authWord : undefined
|
|
262
|
+
const sentCookie = headerList.some(function (h) {
|
|
263
|
+
return h && typeof h.name === 'string' && /^cookie$/i.test(h.name)
|
|
264
|
+
})
|
|
246
265
|
requests.push({
|
|
247
266
|
method: String(request.method).toUpperCase(),
|
|
248
267
|
path: String(request.url).slice(0, MAX_URL),
|
|
249
268
|
status: Number(response.status) || 0,
|
|
250
269
|
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
251
270
|
type: type,
|
|
271
|
+
sentAuthorization: authHeader !== undefined,
|
|
272
|
+
authorization: authScheme,
|
|
273
|
+
sentCookie: sentCookie,
|
|
252
274
|
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
253
275
|
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
254
276
|
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
@@ -430,6 +452,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
430
452
|
...(r.sent ? { sent: r.sent } : {}),
|
|
431
453
|
...(r.got ? { got: r.got } : {}),
|
|
432
454
|
type: r.type || undefined,
|
|
455
|
+
...(r.sentAuthorization ? { sentAuthorization: true } : {}),
|
|
456
|
+
...(r.authorization ? { authorization: r.authorization } : {}),
|
|
457
|
+
...(r.sentCookie ? { sentCookie: true } : {}),
|
|
433
458
|
}
|
|
434
459
|
}
|
|
435
460
|
// The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
|
|
@@ -590,6 +615,12 @@ function parseTraceRequest(value) {
|
|
|
590
615
|
...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
|
|
591
616
|
...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
|
|
592
617
|
...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
|
|
618
|
+
// The scheme word only: a raw header value is a live credential and is never parsed in.
|
|
619
|
+
...(r.sentAuthorization === true ? { sentAuthorization: true } : {}),
|
|
620
|
+
...(typeof r.authorization === 'string' && r.authorization
|
|
621
|
+
? { authorization: r.authorization }
|
|
622
|
+
: {}),
|
|
623
|
+
...(r.sentCookie === true ? { sentCookie: true } : {}),
|
|
593
624
|
};
|
|
594
625
|
}
|
|
595
626
|
/**
|
|
@@ -1031,6 +1062,94 @@ function sameOriginPath(request) {
|
|
|
1031
1062
|
return null;
|
|
1032
1063
|
return raw.replace(/\/+$/, '');
|
|
1033
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* The sentence a 401/403 on the app's own API gets: which half of the wire refused the request.
|
|
1067
|
+
*
|
|
1068
|
+
* A fixed "read the handler this route reaches" line is the wrong half for a credential refusal.
|
|
1069
|
+
* The request's own `Authorization` header — recorded by the trace reader as its scheme WORD, never
|
|
1070
|
+
* its value — decides: absent means the CLIENT never attached the token (a session store that the
|
|
1071
|
+
* HTTP client does not read, a bare `fetch`, an auth bond the app replaced), while present means
|
|
1072
|
+
* the SERVER refused a token it was sent (scheme or issuer). A 403 is a permission answer, not a
|
|
1073
|
+
* credential one, and says so.
|
|
1074
|
+
*
|
|
1075
|
+
* xr-molecule-flash-m148 (+29:18): `POST /api/articles/…/favorite → 401 {"errors":{"token":["is
|
|
1076
|
+
* missing"]}}` was answered with the handler sentence for six local walks, while the trace proved
|
|
1077
|
+
* the request carried no `Authorization` header at all and the app's hand-rolled session store
|
|
1078
|
+
* never handed its token to the shared client.
|
|
1079
|
+
*
|
|
1080
|
+
* The trace cannot name the file that made the call: a real HAR entry carries the request, response,
|
|
1081
|
+
* `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
|
|
1082
|
+
* the shared client and the auth bond instead of guessing a file.
|
|
1083
|
+
*
|
|
1084
|
+
* @param request - The refused request, a 401 or 403 on the app's own origin.
|
|
1085
|
+
* @returns The sentence the note appends.
|
|
1086
|
+
*/
|
|
1087
|
+
function authRefusalSentence(request) {
|
|
1088
|
+
const label = requestLabel(request);
|
|
1089
|
+
// The header's scheme, when it parsed; presence alone is still the server's half of the wire.
|
|
1090
|
+
const carried = request.authorization !== undefined
|
|
1091
|
+
? `with \`Authorization: ${request.authorization} …\``
|
|
1092
|
+
: 'with an `Authorization` header';
|
|
1093
|
+
if (request.status === 403) {
|
|
1094
|
+
return request.sentAuthorization === true
|
|
1095
|
+
? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
|
|
1096
|
+
'accepted but this user may not do it: read the route’s role/permission check in your ' +
|
|
1097
|
+
'own source, and the account the session belongs to.'
|
|
1098
|
+
: `The browser sent \`${label}\` and the API answered 403 — the request carried no ` +
|
|
1099
|
+
'`Authorization` header, so either the client did not attach the token (check that the ' +
|
|
1100
|
+
'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
|
|
1101
|
+
'token is stored where the client reads it) or the route refuses this user: read the ' +
|
|
1102
|
+
'route’s role/permission check in your own source.';
|
|
1103
|
+
}
|
|
1104
|
+
if (request.sentAuthorization === true) {
|
|
1105
|
+
return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
|
|
1106
|
+
'rejected — the scheme (the client’s `authScheme`, e.g. `Token` vs `Bearer`) or the ' +
|
|
1107
|
+
'token’s issuer differs from what the server verifies; read the verify middleware this ' +
|
|
1108
|
+
'route reaches.');
|
|
1109
|
+
}
|
|
1110
|
+
return (`The browser sent \`${label}\` with NO \`Authorization\` header — the client did not attach ` +
|
|
1111
|
+
'the token: check that the request goes through the app’s HTTP client that the auth bond ' +
|
|
1112
|
+
'configures (not a bare `fetch`), and that the token is stored where the client reads it. ' +
|
|
1113
|
+
'The shared client carries it through `setAuthToken` — the auth bond ' +
|
|
1114
|
+
'(`createDefaultAuthClientWithHttpSync` in `@molecule/app-bonds-default-react`) mirrors the ' +
|
|
1115
|
+
'auth client’s token there, and a session store of your own has to call it too. ' +
|
|
1116
|
+
(request.sentCookie
|
|
1117
|
+
? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
|
|
1118
|
+
'that is the credential the server did not accept. '
|
|
1119
|
+
: '') +
|
|
1120
|
+
'The API handler is not the problem.');
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* The note's credential-refusal advice, one sentence per distinct shape — at most two.
|
|
1124
|
+
*
|
|
1125
|
+
* The shapes are `(status, Authorization scheme, cookie present)`: m148's ten walks repeated one
|
|
1126
|
+
* shape (`401`, no header) on every failing test, and repeating its sentence per row would bury
|
|
1127
|
+
* the fact rather than state it.
|
|
1128
|
+
*
|
|
1129
|
+
* @param items - The traces the note is about.
|
|
1130
|
+
* @returns The sentences to append (each led by a space), or '' when no same-origin 401/403 was
|
|
1131
|
+
* traced.
|
|
1132
|
+
*/
|
|
1133
|
+
function authRefusalAdvice(items) {
|
|
1134
|
+
const sentences = [];
|
|
1135
|
+
const seen = new Set();
|
|
1136
|
+
for (const item of items) {
|
|
1137
|
+
for (const request of item.requests) {
|
|
1138
|
+
if (request.status !== 401 && request.status !== 403)
|
|
1139
|
+
continue;
|
|
1140
|
+
if (sameOriginPath(request) === null)
|
|
1141
|
+
continue;
|
|
1142
|
+
const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
|
|
1143
|
+
if (seen.has(key))
|
|
1144
|
+
continue;
|
|
1145
|
+
seen.add(key);
|
|
1146
|
+
sentences.push(authRefusalSentence(request));
|
|
1147
|
+
if (sentences.length === 2)
|
|
1148
|
+
return ` ${sentences.join(' ')}`;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
|
|
1152
|
+
}
|
|
1034
1153
|
/**
|
|
1035
1154
|
* The API path prefixes the traced requests themselves show.
|
|
1036
1155
|
*
|
|
@@ -1162,6 +1281,15 @@ function missingApiPrefixSentence(request) {
|
|
|
1162
1281
|
* request that never reached the API. The prefix is READ (the caller passes what the app's client
|
|
1163
1282
|
* config names; the traces add what their own requests show) and never assumed to be `/api`.
|
|
1164
1283
|
*
|
|
1284
|
+
* A 401/403 on the app's own origin is answered from the request's own credential record instead
|
|
1285
|
+
* ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
|
|
1286
|
+
* token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
|
|
1287
|
+
* header the API then refused moves the answer to the server. The handler sentence is kept for
|
|
1288
|
+
* the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
|
|
1289
|
+
* trace that proves the header was never sent cannot be explained by the handler.
|
|
1290
|
+
* xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
|
|
1291
|
+
* `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
|
|
1292
|
+
*
|
|
1165
1293
|
* @param items - The traces {@link parseTraceArtifacts} read.
|
|
1166
1294
|
* @param target - Where the run went — the deployed host, as the note names it.
|
|
1167
1295
|
* @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
|
|
@@ -1212,6 +1340,11 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1212
1340
|
const prefixAdvice = missingPrefix.length > 0
|
|
1213
1341
|
? ` ${missingPrefix.slice(0, 2).map(missingApiPrefixSentence).join(' ')}`
|
|
1214
1342
|
: '';
|
|
1343
|
+
// A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
|
|
1344
|
+
// own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
|
|
1345
|
+
const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
|
|
1346
|
+
const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
|
|
1347
|
+
const authAdvice = authRefusalAdvice(traced);
|
|
1215
1348
|
// The document subresources that never finished anywhere in this read — the assets a timed-out
|
|
1216
1349
|
// navigation was waiting on, when there is no failed request to explain the timeout.
|
|
1217
1350
|
const stalledAssets = [
|
|
@@ -1352,9 +1485,14 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1352
1485
|
(anyFailed
|
|
1353
1486
|
? everyFailureLacksPrefix
|
|
1354
1487
|
? ''
|
|
1355
|
-
:
|
|
1356
|
-
|
|
1357
|
-
|
|
1488
|
+
: everyFailureAuth
|
|
1489
|
+
? // Every failure was a credential refusal: the handler is not the problem, and the
|
|
1490
|
+
// auth advice already names what is (m148 was sent to the handler six walks running).
|
|
1491
|
+
authAdvice
|
|
1492
|
+
: 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
|
|
1493
|
+
'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
|
|
1494
|
+
'user action silently — log it.' +
|
|
1495
|
+
authAdvice
|
|
1358
1496
|
: stalledAssets.length > 0
|
|
1359
1497
|
? 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
|
|
1360
1498
|
'not finish loading because its own subresource never answered. Do not read this as a ' +
|