@ai-ecoverse/slicc-browsers 1.12.0 → 1.13.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 +9 -1
- package/corpus.json +91 -28
- package/fixture-server.mjs +23 -3
- package/guard-runner.mjs +25 -4
- package/guard-vectors.json +645 -1
- package/package.json +7 -4
- package/redirect-fragments.mjs +38 -0
- package/redirect-secret.mjs +466 -0
- package/schema/browsers.schema.json +5 -1
- package/schema/guard-vectors.schema.json +9 -0
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ A same-site frame shares its parent's process and has no target of its own. Ever
|
|
|
169
169
|
|
|
170
170
|
**Storage keys** (`Storage`, `IndexedDB`, `CacheStorage` and `FileSystem` in CDP): a key is protected when its origin (up to the first `^`) or its top-level site (a part after `^0`) is a protected URL.
|
|
171
171
|
|
|
172
|
+
**Redirect secrets.** An allowed response can name a protected destination without becoming a protected document itself. Every protected URL in a nested delivered string or object key is redacted in place to its origin, including embedded `Location`, `Content-Location`, `Refresh` and `Link` values, raw `headersText` lines, `redirectResponse`, Page URL/history/error fields, console/exception text and Audits issues. Percent-encoded representations must not retain the tail. A separate `urlFragment` alongside a protected `url` is cleared too. The event remains delivered so Network ExtraInfo pairing survives; dropping an ExtraInfo event fails these vectors. A request whose response names a protected URL has its response body refused (`slicc: refused:`), through both Fetch and Network, even though the request started at an ordinary URL. Body reads, replay and URL overrides must not follow an ordinary redirect into protected content or send its cookies. Ordinary redirect headers and bodies stay readable.
|
|
173
|
+
|
|
172
174
|
**Refusals** carry the text in `refusal`, `slicc: refused: target is on seven's origin`.
|
|
173
175
|
|
|
174
176
|
**`protected.json`** (schema `schema/protected.schema.json`): `errorPage` (the committed-error previous-decision and origin-redaction rule), `unparseable` (`protected`) and `unparseableRefusal`, in step with the catalog; `origins`, the extra origins every vector runs with; `urls` (`url`, `protected`); `targets` (`url`, `documentURL?` and `unreachableURL?` for validated main-frame error-commit evidence, `navigating?` for a main-frame navigation in flight; `type?`, the target's `TargetInfo.type`; `previous?`, whether the target was protected before; `opener?`, `protected`, `unprotected` or `unknown`, and no `opener` for a target without one; `protected`); `cookieDomains` (`domain`, `protected`); `storageKeys` (`key`, `protected`). `npm run validate` runs every vector through `protected.mjs`, and no vector uses a port from 9222 to 9229.
|
|
@@ -186,6 +188,10 @@ The `C9` foreign extension isolated-world family requires `extensionWorld`: a br
|
|
|
186
188
|
|
|
187
189
|
The integration launcher loads shared-web 1.25.2’s harness extension exactly once through `Extensions.loadUnpacked` (`ankmbodmgimjemnojmeddfcdlbocolob`), whose content script runs on HTTP(S) pages. shared-web 1.25.2 also loads it by default in its own harness. A transport unable to host a foreign content-script extension omits `extensionWorld` and reports `not-supported:extensionWorld`; an advertised capability with no observed world fails, rather than silently skipping.
|
|
188
190
|
|
|
191
|
+
`C2.redirect-secret` probes ordinary fixture 302s to seven’s `/auth/callback?code=…&state=…` and vault’s `/oauth/mcp-callback?code=…`, plus `#access_token=…` variants of both. It navigates with Page and scripted `location.href`, fetches a subresource and sends an XHR. The 302 carries `Location`, `Content-Location`, `Refresh: 0;url=…` and `Link: <…>; rel=preconnect`. Page, Network (including ExtraInfo), Runtime, Log, Audits and both Fetch stages are enabled. Every received raw message is scanned, including replies, across an eight-second tail, for raw, percent-encoded, JSON-escaped and base64 marker representations. Both response-body APIs are attempted on each hop; guarded bodies need matched refusals. Page frame trees, resource trees and navigation history are read before recovery to catch error-page leftovers. A separate structural check requires every protected `Network.Request.urlFragment` and `Page.Frame.urlFragment` to be empty or absent, even if its value contains no marker; the ordinary `#access_token` control independently requires fragment evidence in request events, frame events, frame-tree replies and resource-tree replies. The ordinary session must survive. The unguarded ordinary redirect control preserves all four headers in `redirectResponse`, ExtraInfo’s headers and `headersText`, and Fetch’s response headers. `C2.redirect-secret-mix` repeats that matrix with 301/303/307/308. The protected leg requires in-place ExtraInfo delivery with origin-only header values, not merely the absence of a marker.
|
|
192
|
+
|
|
193
|
+
`C2.redirect-body` calls `Network.loadNetworkResource` on an ordinary redirect with `includeCredentials: true`, reads any stream with `IO.read`, replays a redirected XHR, requests Page resource content, and captures Page/MHTML and DOM snapshots after a main-frame redirect. It also captures an ordinary parent containing an iframe redirected to vault, including its error document, and overrides a paused ordinary Fetch request to the allowed redirect URL. No protected marker may appear in any message or body, and no request from those operations may send the independently seeded seven cookie. The ordinary control must return the body, resource content and snapshots, and accept the URL override. Each probe cleans up its redirect registrations, observer session and cookie. The focused Chrome tests use HTTPS with SPKI-pinned fixture trust and default-port protected URLs; host resolver rules route seven and vault to the fixture rather than the public service.
|
|
194
|
+
|
|
189
195
|
`C8.unparseable` probes both raw and canonical space/U+200D/invalid-punycode URLs through creation and navigation. Every guarded destination needs a matched refusal. Its unguarded controls record `document` or `native-rejection` outcomes, including Chrome builds that reject raw U+200D instead of committing it. Both raw and encoded-space forms must commit real fixture documents, because that is the proven cookie leak. An independent observer loads the encoded-space document and confirms it shares a uniquely seeded `.sliccy.ai` cookie; the guarded client must neither list nor attach to it, read its cookie, query that cookie by URL, nor cause a cookie-bearing destination request. The guard does not erase the observer’s private browser state. The row runs on both browser-session and page-session-only transports, uses the default context, and cleans up its own targets and cookie. Cookie seeding always uses the unguarded observer’s browser session, never the guarded client. Only guarded browser-session target listing requires `browserSession`; without it, `listed` records `not-supported` explicitly in probe and observation evidence, and the result lists that field in `notSupported`. Creation, navigation, attachment and `Network.getCookies` with `urls` still require matched refusals; filtering cookies without refusing fails `cookieRefused`. Fixture URLs omit the colon when the port is empty, and reported URLs are compared after canonicalisation, including Chrome’s WHATWG-unparseable encoded-space hosts. Launchers must resolve the raw/canonical space, U+200D/punycode and invalid-punycode `.sliccy.ai` fixture hosts to the fixture server, including for default-port HTTP or HTTPS origins. The HTTP fixture accepts Chrome’s encoded-space Host header without reparsing it as a WHATWG origin.
|
|
190
196
|
|
|
191
197
|
`C8.chrome-search` requests `chrome-search://most-visited/` through `Page.navigate` and requires an explicit guard refusal. Its unguarded control requires a CDP response, not a successful document commit: Chrome can answer with `errorText` when that internal data source is unavailable outside an Instant process. Browser-side load failure is not a guard refusal, and merely hiding the target must fail this row.
|
|
@@ -255,6 +261,8 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
|
|
|
255
261
|
|
|
256
262
|
`{ "errorPage": "recover", "secret"?: "all" | "query" | "fragment", "as": "errorPage" }` (also modes `restore` and `sticky`; `secret` defaults to `all`) runs the protected-error-page compound probe with independent commit evidence, discovery redaction and recovery or sticky-lock checks. The unguarded observer seeds the protected history and failed commit, so the rows run on page-session-only transports too. The guarded client uses only `Target.getTargetInfo` with a `targetId`, `Target.getTargets`, `Target.setDiscoverTargets`, `Target.attachToTarget`, `Target.detachFromTarget` and `Target.closeTarget` without a session, which page-session transports answer themselves, and everything else on its page session; the rows require no capability. The runner leaves this probe’s independent Fetch sessions to the probe rather than answering their pauses itself.
|
|
257
263
|
|
|
264
|
+
`{ "redirectSecret": "headers" | "mix" | "body", "as": "redirect" }` runs a C2 compound probe and owns its Fetch pauses. The client must expose `messages`, an append-only array of every raw received CDP text frame, recorded before parsing; an events-only or reply-only tap fails `rawRecorded`. The fixture must implement `redirectSecret({ path, host?, search?, to?, code?, body, remove? })`: registering an allowed hop without putting the protected marker in the initiating URL, and registering the callback body on the protected host. `fixtureServer()` supplies it. Fixture logs must include headers for the cookie check. The guard’s events and raw messages must represent the same delivered stream.
|
|
265
|
+
|
|
258
266
|
`{ "unparseable": true, "as": "unparseable" }` runs the C8 malformed-URL compound probe on the current ordinary page: independent browser ground truth, raw/canonical destination commands, discovery/attach and cookie checks. Boolean evidence is consumed by ordinary `result` checks, with native Chrome outcomes retained in its diagnostics.
|
|
259
267
|
|
|
260
268
|
**The control rule.** Every vector has a control on an ordinary origin: the same setup and trigger against an unprotected origin must succeed. A missing or failing control fails the id. With `via: "guard"`, the control runs through the guard. With `via: "unguarded"`, the runner runs setup and trigger through its own CDP against the ordinary origin and against the protected stand-in, and both must show the effect. The runner reports the control's transport as `via`.
|
|
@@ -544,7 +552,7 @@ A six endpoint has tier `six`, whatever its seven tabs. **A program never choose
|
|
|
544
552
|
|
|
545
553
|
## Output schema 1
|
|
546
554
|
|
|
547
|
-
`schema/browsers.schema.json` describes `slicc browsers --json`, which `playwright-cli list --json` also prints. [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; `source` is one of `devtools-active-port`, `flag`, `probe` and `process`. `tier` is 1 to 6, and `six: true` marks an endpoint in SLICC six's profile (tier 6). An endpoint may carry `tabs`: every page target except those on protected origins. Seven's own tabs are protected, so they are in `sevenTabs` only, never in `tabs`. `playwright-cli list --json` prints it, and a corpus case that checks it needs `tabs`.
|
|
555
|
+
`schema/browsers.schema.json` describes `slicc browsers --json`, which `playwright-cli list --json` also prints. [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; `source` is one of `devtools-active-port`, `flag`, `probe` and `process`. `tier` is 1 to 6, and `six: true` marks an endpoint in SLICC six's profile (tier 6). An endpoint may carry `tabs`: every page target except those on protected origins. Seven's own tabs are protected, so they are in `sevenTabs` only, never in `tabs`. In `sevenTabs`, `url` and `title` are the origin only, never a path, query, fragment or document title, including configured extra seven origins and default-port normalization. `playwright-cli list --json` prints it, and a corpus case that checks it needs `tabs`.
|
|
548
556
|
|
|
549
557
|
## The corpus
|
|
550
558
|
|
package/corpus.json
CHANGED
|
@@ -761,8 +761,8 @@
|
|
|
761
761
|
"sevenTabs": [
|
|
762
762
|
{
|
|
763
763
|
"id": "P1",
|
|
764
|
-
"url": "https://lars.sliccy.ai
|
|
765
|
-
"title": "
|
|
764
|
+
"url": "https://lars.sliccy.ai",
|
|
765
|
+
"title": "https://lars.sliccy.ai"
|
|
766
766
|
}
|
|
767
767
|
],
|
|
768
768
|
"tabs": [
|
|
@@ -854,13 +854,13 @@
|
|
|
854
854
|
"sevenTabs": [
|
|
855
855
|
{
|
|
856
856
|
"id": "O1",
|
|
857
|
-
"url": "https://dev.example.test
|
|
858
|
-
"title": "dev"
|
|
857
|
+
"url": "https://dev.example.test",
|
|
858
|
+
"title": "https://dev.example.test"
|
|
859
859
|
},
|
|
860
860
|
{
|
|
861
861
|
"id": "O3",
|
|
862
|
-
"url": "https://dev.example.test
|
|
863
|
-
"title": "dev
|
|
862
|
+
"url": "https://dev.example.test",
|
|
863
|
+
"title": "https://dev.example.test"
|
|
864
864
|
}
|
|
865
865
|
]
|
|
866
866
|
}
|
|
@@ -1298,8 +1298,8 @@
|
|
|
1298
1298
|
"sevenTabs": [
|
|
1299
1299
|
{
|
|
1300
1300
|
"id": "P1",
|
|
1301
|
-
"url": "https://lars.sliccy.ai
|
|
1302
|
-
"title": "
|
|
1301
|
+
"url": "https://lars.sliccy.ai",
|
|
1302
|
+
"title": "https://lars.sliccy.ai"
|
|
1303
1303
|
}
|
|
1304
1304
|
]
|
|
1305
1305
|
}
|
|
@@ -1713,8 +1713,8 @@
|
|
|
1713
1713
|
"sevenTabs": [
|
|
1714
1714
|
{
|
|
1715
1715
|
"id": "S1",
|
|
1716
|
-
"url": "https://b.sliccy.ai
|
|
1717
|
-
"title": "
|
|
1716
|
+
"url": "https://b.sliccy.ai",
|
|
1717
|
+
"title": "https://b.sliccy.ai"
|
|
1718
1718
|
}
|
|
1719
1719
|
]
|
|
1720
1720
|
}
|
|
@@ -1737,8 +1737,8 @@
|
|
|
1737
1737
|
"sevenTabs": [
|
|
1738
1738
|
{
|
|
1739
1739
|
"id": "S2",
|
|
1740
|
-
"url": "https://a.sliccy.ai
|
|
1741
|
-
"title": "
|
|
1740
|
+
"url": "https://a.sliccy.ai",
|
|
1741
|
+
"title": "https://a.sliccy.ai"
|
|
1742
1742
|
}
|
|
1743
1743
|
]
|
|
1744
1744
|
}
|
|
@@ -2012,8 +2012,8 @@
|
|
|
2012
2012
|
"sevenTabs": [
|
|
2013
2013
|
{
|
|
2014
2014
|
"id": "D1",
|
|
2015
|
-
"url": "https://seven.sliccy.ai
|
|
2016
|
-
"title": "seven"
|
|
2015
|
+
"url": "https://seven.sliccy.ai",
|
|
2016
|
+
"title": "https://seven.sliccy.ai"
|
|
2017
2017
|
}
|
|
2018
2018
|
]
|
|
2019
2019
|
}
|
|
@@ -2039,6 +2039,69 @@
|
|
|
2039
2039
|
]
|
|
2040
2040
|
}
|
|
2041
2041
|
},
|
|
2042
|
+
{
|
|
2043
|
+
"name": "seven-tab-origin-redaction",
|
|
2044
|
+
"case": {
|
|
2045
|
+
"description": "sevenTabs URL and title reveal only the origin, never a secret path, query, fragment or document title",
|
|
2046
|
+
"needs": [
|
|
2047
|
+
"flag"
|
|
2048
|
+
],
|
|
2049
|
+
"cdp": [
|
|
2050
|
+
"127.0.0.1:9531"
|
|
2051
|
+
],
|
|
2052
|
+
"http": {
|
|
2053
|
+
"127.0.0.1:9531": {
|
|
2054
|
+
"version": {
|
|
2055
|
+
"Browser": "Chrome/154.0.8037.98",
|
|
2056
|
+
"Protocol-Version": "1.3",
|
|
2057
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.8037.98 Safari/537.36",
|
|
2058
|
+
"V8-Version": "15.4.79.1",
|
|
2059
|
+
"WebKit-Version": "537.36 (@a1b2c3)",
|
|
2060
|
+
"webSocketDebuggerUrl": "ws://127.0.0.1:9531/devtools/browser/d1"
|
|
2061
|
+
},
|
|
2062
|
+
"list": [
|
|
2063
|
+
{
|
|
2064
|
+
"id": "D1",
|
|
2065
|
+
"type": "page",
|
|
2066
|
+
"url": "https://seven.sliccy.ai/auth/secret-path-marker?code=secret-query-marker#access_token=secret-fragment-marker",
|
|
2067
|
+
"title": "secret-title-marker",
|
|
2068
|
+
"webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/D1"
|
|
2069
|
+
}
|
|
2070
|
+
]
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
"expected": {
|
|
2075
|
+
"schema": 1,
|
|
2076
|
+
"minChromium": 116,
|
|
2077
|
+
"entries": [
|
|
2078
|
+
{
|
|
2079
|
+
"id": "cdp:127.0.0.1:9531",
|
|
2080
|
+
"kind": "endpoint",
|
|
2081
|
+
"tier": 1,
|
|
2082
|
+
"name": "Chrome",
|
|
2083
|
+
"version": "154.0.8037.98",
|
|
2084
|
+
"endpoint": {
|
|
2085
|
+
"cdp": "http://127.0.0.1:9531",
|
|
2086
|
+
"webSocketDebuggerUrl": "ws://127.0.0.1:9531/devtools/browser/d1",
|
|
2087
|
+
"name": "Chrome",
|
|
2088
|
+
"version": "154.0.8037.98",
|
|
2089
|
+
"product": "Chrome/154.0.8037.98",
|
|
2090
|
+
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.8037.98 Safari/537.36",
|
|
2091
|
+
"source": "flag",
|
|
2092
|
+
"sevenOpen": true,
|
|
2093
|
+
"sevenTabs": [
|
|
2094
|
+
{
|
|
2095
|
+
"id": "D1",
|
|
2096
|
+
"url": "https://seven.sliccy.ai",
|
|
2097
|
+
"title": "https://seven.sliccy.ai"
|
|
2098
|
+
}
|
|
2099
|
+
]
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
]
|
|
2103
|
+
}
|
|
2104
|
+
},
|
|
2042
2105
|
{
|
|
2043
2106
|
"name": "seven-tabs",
|
|
2044
2107
|
"case": {
|
|
@@ -2189,13 +2252,13 @@
|
|
|
2189
2252
|
"sevenTabs": [
|
|
2190
2253
|
{
|
|
2191
2254
|
"id": "T1",
|
|
2192
|
-
"url": "https://lars.sliccy.ai
|
|
2193
|
-
"title": "
|
|
2255
|
+
"url": "https://lars.sliccy.ai",
|
|
2256
|
+
"title": "https://lars.sliccy.ai"
|
|
2194
2257
|
},
|
|
2195
2258
|
{
|
|
2196
2259
|
"id": "T6",
|
|
2197
|
-
"url": "https://seven.sliccy.ai
|
|
2198
|
-
"title": "seven
|
|
2260
|
+
"url": "https://seven.sliccy.ai",
|
|
2261
|
+
"title": "https://seven.sliccy.ai"
|
|
2199
2262
|
}
|
|
2200
2263
|
]
|
|
2201
2264
|
}
|
|
@@ -2218,8 +2281,8 @@
|
|
|
2218
2281
|
"sevenTabs": [
|
|
2219
2282
|
{
|
|
2220
2283
|
"id": "V1",
|
|
2221
|
-
"url": "https://team-7.sliccy.ai
|
|
2222
|
-
"title": "
|
|
2284
|
+
"url": "https://team-7.sliccy.ai",
|
|
2285
|
+
"title": "https://team-7.sliccy.ai"
|
|
2223
2286
|
}
|
|
2224
2287
|
]
|
|
2225
2288
|
}
|
|
@@ -2400,8 +2463,8 @@
|
|
|
2400
2463
|
"sevenTabs": [
|
|
2401
2464
|
{
|
|
2402
2465
|
"id": "S1",
|
|
2403
|
-
"url": "https://lars.sliccy.ai
|
|
2404
|
-
"title": "
|
|
2466
|
+
"url": "https://lars.sliccy.ai",
|
|
2467
|
+
"title": "https://lars.sliccy.ai"
|
|
2405
2468
|
}
|
|
2406
2469
|
]
|
|
2407
2470
|
}
|
|
@@ -2522,8 +2585,8 @@
|
|
|
2522
2585
|
"sevenTabs": [
|
|
2523
2586
|
{
|
|
2524
2587
|
"id": "S1",
|
|
2525
|
-
"url": "https://lars.sliccy.ai
|
|
2526
|
-
"title": "
|
|
2588
|
+
"url": "https://lars.sliccy.ai",
|
|
2589
|
+
"title": "https://lars.sliccy.ai"
|
|
2527
2590
|
}
|
|
2528
2591
|
]
|
|
2529
2592
|
}
|
|
@@ -2674,8 +2737,8 @@
|
|
|
2674
2737
|
"sevenTabs": [
|
|
2675
2738
|
{
|
|
2676
2739
|
"id": "S1",
|
|
2677
|
-
"url": "https://lars.sliccy.ai
|
|
2678
|
-
"title": "
|
|
2740
|
+
"url": "https://lars.sliccy.ai",
|
|
2741
|
+
"title": "https://lars.sliccy.ai"
|
|
2679
2742
|
}
|
|
2680
2743
|
]
|
|
2681
2744
|
}
|
|
@@ -2775,8 +2838,8 @@
|
|
|
2775
2838
|
"sevenTabs": [
|
|
2776
2839
|
{
|
|
2777
2840
|
"id": "S1",
|
|
2778
|
-
"url": "https://lars.sliccy.ai
|
|
2779
|
-
"title": "
|
|
2841
|
+
"url": "https://lars.sliccy.ai",
|
|
2842
|
+
"title": "https://lars.sliccy.ai"
|
|
2780
2843
|
}
|
|
2781
2844
|
]
|
|
2782
2845
|
}
|
package/fixture-server.mjs
CHANGED
|
@@ -26,7 +26,20 @@ function trigger(p) {
|
|
|
26
26
|
return `<p>trigger</p><button onclick="go()">Go</button>${frame}<script>const to=${JSON.stringify(to)};function go(){const k=${JSON.stringify(kind)};if(k==='href'||k==='deferred')location.href=to;if(k==='replace')location.replace(to);if(k==='popup')window.open(to);if(k==='form'){const u=new URL(to);const f=document.createElement('form');f.action=u.origin+u.pathname;for(const [name,value] of u.searchParams){const i=document.createElement('input');i.type='hidden';i.name=name;i.value=value;f.append(i);}document.body.append(f);f.submit();}return 'went';}${kind === 'deferred' ? `setTimeout(go,${Number(p.get('defer') ?? 3000)});` : ''}</script>`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function route(req, url, send, html) {
|
|
29
|
+
function route(req, url, send, html, redirects) {
|
|
30
|
+
const secret =
|
|
31
|
+
redirects.get(`${url.hostname}${url.pathname}${url.search ?? ''}`) ??
|
|
32
|
+
redirects.get(`${url.hostname}${url.pathname}`) ??
|
|
33
|
+
redirects.get(url.pathname);
|
|
34
|
+
if (secret) {
|
|
35
|
+
if (!secret.to) return send(200, secret.body, 'text/plain');
|
|
36
|
+
return send(secret.code ?? 302, secret.body, 'text/plain', {
|
|
37
|
+
location: secret.to,
|
|
38
|
+
'content-location': secret.to,
|
|
39
|
+
refresh: `0;url=${secret.to}`,
|
|
40
|
+
link: `<${secret.to}>; rel=preconnect`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
30
43
|
const p = url.searchParams;
|
|
31
44
|
const routes = {
|
|
32
45
|
'/beacon': () => send(204, ''),
|
|
@@ -95,6 +108,7 @@ function requestURL(req, scheme) {
|
|
|
95
108
|
|
|
96
109
|
export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency } = {}) {
|
|
97
110
|
let entries = [];
|
|
111
|
+
const redirects = new Map();
|
|
98
112
|
const scheme = tls ? 'https' : 'http';
|
|
99
113
|
const handler = (req, res) => {
|
|
100
114
|
const url = requestURL(req, scheme);
|
|
@@ -123,8 +137,8 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
|
|
|
123
137
|
headers
|
|
124
138
|
);
|
|
125
139
|
const wait = latency?.(url) ?? 0;
|
|
126
|
-
if (wait > 0) setTimeout(() => route(req, url, send, html), wait);
|
|
127
|
-
else route(req, url, send, html);
|
|
140
|
+
if (wait > 0) setTimeout(() => route(req, url, send, html, redirects), wait);
|
|
141
|
+
else route(req, url, send, html, redirects);
|
|
128
142
|
};
|
|
129
143
|
const server = tls ? createSecureServer(tls, handler) : createServer(handler);
|
|
130
144
|
await new Promise((resolve, reject) => {
|
|
@@ -133,6 +147,12 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency
|
|
|
133
147
|
});
|
|
134
148
|
const address = server.address();
|
|
135
149
|
return {
|
|
150
|
+
redirects,
|
|
151
|
+
redirectSecret: async ({ path, host = '', search = '', remove, ...value }) => {
|
|
152
|
+
const key = `${host}${path}${search}`;
|
|
153
|
+
if (remove) redirects.delete(key);
|
|
154
|
+
else redirects.set(key, value);
|
|
155
|
+
},
|
|
136
156
|
port: address.port,
|
|
137
157
|
origin: `${scheme}://${host}:${address.port}`,
|
|
138
158
|
log: () => structuredClone(entries),
|
package/guard-runner.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
steps,
|
|
15
15
|
} from './guard-driver.mjs';
|
|
16
16
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
17
|
+
import { redirectSecret } from './redirect-secret.mjs';
|
|
17
18
|
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
18
19
|
|
|
19
20
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -623,10 +624,10 @@ class Execution {
|
|
|
623
624
|
);
|
|
624
625
|
if (step.extensionWorld && out.result?.incomplete)
|
|
625
626
|
this.errors.push(`extensionWorld ${step.extensionWorld}: ${out.result.incomplete}`);
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
627
|
+
const probe = ['redirectSecret', 'unparseable', 'errorPage', 'extensionWorld'].find(
|
|
628
|
+
(key) => step[key]
|
|
629
|
+
);
|
|
630
|
+
if (probe && !out.ok) this.errors.push(`${probe}: ${out.error?.message ?? 'probe failed'}`);
|
|
630
631
|
if (step.as && !out.result?.pending) this.calls[step.as] = out;
|
|
631
632
|
if (step.cdp) (this.calls.$cdp ??= []).push({ method: step.cdp, ...out });
|
|
632
633
|
}
|
|
@@ -676,6 +677,26 @@ class Execution {
|
|
|
676
677
|
await this.settle(step.settle);
|
|
677
678
|
return;
|
|
678
679
|
}
|
|
680
|
+
if (step.redirectSecret) {
|
|
681
|
+
this.internalSessions.add(session);
|
|
682
|
+
try {
|
|
683
|
+
return await outcome(
|
|
684
|
+
redirectSecret({
|
|
685
|
+
client: this.client,
|
|
686
|
+
reader: this.reader,
|
|
687
|
+
session,
|
|
688
|
+
targetId: this.pageTarget,
|
|
689
|
+
ordinary: this.vars.ordinary,
|
|
690
|
+
tag: this.vars.tag,
|
|
691
|
+
fixture: this.options.fixture,
|
|
692
|
+
mode: step.redirectSecret,
|
|
693
|
+
side: this.side,
|
|
694
|
+
})
|
|
695
|
+
);
|
|
696
|
+
} finally {
|
|
697
|
+
this.internalSessions.delete(session);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
679
700
|
if (step.errorPage)
|
|
680
701
|
return outcome(
|
|
681
702
|
protectedErrorPage({
|