@ai-ecoverse/slicc-browsers 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -270,7 +270,13 @@ of supported names. `capabilities.scope` selects owner-specific rows.
270
270
  Map `ordinary`, `ordinary-same-site`, `seven`, `vault`, `vault-x`, `apex`, `own`
271
271
  and `client` to origins. Optionally map `plain`, `cross` and `log`; their defaults
272
272
  are `ordinary`, `client` and `ordinary`. Origins must preserve the site
273
- relationships of the data. Tags and cookie names are unique per leg. `variant`
273
+ relationships of the data. Explicitly map an unavailable target to `null`: every
274
+ row needing that origin is reported as `skip` for control, guarded and live, with
275
+ reason `not-applicable:no-<target>-origin`; no substitute origin is used and the
276
+ row never passes. A guard whose whole site is protected, such as the extension
277
+ where every `sliccy.ai` host is protected, passes `ordinary-same-site: null`.
278
+ This also applies to origins referenced in setup, triggers, observations and
279
+ liveness checks, not only the row target. Tags and cookie names are unique per leg. `variant`
274
280
  selects one variant or an array; omission selects all. `parallel` defaults to 1.
275
281
  `log(row)` receives completed rows. A variant supplies the wait before an
276
282
  explicit `Target.detachFromTarget` step and for a `settle.variant` step.
@@ -301,7 +307,13 @@ observations: a missing or failing effect fails the control. Guarded liveness
301
307
  requires the exact target URL in the fixture log or a target-origin `/live`
302
308
  heartbeat; alternatively a pre-action `/live?tag=...&step=pre&to=...` request and
303
309
  an exact-URL block/error from the client or unguarded frame tree. An error for a
304
- different URL is not evidence. A declared setup refusal can supply liveness only
310
+ different URL is not evidence. `blocked` accepts either `{ url }` for one exact URL
311
+ or `{ anyOf: [url, ...] }` for a nonempty list of exact URLs; each URL can be a
312
+ string or `{ hops, to, index? }`. Both forms allow `session` and `frame`. Redirect
313
+ vectors list every nested hop URL and the final destination because Chromium
314
+ reports the URL intercepted by the guard. A matching block still requires the
315
+ pre-action beacon alongside it; an unrelated error cannot certify the chain.
316
+ A declared setup refusal can supply liveness only
305
317
  when its `refusal.match` matched. Transport connectivity and command completion
306
318
  alone never prove liveness. Cookie and storage rows seed a canary outside the
307
319
  guard and read it back outside the guard; refused writes require unchanged
@@ -319,7 +331,12 @@ fixture log. A setup beacon on the same origin and tag cannot settle an unfinish
319
331
  redirect chain. This does not change the observations used to judge either leg.
320
332
 
321
333
  The separately exported Node module `@ai-ecoverse/slicc-browsers/fixture` exposes
322
- `fixtureServer({ host, port })`, defaulting to a random loopback port. It returns
334
+ `fixtureServer({ host, port, tls })`, defaulting to a random loopback port.
335
+ Pass `tls: { key, cert }` to serve HTTPS using Node TLS key and certificate
336
+ values. The returned origin, request logs, redirect URLs and document heartbeats
337
+ use `https://<Host>` with TLS, so `served`, `hops` and `liveCheck` compare against
338
+ the mapped HTTPS origins, including an extension client at `https://*.sliccy.ai`.
339
+ Without `tls`, the server and logged URLs remain HTTP. It returns
323
340
  `origin`, `port`, `log()`, `reset()` and `close()`. Log entries contain `url`,
324
341
  `method`, `headers` and `time`. Mirror that API when using another fixture.
325
342
  The server serves document, frame, redirect, CSP, authentication, data, image,
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
+ import { createServer as createSecureServer } from 'node:https';
2
3
 
3
4
  const tall = Buffer.from(
4
5
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
@@ -75,10 +76,11 @@ function route(req, url, send, html) {
75
76
  return send(404, 'missing', 'text/plain');
76
77
  }
77
78
 
78
- export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
79
+ export async function fixtureServer({ host = '127.0.0.1', port = 0, tls } = {}) {
79
80
  let entries = [];
80
- const server = createServer((req, res) => {
81
- const url = new URL(req.url, `http://${req.headers.host}`);
81
+ const scheme = tls ? 'https' : 'http';
82
+ const handler = (req, res) => {
83
+ const url = new URL(req.url, `${scheme}://${req.headers.host}`);
82
84
  entries.push({
83
85
  url: url.href,
84
86
  method: req.method,
@@ -104,7 +106,8 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
104
106
  headers
105
107
  );
106
108
  route(req, url, send, html);
107
- });
109
+ };
110
+ const server = tls ? createSecureServer(tls, handler) : createServer(handler);
108
111
  await new Promise((resolve, reject) => {
109
112
  server.once('error', reject);
110
113
  server.listen(port, host, resolve);
@@ -112,7 +115,7 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
112
115
  const address = server.address();
113
116
  return {
114
117
  port: address.port,
115
- origin: `http://${host}:${address.port}`,
118
+ origin: `${scheme}://${host}:${address.port}`,
116
119
  log: () => structuredClone(entries),
117
120
  reset: () => {
118
121
  entries = [];
package/guard-runner.mjs CHANGED
@@ -245,7 +245,7 @@ class Execution {
245
245
  }
246
246
 
247
247
  async blocked(spec, observer) {
248
- const url = address(spec.url);
248
+ const urls = (spec.anyOf ?? [spec.url]).map(address);
249
249
  const events = this.client.events.slice(this.eventStart);
250
250
  if (observer === 'client') {
251
251
  const requests = new Map(
@@ -259,10 +259,10 @@ class Execution {
259
259
  (!spec.frame ||
260
260
  event.params.frame.parentId ||
261
261
  event.sessionId === this.sessions.child) &&
262
- event.params.frame.unreachableUrl === url &&
262
+ urls.includes(event.params.frame.unreachableUrl) &&
263
263
  event.params.frame.url.startsWith('chrome-error://')) ||
264
264
  (event.method === 'Network.loadingFailed' &&
265
- requests.get(event.params.requestId) === url &&
265
+ urls.includes(requests.get(event.params.requestId)) &&
266
266
  (event.params.blockedReason || event.params.errorText === 'net::ERR_BLOCKED_BY_CLIENT'))
267
267
  );
268
268
  }
@@ -279,7 +279,8 @@ class Execution {
279
279
  const visit = (tree) =>
280
280
  Boolean(
281
281
  tree &&
282
- ((tree.frame.unreachableUrl === url && tree.frame.url.startsWith('chrome-error://')) ||
282
+ ((urls.includes(tree.frame.unreachableUrl) &&
283
+ tree.frame.url.startsWith('chrome-error://')) ||
283
284
  (tree.childFrames ?? []).some(visit))
284
285
  );
285
286
  return (
@@ -624,6 +625,7 @@ class Execution {
624
625
  );
625
626
  if (c.refused || c.reached) return this.callCheck(c);
626
627
  if (c.detached) {
628
+ const events = this.client.events.slice(this.eventStart, this.cleanupEventStart);
627
629
  const session = this.sessions[c.detached];
628
630
  return (
629
631
  Boolean(session) &&
@@ -832,6 +834,18 @@ const inactive = (vector, target, capabilities) =>
832
834
  !supported(vector.requires, capabilities) ||
833
835
  (vector.scope !== 'any' && vector.scope !== capabilities.scope);
834
836
 
837
+ function unavailableOrigin(vector, target, targets = {}) {
838
+ const defaults = { plain: 'ordinary', cross: 'client', log: 'ordinary', own: 'client' };
839
+ const references = [target, 'plain', vector.control.origin];
840
+ for (const match of q([vector.setup, vector.trigger, vector.observe, vector.liveCheck]).matchAll(
841
+ /\{([A-Za-z][A-Za-z-]*)\}/g
842
+ ))
843
+ references.push(match[1] === 'beacon' || match[1] === 'probe' ? 'log' : match[1]);
844
+ return references
845
+ .map((name) => (targets[name] === undefined ? (defaults[name] ?? name) : name))
846
+ .find((name) => targets[name] === null);
847
+ }
848
+
835
849
  function variables(targets, target, side, slot, index) {
836
850
  const tag = `guard-${Date.now()}-${slot}-${index}-${side}-${Math.random().toString(36).slice(2)}`;
837
851
  const vars = {
@@ -860,6 +874,15 @@ async function executeRow(job, opts, clients, set, slot, index) {
860
874
  via: vector.control.via,
861
875
  transportVariant: clients.transportVariant === true,
862
876
  };
877
+ const unavailable = unavailableOrigin(vector, target, opts.targets);
878
+ if (unavailable)
879
+ return {
880
+ ...base,
881
+ control: 'skip',
882
+ guarded: 'skip',
883
+ live: 'skip',
884
+ reason: `not-applicable:no-${unavailable}-origin`,
885
+ };
863
886
  if (vector.kind === 'text')
864
887
  return {
865
888
  ...base,
@@ -917,6 +940,27 @@ async function executeRow(job, opts, clients, set, slot, index) {
917
940
  return row;
918
941
  }
919
942
 
943
+ const invalidBlocked = (check) =>
944
+ Boolean(
945
+ check?.blocked &&
946
+ ((check.blocked.url === undefined) === (check.blocked.anyOf === undefined) ||
947
+ (check.blocked.anyOf !== undefined &&
948
+ (!Array.isArray(check.blocked.anyOf) || check.blocked.anyOf.length === 0)))
949
+ ) ||
950
+ [...(check?.all ?? []), ...(check?.any ?? []), ...(check?.not ? [check.not] : [])].some(
951
+ invalidBlocked
952
+ );
953
+
954
+ function validateBlocked(vectors) {
955
+ for (const vector of vectors)
956
+ if (
957
+ [vector.liveCheck, ...Object.values(vector.observe ?? {}).map((o) => o.check)].some(
958
+ invalidBlocked
959
+ )
960
+ )
961
+ throw new TypeError(`${vector.id}: blocked needs exactly one url or nonempty anyOf`);
962
+ }
963
+
920
964
  function validate(set, opts) {
921
965
  if (!opts.fixture?.log) throw new TypeError('fixture.log is required');
922
966
  for (const vector of set.vectors)
@@ -933,6 +977,7 @@ function validate(set, opts) {
933
977
  for (const vector of set.vectors)
934
978
  if (vector.kind === 'cdp' && invalidLive(vector.liveCheck))
935
979
  throw new TypeError(`${vector.id}: liveCheck needs guarded attempt evidence`);
980
+ validateBlocked(set.vectors);
936
981
  const variants = Array.isArray(opts.variant) ? opts.variant : [opts.variant];
937
982
  for (const name of variants)
938
983
  if (!(name in set.variants)) throw new TypeError(`Unknown variant: ${name}`);