@aloud/runner 0.3.1 → 0.3.3

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.
@@ -145,7 +145,7 @@ export async function executeLease(deps: ExecuteDeps): Promise<ExecuteResult> {
145
145
  const workers = new GuardedWorkerFactory(
146
146
  deps.workers ?? new PlaywrightWorkerFactory(),
147
147
  effective.allowedHosts,
148
- deps.local,
148
+ { ...deps.local, allowPrivateNetwork: effective.allowPrivateNetwork },
149
149
  );
150
150
 
151
151
  coordinator = new RunCoordinator(
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  evaluateTargetUrl,
3
+ hostPermitted,
3
4
  intersectHosts,
4
5
  leasePermits,
5
6
  normaliseHosts,
7
+ targetNetworkAccess,
6
8
  type JobLease,
7
9
  type StudySnapshot,
8
10
  } from "@aloud/core";
@@ -31,6 +33,8 @@ export class SnapshotRefused extends Error {
31
33
  export interface SanitisedSnapshot {
32
34
  snapshot: StudySnapshot;
33
35
  allowedHosts: string[];
36
+ /** False for an automatic public grant, which keeps DNS rebinding out of the local network. */
37
+ allowPrivateNetwork: boolean;
34
38
  }
35
39
 
36
40
  export async function sanitiseSnapshot(
@@ -44,38 +48,56 @@ export async function sanitiseSnapshot(
44
48
  const granted = normaliseHosts(lease.allowedHosts);
45
49
  const permitted = normaliseHosts(local.allowedHosts);
46
50
 
47
- // The server already did this intersection. Doing it again here is the point: a server that got
48
- // it wrong, or lied about it, changes nothing on this machine.
49
- const effective = intersectHosts(granted, permitted);
50
- if (effective.length === 0) {
51
- throw new SnapshotRefused(
52
- `This study wants ${granted.join(", ") || "nothing"}, and this machine allows ` +
53
- `${permitted.join(", ") || "nothing"}. Nothing in common, so nothing will be opened. ` +
54
- "Add a host with `aloud allow <host>` if that is wrong.",
55
- );
56
- }
57
-
58
51
  const startUrl = snapshot.study?.startUrl;
59
52
  if (typeof startUrl !== "string" || startUrl.length === 0) {
60
53
  throw new SnapshotRefused("This study has no start URL.");
61
54
  }
62
55
 
63
- // Checked against the *effective* list, not the one the lease claimed.
64
- const verdict = leasePermits({ ...lease, allowedHosts: effective }, startUrl, nowIso);
56
+ // The lease is still the outer boundary. Automatic access never invents a host the lease did not
57
+ // name; it only decides whether that lease host needs a durable local permission as well.
58
+ const verdict = leasePermits({ ...lease, allowedHosts: granted }, startUrl, nowIso);
65
59
  if (!verdict.allowed) throw new SnapshotRefused(verdict.reason);
66
60
 
61
+ const access = await targetNetworkAccess(startUrl, resolver);
62
+ if (access.kind === "blocked") throw new SnapshotRefused(access.reason);
63
+
64
+ let effective: string[];
65
+ let allowPrivateNetwork: boolean;
66
+ if (access.kind === "public") {
67
+ // A covering lease entry proves the start host was in scope, but the browser receives only the
68
+ // exact start hostname. A hostile lease cannot smuggle an unrelated domain—or a broad parent
69
+ // that grants sibling subdomains—into the automatic browser policy.
70
+ effective = granted.some((entry) => hostPermitted(access.hostname, [entry]))
71
+ ? [access.hostname]
72
+ : [];
73
+ allowPrivateNetwork = false;
74
+ } else {
75
+ // Private and loopback access still needs both halves of the durable permission. The server can
76
+ // offer it, but cannot make this intersection non-empty.
77
+ effective = intersectHosts(granted, permitted);
78
+ allowPrivateNetwork = local.allowPrivateNetwork;
79
+ }
80
+ if (effective.length === 0) {
81
+ throw new SnapshotRefused(
82
+ `This private or local study wants ${granted.join(", ") || "nothing"}, and this machine allows ` +
83
+ `${permitted.join(", ") || "nothing"}. Nothing in common, so nothing will be opened. ` +
84
+ "Add the private host with `aloud allow <host>` if that is intentional.",
85
+ );
86
+ }
87
+
67
88
  // And then the same check the browser worker will make, so a refusal happens before anything is
68
89
  // launched rather than three browsers in. Cloud metadata endpoints are refused unconditionally
69
90
  // in here, regardless of the private-network setting.
70
91
  const target = await evaluateTargetUrl(startUrl, {
71
92
  allowedDomains: effective,
72
- allowPrivateNetwork: local.allowPrivateNetwork,
93
+ allowPrivateNetwork,
73
94
  ...(resolver ? { resolver } : {}),
74
95
  });
75
96
  if (!target.allowed) throw new SnapshotRefused(target.reason);
76
97
 
77
98
  return {
78
99
  allowedHosts: effective,
100
+ allowPrivateNetwork,
79
101
  snapshot: {
80
102
  ...snapshot,
81
103
  environment: {
package/src/ui/output.ts CHANGED
@@ -39,7 +39,8 @@ export class TerminalReporter implements RunReporter {
39
39
  this.say("");
40
40
  this.say(` Chromium ${input.chromium ? GREEN + "ready" + RESET : YELLOW + "not installed yet" + RESET}`);
41
41
  this.say(` Connection ${input.server}`);
42
- this.say(` Allowed here ${input.allowedHosts.join(", ") || "nothing yet - run `aloud allow <host>`"}`);
42
+ this.say(` Public sites ${GREEN}automatic per study${RESET}`);
43
+ this.say(` Private/local ${input.allowedHosts.join(", ") || "nothing approved"}`);
43
44
  this.say("");
44
45
  }
45
46
 
package/src/version.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * package.json beside it to read, and importing one into the source trips the composite build's
11
11
  * rootDir. `version.test.ts` asserts this matches, so the drift this invites cannot survive CI.
12
12
  */
13
- export const RUNNER_VERSION = "0.3.1";
13
+ export const RUNNER_VERSION = "0.3.3";
14
14
 
15
15
  /** The header the server reads it from. */
16
16
  export const RUNNER_VERSION_HEADER = "x-aloud-runner-version";