@specific.dev/spectest 0.72.0 → 0.73.1

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/dist/browser.js CHANGED
@@ -236,11 +236,17 @@ async function applySafeAreaInsets(cdp, d) {
236
236
  // ────────────────────────────────────────────────────────────────────────
237
237
  // rrweb bootstrap
238
238
  // ────────────────────────────────────────────────────────────────────────
239
- // Vendored `@rrweb/record` UMD bundle (rrweb 2.x). Its global `rrwebRecord`
240
- // is a module object — the record function is `rrwebRecord.record` (the
241
- // bootstrap resolves both this and the legacy function-shaped global). Read
242
- // once at module init — the SDK ships this file in the base snapshot so
243
- // no network fetch happens inside the VM.
239
+ // Vendored `@rrweb/record` UMD bundle (rrweb 2.x), BUILT FROM OUR FORK by
240
+ // `scripts/rrweb-record/build.sh` (specific-dev/rrweb, branch `spectest` =
241
+ // the `rrweb@2.1.0` tag + upstream PR 1802). Never copy the npm dist over
242
+ // it: upstream's `@rrweb/utils` lists `querySelector` as an accessor, so its
243
+ // pristine-prototype test always fails and `record()` appends an <iframe>
244
+ // to every document — measured at ~1 s per navigation (see the top-frame
245
+ // gate on PAGE_INIT_SCRIPT below). Its global `rrwebRecord` is a module
246
+ // object — the record function is `rrwebRecord.record` (the bootstrap
247
+ // resolves both this and the legacy function-shaped global). Read once at
248
+ // module init — the SDK ships this file in the base snapshot so no network
249
+ // fetch happens inside the VM.
244
250
  const RRWEB_BUNDLE = (() => {
245
251
  try {
246
252
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -711,8 +717,22 @@ const RRWEB_BOOTSTRAP = `
711
717
  // call expression — the TypeError kills everything after the bundle, so
712
718
  // the globals define but the bootstrap never runs (zero rrweb events,
713
719
  // empty replays).
720
+ //
721
+ // The whole script runs in the TOP frame only. `Page.addScriptToEvaluateOnNewDocument`
722
+ // fires for every frame, and until 2026-09-06 that was a storm: rrweb's
723
+ // `getUntaintedPrototype` appends a hidden <iframe> to read pristine DOM
724
+ // prototypes, this script then ran inside it, the child's `record()` (which
725
+ // treats every frame as an emitting frame unless `recordCrossOriginIframes`
726
+ // is set) appended an iframe of its own, and so on to Chromium's frame-depth
727
+ // cap — 44 nested frames and 45 executions of ~180 KB of JS per navigation,
728
+ // ~0.8–1.3 s inside the page's `load` handler even for a one-line document
729
+ // (`goto` p50 was ~1.5 s in every project). The vendored bundle's fork patch
730
+ // removes the iframe on an unpatched page; this gate bounds it on a page
731
+ // whose prototypes really are patched (zone.js). Nothing is lost: rrweb
732
+ // records same-origin iframes from the parent, and a cross-origin child's
733
+ // events were never drained (they land in the child's own buffer).
714
734
  const PAGE_INIT_SCRIPT = RRWEB_BUNDLE
715
- ? `${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}`
735
+ ? `if (window === window.top) {\n${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}\n}`
716
736
  : "";
717
737
  // Page-side expression that atomically swaps in a fresh buffer and
718
738
  // returns the old one. Wrapped as a single expression so view.evaluate's
package/dist/daemon.js CHANGED
@@ -913,7 +913,6 @@ async function prepareServiceImage(svc, opts) {
913
913
  if (tagr.code !== 0) {
914
914
  throw new Error(`docker tag ${ref} ${tag} failed: ${tagr.stderr.trim()}`);
915
915
  }
916
- await ensureCaTrustedImage(svc.name, tag);
917
916
  return { tag };
918
917
  }
919
918
  // Dockerfile build. Within one bootstrap, identical definitions (shared
@@ -958,61 +957,6 @@ async function prepareServiceImage(svc, opts) {
958
957
  }
959
958
  return buildServiceImage(svc.name, image, tag);
960
959
  }
961
- /** Printed by the folded CA step when it could not write the trust
962
- * store at all — the one outcome that still needs the derivative build. */
963
- const CA_FOLD_UNWRITABLE = "[spectest-ca] trust store not writable";
964
- /**
965
- * The CA-trust steps as a suffix appended to a dockerfile service's OWN
966
- * Dockerfile, so one build produces the finished image instead of a build
967
- * plus a derivative rebuild ({@link ensureCaTrustedImage}) per service.
968
- * Returns null when there is no CA to layer, or when the PEM can't be
969
- * quoted — the caller then falls back to the derivative build.
970
- *
971
- * The PEM is written INLINE rather than `COPY`d: the build context is
972
- * `/workspace` under a per-service ignore file that the project itself
973
- * contributes to (a `**` line with re-includes is the common idiom), and
974
- * a context path we don't control is a context path that can be excluded.
975
- * printf needs nothing but a shell.
976
- *
977
- * Two rules make this safe to bolt onto user code. It must never fail the
978
- * build — every branch ends in an echo, so the RUN exits 0 whatever the
979
- * image lacks — and it must never change the image, beyond the trust
980
- * store: notably no `USER root`, since we cannot know statically what
981
- * user to hand back. An image that declares a non-root user therefore
982
- * fails to write and is finished by the derivative build, which inspects
983
- * the built image and can escalate properly.
984
- */
985
- async function caTrustSuffix() {
986
- if (!existsSync(CA_PATH))
987
- return null;
988
- const pem = (await fs.readFile(CA_PATH, "utf8")).trim();
989
- // A quote in the PEM would break out of the shell quoting below. PEM is
990
- // base64 and dashes, so this is a guard, not a case we expect.
991
- if (!pem || pem.includes("'"))
992
- return null;
993
- const args = pem
994
- .split("\n")
995
- .map((l) => `'${l.trimEnd()}'`)
996
- .join(" ");
997
- const dst = "/usr/local/share/ca-certificates/spectest-ca.crt";
998
- return `
999
- # spectest: trust the environment's root CA. Appended by the harness —
1000
- # not part of the project's Dockerfile.
1001
- RUN P='[spectest-ca]'; \\
1002
- mkdir -p /usr/local/share/ca-certificates 2>/dev/null; \\
1003
- if printf '%s\\n' ${args} > ${dst} 2>/dev/null; then \\
1004
- if command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates >/dev/null 2>&1; then \\
1005
- echo "$P trusted via update-ca-certificates"; \\
1006
- elif command -v update-ca-trust >/dev/null 2>&1 && cp ${dst} /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract >/dev/null 2>&1; then \\
1007
- echo "$P trusted via update-ca-trust"; \\
1008
- else \\
1009
- echo "$P no system CA trust tool in image; env-var trust only"; \\
1010
- fi; \\
1011
- else \\
1012
- echo "$P trust store not writable by this image's user"; \\
1013
- fi
1014
- `;
1015
- }
1016
960
  /** The built form of a dockerfile image. `resolveServiceImage` read a
1017
961
  * `path` into `content` at config time, so a service that still carries
1018
962
  * `path` here skipped that step — a programming error, not user input. */
@@ -1024,58 +968,29 @@ function dockerfileContent(svc) {
1024
968
  return image;
1025
969
  }
1026
970
  /**
1027
- * Build a dockerfile service's image.
971
+ * Build a dockerfile service's image — the project's Dockerfile as
972
+ * written, nothing appended.
1028
973
  *
1029
- * The CA-trust layer is folded into THIS build when it can be (see
1030
- * {@link caTrustSuffix}), so a service costs one image build and one
1031
- * export rather than two. The derivative build stays as the fallback for
1032
- * everything the folded form can't serve: an image with no shell to run
1033
- * the step (distroless, scratch — the appended RUN can't execute, so the
1034
- * build fails and we rebuild the project's Dockerfile untouched), and an
1035
- * image that declares a non-root user (the step runs as that user and
1036
- * can't write the trust store).
974
+ * Trust in the environment's CA is not the image's business any more:
975
+ * `runContainer` mounts the combined bundle over the system trust store
976
+ * (`container-run.ts`, `SYSTEM_TRUST_BUNDLE_PATHS`). Until 2026-09-06 a
977
+ * CA step was folded into this build and a derivative `docker build`
978
+ * finished any image the fold could not write to (non-root `USER`, no
979
+ * shell), which was one or two extra builds per service, and one per
980
+ * pulled image, on every cold start.
1037
981
  */
1038
982
  async function buildServiceImage(name, image, tag) {
1039
- const suffix = await caTrustSuffix();
1040
- let attempt = await runServiceBuild(name, image, tag, suffix);
1041
- if (!attempt.ok && suffix) {
1042
- // Our step must not be able to break a project's build.
1043
- // eslint-disable-next-line no-console
1044
- console.warn(`[ca-trust] folded CA step could not run in ${name}'s image; rebuilding without it`);
1045
- attempt = await runServiceBuild(name, image, tag, null);
1046
- }
983
+ const attempt = await runServiceBuild(name, image, tag);
1047
984
  if (!attempt.ok) {
1048
985
  progressService(name, { status: "failed" });
1049
986
  throw new Error(`docker build for ${name} failed:\n${attempt.log}`);
1050
987
  }
1051
- // The derivative build is still needed for the one thing the folded step
1052
- // cannot do: write the trust store of an image that does not run as
1053
- // root. That is decided on the IMAGE, not on the build log — a CACHED
1054
- // layer prints nothing, so a log-only check would quietly stop
1055
- // re-applying the moment BuildKit had the layer. The log covers the
1056
- // rarer case of a root image whose /etc is read-only.
1057
- //
1058
- // An image with no trust tool at all needs nothing further: the
1059
- // derivative build would reach the same dead end, and `runContainer`'s
1060
- // env vars are the fallback either way.
1061
- const needsDerivative = !attempt.folded ||
1062
- attempt.log.includes(CA_FOLD_UNWRITABLE) ||
1063
- !(await imageRunsAsRoot(tag));
1064
- if (needsDerivative)
1065
- await ensureCaTrustedImage(name, tag);
1066
988
  return { tag, buildSteps: attempt.buildSteps };
1067
989
  }
1068
- /** Whether `tag`'s declared `USER` is root (or unset, which means root). */
1069
- async function imageRunsAsRoot(tag) {
1070
- const user = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
1071
- return user === "" || user === "root" || user === "0";
1072
- }
1073
- async function runServiceBuild(name, image, tag, caSuffix) {
990
+ async function runServiceBuild(name, image, tag) {
1074
991
  let buildSteps;
1075
992
  {
1076
- const content = caSuffix
1077
- ? `${image.content.replace(/\n*$/, "\n")}${caSuffix}`
1078
- : image.content;
993
+ const content = image.content;
1079
994
  const dfDir = path.join(WORKSPACE, ".spectest", "services", name);
1080
995
  await fs.mkdir(dfDir, { recursive: true });
1081
996
  const dfPath = path.join(dfDir, "Dockerfile");
@@ -1176,9 +1091,7 @@ async function runServiceBuild(name, image, tag, caSuffix) {
1176
1091
  });
1177
1092
  const log = `${build.stderr.trim()}\n${build.stdout.trim()}`;
1178
1093
  if (build.code !== 0) {
1179
- // The caller decides whether this is fatal: a failure with the CA
1180
- // step appended is retried without it before anyone hears about it.
1181
- return { ok: false, folded: caSuffix !== null, log };
1094
+ return { ok: false, log };
1182
1095
  }
1183
1096
  if (useBuildKit) {
1184
1097
  // Keep only the slowest dozen steps ≥1s — enough to profile, small
@@ -1187,50 +1100,7 @@ async function runServiceBuild(name, image, tag, caSuffix) {
1187
1100
  .filter((s) => s.secs >= 1)
1188
1101
  .slice(0, 12);
1189
1102
  }
1190
- return { ok: true, folded: caSuffix !== null, log, buildSteps };
1191
- }
1192
- }
1193
- /**
1194
- * Build a derivative image on top of `tag` that copies the spectest
1195
- * root CA into the system trust store. Tagged back as `tag`, so the
1196
- * rest of the orchestrator (runContainer, image cache) is oblivious.
1197
- * Failures are warned-and-ignored: the env-var injection in
1198
- * runContainer is the universal fallback, so apps that use it (most
1199
- * Node/Python/Ruby/AWS clients) still trust the CA even when the
1200
- * image's trust store can't be updated.
1201
- */
1202
- async function ensureCaTrustedImage(serviceName, tag) {
1203
- if (!existsSync(CA_PATH)) {
1204
- // Daemon running outside a base-snapshot VM (dev/test). Nothing to
1205
- // layer; env vars also harmless (they point at a missing path, but
1206
- // most consumers ignore missing files).
1207
- return;
1208
- }
1209
- const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
1210
- await fs.mkdir(ctxDir, { recursive: true });
1211
- await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
1212
- // An image that declares a non-root `USER` runs this step as that user,
1213
- // and writing the trust store then fails ("cannot create
1214
- // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
1215
- // silently never lands. Take root for the one command, then hand the
1216
- // image back its own user so containers still run as it.
1217
- const declaredUser = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
1218
- const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
1219
- const dockerfile = `FROM ${tag}
1220
- ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
1221
- RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
1222
- update-ca-certificates; \\
1223
- elif command -v update-ca-trust >/dev/null 2>&1; then \\
1224
- cp /usr/local/share/ca-certificates/spectest-ca.crt /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract; \\
1225
- else \\
1226
- echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
1227
- fi
1228
- ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
1229
- await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
1230
- const build = await docker(["build", "-t", tag, ctxDir], 300_000);
1231
- if (build.code !== 0) {
1232
- // eslint-disable-next-line no-console
1233
- console.warn(`[ca-trust] could not layer spectest CA into ${serviceName} (${tag}); env-var fallback only:\n${build.stderr.trim() || build.stdout.trim()}`);
1103
+ return { ok: true, log, buildSteps };
1234
1104
  }
1235
1105
  }
1236
1106
  /** Memoized across containers — the CA never rotates within a VM's life. */
@@ -1240,10 +1110,14 @@ let caBundlePromise = null;
1240
1110
  * return its path, or null when there's no CA to trust (daemon running
1241
1111
  * outside a base-snapshot VM).
1242
1112
  *
1243
- * This is what the replace-semantics trust env vars must point at. Falling
1244
- * back to the bare CA when the guest bundle is unreadable keeps the old
1245
- * behaviour — fakes verify, public HTTPS doesn't — which is strictly better
1246
- * than dropping the CA and breaking the fakes everything else depends on.
1113
+ * This is what the replace-semantics trust env vars must point at, and
1114
+ * what `runContainer` mounts over the image's own system trust store
1115
+ * (`container-run.ts`). Falling back to the bare CA when the guest bundle
1116
+ * is unreadable keeps the old behaviour for the variables — fakes verify,
1117
+ * public HTTPS doesn't — which is strictly better than dropping the CA and
1118
+ * breaking the fakes everything else depends on; the system-store mount
1119
+ * is skipped in that case, since the bare CA there would take the public
1120
+ * roots away from clients that read no variable.
1247
1121
  */
1248
1122
  async function ensureCaBundle() {
1249
1123
  caBundlePromise ??= (async () => {
@@ -35,6 +35,20 @@
35
35
  * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
36
  * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
37
  *
38
+ * **The system trust store is the fourth, and it is a mount, not a
39
+ * layer.** Go, curl, OpenSSL's defaults and Python's `ssl` with no
40
+ * variable set read the distro's bundle file directly, so the combined
41
+ * bundle is bind-mounted read-only over the paths those bundles live at
42
+ * ({@link SYSTEM_TRUST_BUNDLE_PATHS}). Until 2026-09-06 this was a
43
+ * derivative image build per service (`COPY` the CA, run
44
+ * `update-ca-certificates`) — a `docker build` for every image, built or
45
+ * pulled, on every cold start, and most of what a cached bring-up still
46
+ * paid. A mount costs nothing, needs no cache, and behaves the same on a
47
+ * warm, delta or cold start. What it does not cover: a Java keystore
48
+ * (`cacerts`), and an entrypoint that runs `update-ca-certificates`
49
+ * itself — its final `mv` over the bundle fails with EBUSY on a
50
+ * mountpoint.
51
+ *
38
52
  * **`command` and `args` are different overrides.** `command` replaces the
39
53
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
40
54
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -91,5 +105,17 @@ export interface ContainerRunInput {
91
105
  * never `--network=host`, where writing `net.*` is denied.
92
106
  */
93
107
  export declare const TCP_RETRIES2 = 6;
108
+ /**
109
+ * Where a distro keeps the bundle that clients reading the system store
110
+ * open. Debian, Ubuntu and Alpine resolve every default path to the first
111
+ * through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
112
+ * RHEL family resolves to the second (itself a symlink into
113
+ * `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
114
+ * mounts). Go checks both names itself. An image with neither path gets
115
+ * the file created by the mount, which is what a scratch image with a
116
+ * Go binary wants. Read-only, so a container cannot change what its
117
+ * siblings trust.
118
+ */
119
+ export declare const SYSTEM_TRUST_BUNDLE_PATHS: readonly ["/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"];
94
120
  /** Build the full `docker run` argv. Pure: no I/O, no module state. */
95
121
  export declare function runContainerArgs(input: ContainerRunInput): string[];
@@ -35,6 +35,20 @@
35
35
  * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
36
  * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
37
  *
38
+ * **The system trust store is the fourth, and it is a mount, not a
39
+ * layer.** Go, curl, OpenSSL's defaults and Python's `ssl` with no
40
+ * variable set read the distro's bundle file directly, so the combined
41
+ * bundle is bind-mounted read-only over the paths those bundles live at
42
+ * ({@link SYSTEM_TRUST_BUNDLE_PATHS}). Until 2026-09-06 this was a
43
+ * derivative image build per service (`COPY` the CA, run
44
+ * `update-ca-certificates`) — a `docker build` for every image, built or
45
+ * pulled, on every cold start, and most of what a cached bring-up still
46
+ * paid. A mount costs nothing, needs no cache, and behaves the same on a
47
+ * warm, delta or cold start. What it does not cover: a Java keystore
48
+ * (`cacerts`), and an entrypoint that runs `update-ca-certificates`
49
+ * itself — its final `mv` over the bundle fails with EBUSY on a
50
+ * mountpoint.
51
+ *
38
52
  * **`command` and `args` are different overrides.** `command` replaces the
39
53
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
40
54
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -57,6 +71,21 @@
57
71
  * never `--network=host`, where writing `net.*` is denied.
58
72
  */
59
73
  export const TCP_RETRIES2 = 6;
74
+ /**
75
+ * Where a distro keeps the bundle that clients reading the system store
76
+ * open. Debian, Ubuntu and Alpine resolve every default path to the first
77
+ * through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
78
+ * RHEL family resolves to the second (itself a symlink into
79
+ * `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
80
+ * mounts). Go checks both names itself. An image with neither path gets
81
+ * the file created by the mount, which is what a scratch image with a
82
+ * Go binary wants. Read-only, so a container cannot change what its
83
+ * siblings trust.
84
+ */
85
+ export const SYSTEM_TRUST_BUNDLE_PATHS = [
86
+ "/etc/ssl/certs/ca-certificates.crt",
87
+ "/etc/pki/tls/certs/ca-bundle.crt",
88
+ ];
60
89
  /** Build the full `docker run` argv. Pure: no I/O, no module state. */
61
90
  export function runContainerArgs(input) {
62
91
  const { svc, tag, network } = input;
@@ -107,6 +136,14 @@ export function runContainerArgs(input) {
107
136
  args.push("-e", `SSL_CERT_FILE=${bundle}`);
108
137
  args.push("-e", `REQUESTS_CA_BUNDLE=${bundle}`);
109
138
  args.push("-e", `AWS_CA_BUNDLE=${bundle}`);
139
+ // The system store itself, for clients that read no variable at all.
140
+ // Only a real bundle: the caller falls back to the bare CA when the
141
+ // guest's roots are unreadable, and the bare CA over the system store
142
+ // would leave Go and curl trusting spectest and nothing else.
143
+ if (bundle !== input.caPath) {
144
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS)
145
+ args.push(`--volume=${bundle}:${p}:ro`);
146
+ }
110
147
  }
111
148
  // The service's own env comes after ours, so a project can override the
112
149
  // defaults we set rather than being silently overridden by them.
@@ -131,13 +131,13 @@ const testableAccessors = {
131
131
  "ownerDocument"
132
132
  ],
133
133
  ShadowRoot: ["host", "styleSheets"],
134
- Element: ["shadowRoot", "querySelector", "querySelectorAll"],
134
+ Element: ["shadowRoot"],
135
135
  MutationObserver: []
136
136
  };
137
137
  const testableMethods = {
138
138
  Node: ["contains", "getRootNode"],
139
139
  ShadowRoot: ["getSelection"],
140
- Element: [],
140
+ Element: ["querySelector", "querySelectorAll"],
141
141
  MutationObserver: ["constructor"]
142
142
  };
143
143
  const untaintedBasePrototype = {};
@@ -263,10 +263,10 @@ function shadowRoot(n) {
263
263
  return getUntaintedAccessor("Element", n, "shadowRoot");
264
264
  }
265
265
  function querySelector(n, selectors) {
266
- return getUntaintedAccessor("Element", n, "querySelector")(selectors);
266
+ return getUntaintedMethod("Element", n, "querySelector")(selectors);
267
267
  }
268
268
  function querySelectorAll(n, selectors) {
269
- return getUntaintedAccessor("Element", n, "querySelectorAll")(selectors);
269
+ return getUntaintedMethod("Element", n, "querySelectorAll")(selectors);
270
270
  }
271
271
  function mutationObserverCtor() {
272
272
  var _a2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.72.0",
3
+ "version": "0.73.1",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/browser.ts CHANGED
@@ -615,11 +615,17 @@ async function applySafeAreaInsets(
615
615
  // rrweb bootstrap
616
616
  // ────────────────────────────────────────────────────────────────────────
617
617
 
618
- // Vendored `@rrweb/record` UMD bundle (rrweb 2.x). Its global `rrwebRecord`
619
- // is a module object — the record function is `rrwebRecord.record` (the
620
- // bootstrap resolves both this and the legacy function-shaped global). Read
621
- // once at module init — the SDK ships this file in the base snapshot so
622
- // no network fetch happens inside the VM.
618
+ // Vendored `@rrweb/record` UMD bundle (rrweb 2.x), BUILT FROM OUR FORK by
619
+ // `scripts/rrweb-record/build.sh` (specific-dev/rrweb, branch `spectest` =
620
+ // the `rrweb@2.1.0` tag + upstream PR 1802). Never copy the npm dist over
621
+ // it: upstream's `@rrweb/utils` lists `querySelector` as an accessor, so its
622
+ // pristine-prototype test always fails and `record()` appends an <iframe>
623
+ // to every document — measured at ~1 s per navigation (see the top-frame
624
+ // gate on PAGE_INIT_SCRIPT below). Its global `rrwebRecord` is a module
625
+ // object — the record function is `rrwebRecord.record` (the bootstrap
626
+ // resolves both this and the legacy function-shaped global). Read once at
627
+ // module init — the SDK ships this file in the base snapshot so no network
628
+ // fetch happens inside the VM.
623
629
  const RRWEB_BUNDLE: string = (() => {
624
630
  try {
625
631
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -1094,8 +1100,22 @@ const RRWEB_BOOTSTRAP = `
1094
1100
  // call expression — the TypeError kills everything after the bundle, so
1095
1101
  // the globals define but the bootstrap never runs (zero rrweb events,
1096
1102
  // empty replays).
1103
+ //
1104
+ // The whole script runs in the TOP frame only. `Page.addScriptToEvaluateOnNewDocument`
1105
+ // fires for every frame, and until 2026-09-06 that was a storm: rrweb's
1106
+ // `getUntaintedPrototype` appends a hidden <iframe> to read pristine DOM
1107
+ // prototypes, this script then ran inside it, the child's `record()` (which
1108
+ // treats every frame as an emitting frame unless `recordCrossOriginIframes`
1109
+ // is set) appended an iframe of its own, and so on to Chromium's frame-depth
1110
+ // cap — 44 nested frames and 45 executions of ~180 KB of JS per navigation,
1111
+ // ~0.8–1.3 s inside the page's `load` handler even for a one-line document
1112
+ // (`goto` p50 was ~1.5 s in every project). The vendored bundle's fork patch
1113
+ // removes the iframe on an unpatched page; this gate bounds it on a page
1114
+ // whose prototypes really are patched (zone.js). Nothing is lost: rrweb
1115
+ // records same-origin iframes from the parent, and a cross-origin child's
1116
+ // events were never drained (they land in the child's own buffer).
1097
1117
  const PAGE_INIT_SCRIPT = RRWEB_BUNDLE
1098
- ? `${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}`
1118
+ ? `if (window === window.top) {\n${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}\n}`
1099
1119
  : "";
1100
1120
 
1101
1121
  // Page-side expression that atomically swaps in a fresh buffer and
package/src/daemon.ts CHANGED
@@ -1239,7 +1239,6 @@ async function prepareServiceImage(
1239
1239
  if (tagr.code !== 0) {
1240
1240
  throw new Error(`docker tag ${ref} ${tag} failed: ${tagr.stderr.trim()}`);
1241
1241
  }
1242
- await ensureCaTrustedImage(svc.name, tag);
1243
1242
  return { tag };
1244
1243
  }
1245
1244
  // Dockerfile build. Within one bootstrap, identical definitions (shared
@@ -1283,61 +1282,6 @@ async function prepareServiceImage(
1283
1282
  return buildServiceImage(svc.name, image, tag);
1284
1283
  }
1285
1284
 
1286
- /** Printed by the folded CA step when it could not write the trust
1287
- * store at all — the one outcome that still needs the derivative build. */
1288
- const CA_FOLD_UNWRITABLE = "[spectest-ca] trust store not writable";
1289
-
1290
- /**
1291
- * The CA-trust steps as a suffix appended to a dockerfile service's OWN
1292
- * Dockerfile, so one build produces the finished image instead of a build
1293
- * plus a derivative rebuild ({@link ensureCaTrustedImage}) per service.
1294
- * Returns null when there is no CA to layer, or when the PEM can't be
1295
- * quoted — the caller then falls back to the derivative build.
1296
- *
1297
- * The PEM is written INLINE rather than `COPY`d: the build context is
1298
- * `/workspace` under a per-service ignore file that the project itself
1299
- * contributes to (a `**` line with re-includes is the common idiom), and
1300
- * a context path we don't control is a context path that can be excluded.
1301
- * printf needs nothing but a shell.
1302
- *
1303
- * Two rules make this safe to bolt onto user code. It must never fail the
1304
- * build — every branch ends in an echo, so the RUN exits 0 whatever the
1305
- * image lacks — and it must never change the image, beyond the trust
1306
- * store: notably no `USER root`, since we cannot know statically what
1307
- * user to hand back. An image that declares a non-root user therefore
1308
- * fails to write and is finished by the derivative build, which inspects
1309
- * the built image and can escalate properly.
1310
- */
1311
- async function caTrustSuffix(): Promise<string | null> {
1312
- if (!existsSync(CA_PATH)) return null;
1313
- const pem = (await fs.readFile(CA_PATH, "utf8")).trim();
1314
- // A quote in the PEM would break out of the shell quoting below. PEM is
1315
- // base64 and dashes, so this is a guard, not a case we expect.
1316
- if (!pem || pem.includes("'")) return null;
1317
- const args = pem
1318
- .split("\n")
1319
- .map((l) => `'${l.trimEnd()}'`)
1320
- .join(" ");
1321
- const dst = "/usr/local/share/ca-certificates/spectest-ca.crt";
1322
- return `
1323
- # spectest: trust the environment's root CA. Appended by the harness —
1324
- # not part of the project's Dockerfile.
1325
- RUN P='[spectest-ca]'; \\
1326
- mkdir -p /usr/local/share/ca-certificates 2>/dev/null; \\
1327
- if printf '%s\\n' ${args} > ${dst} 2>/dev/null; then \\
1328
- if command -v update-ca-certificates >/dev/null 2>&1 && update-ca-certificates >/dev/null 2>&1; then \\
1329
- echo "$P trusted via update-ca-certificates"; \\
1330
- elif command -v update-ca-trust >/dev/null 2>&1 && cp ${dst} /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract >/dev/null 2>&1; then \\
1331
- echo "$P trusted via update-ca-trust"; \\
1332
- else \\
1333
- echo "$P no system CA trust tool in image; env-var trust only"; \\
1334
- fi; \\
1335
- else \\
1336
- echo "$P trust store not writable by this image's user"; \\
1337
- fi
1338
- `;
1339
- }
1340
-
1341
1285
  /** The built form of a dockerfile image. `resolveServiceImage` read a
1342
1286
  * `path` into `content` at config time, so a service that still carries
1343
1287
  * `path` here skipped that step — a programming error, not user input. */
@@ -1350,73 +1294,38 @@ function dockerfileContent(svc: NamedService): DockerfileBuild {
1350
1294
  }
1351
1295
 
1352
1296
  /**
1353
- * Build a dockerfile service's image.
1297
+ * Build a dockerfile service's image — the project's Dockerfile as
1298
+ * written, nothing appended.
1354
1299
  *
1355
- * The CA-trust layer is folded into THIS build when it can be (see
1356
- * {@link caTrustSuffix}), so a service costs one image build and one
1357
- * export rather than two. The derivative build stays as the fallback for
1358
- * everything the folded form can't serve: an image with no shell to run
1359
- * the step (distroless, scratch — the appended RUN can't execute, so the
1360
- * build fails and we rebuild the project's Dockerfile untouched), and an
1361
- * image that declares a non-root user (the step runs as that user and
1362
- * can't write the trust store).
1300
+ * Trust in the environment's CA is not the image's business any more:
1301
+ * `runContainer` mounts the combined bundle over the system trust store
1302
+ * (`container-run.ts`, `SYSTEM_TRUST_BUNDLE_PATHS`). Until 2026-09-06 a
1303
+ * CA step was folded into this build and a derivative `docker build`
1304
+ * finished any image the fold could not write to (non-root `USER`, no
1305
+ * shell), which was one or two extra builds per service, and one per
1306
+ * pulled image, on every cold start.
1363
1307
  */
1364
1308
  async function buildServiceImage(
1365
1309
  name: string,
1366
1310
  image: DockerfileBuild,
1367
1311
  tag: string,
1368
1312
  ): Promise<{ tag: string; buildSteps?: BuildStep[] }> {
1369
- const suffix = await caTrustSuffix();
1370
- let attempt = await runServiceBuild(name, image, tag, suffix);
1371
- if (!attempt.ok && suffix) {
1372
- // Our step must not be able to break a project's build.
1373
- // eslint-disable-next-line no-console
1374
- console.warn(
1375
- `[ca-trust] folded CA step could not run in ${name}'s image; rebuilding without it`,
1376
- );
1377
- attempt = await runServiceBuild(name, image, tag, null);
1378
- }
1313
+ const attempt = await runServiceBuild(name, image, tag);
1379
1314
  if (!attempt.ok) {
1380
1315
  progressService(name, { status: "failed" });
1381
1316
  throw new Error(`docker build for ${name} failed:\n${attempt.log}`);
1382
1317
  }
1383
- // The derivative build is still needed for the one thing the folded step
1384
- // cannot do: write the trust store of an image that does not run as
1385
- // root. That is decided on the IMAGE, not on the build log — a CACHED
1386
- // layer prints nothing, so a log-only check would quietly stop
1387
- // re-applying the moment BuildKit had the layer. The log covers the
1388
- // rarer case of a root image whose /etc is read-only.
1389
- //
1390
- // An image with no trust tool at all needs nothing further: the
1391
- // derivative build would reach the same dead end, and `runContainer`'s
1392
- // env vars are the fallback either way.
1393
- const needsDerivative =
1394
- !attempt.folded ||
1395
- attempt.log.includes(CA_FOLD_UNWRITABLE) ||
1396
- !(await imageRunsAsRoot(tag));
1397
- if (needsDerivative) await ensureCaTrustedImage(name, tag);
1398
1318
  return { tag, buildSteps: attempt.buildSteps };
1399
1319
  }
1400
1320
 
1401
- /** Whether `tag`'s declared `USER` is root (or unset, which means root). */
1402
- async function imageRunsAsRoot(tag: string): Promise<boolean> {
1403
- const user = (
1404
- await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)
1405
- ).stdout.trim();
1406
- return user === "" || user === "root" || user === "0";
1407
- }
1408
-
1409
1321
  async function runServiceBuild(
1410
1322
  name: string,
1411
1323
  image: DockerfileBuild,
1412
1324
  tag: string,
1413
- caSuffix: string | null,
1414
- ): Promise<{ ok: boolean; folded: boolean; log: string; buildSteps?: BuildStep[] }> {
1325
+ ): Promise<{ ok: boolean; log: string; buildSteps?: BuildStep[] }> {
1415
1326
  let buildSteps: BuildStep[] | undefined;
1416
1327
  {
1417
- const content = caSuffix
1418
- ? `${image.content.replace(/\n*$/, "\n")}${caSuffix}`
1419
- : image.content;
1328
+ const content = image.content;
1420
1329
  const dfDir = path.join(WORKSPACE, ".spectest", "services", name);
1421
1330
  await fs.mkdir(dfDir, { recursive: true });
1422
1331
  const dfPath = path.join(dfDir, "Dockerfile");
@@ -1514,9 +1423,7 @@ async function runServiceBuild(
1514
1423
  });
1515
1424
  const log = `${build.stderr.trim()}\n${build.stdout.trim()}`;
1516
1425
  if (build.code !== 0) {
1517
- // The caller decides whether this is fatal: a failure with the CA
1518
- // step appended is retried without it before anyone hears about it.
1519
- return { ok: false, folded: caSuffix !== null, log };
1426
+ return { ok: false, log };
1520
1427
  }
1521
1428
  if (useBuildKit) {
1522
1429
  // Keep only the slowest dozen steps ≥1s — enough to profile, small
@@ -1525,55 +1432,7 @@ async function runServiceBuild(
1525
1432
  .filter((s) => s.secs >= 1)
1526
1433
  .slice(0, 12);
1527
1434
  }
1528
- return { ok: true, folded: caSuffix !== null, log, buildSteps };
1529
- }
1530
- }
1531
-
1532
- /**
1533
- * Build a derivative image on top of `tag` that copies the spectest
1534
- * root CA into the system trust store. Tagged back as `tag`, so the
1535
- * rest of the orchestrator (runContainer, image cache) is oblivious.
1536
- * Failures are warned-and-ignored: the env-var injection in
1537
- * runContainer is the universal fallback, so apps that use it (most
1538
- * Node/Python/Ruby/AWS clients) still trust the CA even when the
1539
- * image's trust store can't be updated.
1540
- */
1541
- async function ensureCaTrustedImage(serviceName: string, tag: string): Promise<void> {
1542
- if (!existsSync(CA_PATH)) {
1543
- // Daemon running outside a base-snapshot VM (dev/test). Nothing to
1544
- // layer; env vars also harmless (they point at a missing path, but
1545
- // most consumers ignore missing files).
1546
- return;
1547
- }
1548
- const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
1549
- await fs.mkdir(ctxDir, { recursive: true });
1550
- await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
1551
- // An image that declares a non-root `USER` runs this step as that user,
1552
- // and writing the trust store then fails ("cannot create
1553
- // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
1554
- // silently never lands. Take root for the one command, then hand the
1555
- // image back its own user so containers still run as it.
1556
- const declaredUser = (
1557
- await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)
1558
- ).stdout.trim();
1559
- const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
1560
- const dockerfile = `FROM ${tag}
1561
- ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
1562
- RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
1563
- update-ca-certificates; \\
1564
- elif command -v update-ca-trust >/dev/null 2>&1; then \\
1565
- cp /usr/local/share/ca-certificates/spectest-ca.crt /etc/pki/ca-trust/source/anchors/spectest-ca.crt && update-ca-trust extract; \\
1566
- else \\
1567
- echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
1568
- fi
1569
- ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
1570
- await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
1571
- const build = await docker(["build", "-t", tag, ctxDir], 300_000);
1572
- if (build.code !== 0) {
1573
- // eslint-disable-next-line no-console
1574
- console.warn(
1575
- `[ca-trust] could not layer spectest CA into ${serviceName} (${tag}); env-var fallback only:\n${build.stderr.trim() || build.stdout.trim()}`,
1576
- );
1435
+ return { ok: true, log, buildSteps };
1577
1436
  }
1578
1437
  }
1579
1438
 
@@ -1585,10 +1444,14 @@ let caBundlePromise: Promise<string | null> | null = null;
1585
1444
  * return its path, or null when there's no CA to trust (daemon running
1586
1445
  * outside a base-snapshot VM).
1587
1446
  *
1588
- * This is what the replace-semantics trust env vars must point at. Falling
1589
- * back to the bare CA when the guest bundle is unreadable keeps the old
1590
- * behaviour — fakes verify, public HTTPS doesn't — which is strictly better
1591
- * than dropping the CA and breaking the fakes everything else depends on.
1447
+ * This is what the replace-semantics trust env vars must point at, and
1448
+ * what `runContainer` mounts over the image's own system trust store
1449
+ * (`container-run.ts`). Falling back to the bare CA when the guest bundle
1450
+ * is unreadable keeps the old behaviour for the variables — fakes verify,
1451
+ * public HTTPS doesn't — which is strictly better than dropping the CA and
1452
+ * breaking the fakes everything else depends on; the system-store mount
1453
+ * is skipped in that case, since the bare CA there would take the public
1454
+ * roots away from clients that read no variable.
1592
1455
  */
1593
1456
  async function ensureCaBundle(): Promise<string | null> {
1594
1457
  caBundlePromise ??= (async () => {
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { TCP_RETRIES2, runContainerArgs, type ContainerRunInput } from "./container-run";
3
+ import {
4
+ SYSTEM_TRUST_BUNDLE_PATHS,
5
+ TCP_RETRIES2,
6
+ runContainerArgs,
7
+ type ContainerRunInput,
8
+ } from "./container-run";
4
9
 
5
10
  const base = (over: Partial<ContainerRunInput> = {}): ContainerRunInput => ({
6
11
  svc: { name: "web" },
@@ -122,6 +127,31 @@ describe("certificate trust", () => {
122
127
  const mounts = args.filter((a) => a === "--volume=/etc/spectest/ca.crt:/etc/spectest/ca.crt:ro");
123
128
  expect(mounts).toHaveLength(1);
124
129
  });
130
+
131
+ /** Clients that read no variable — Go, curl, OpenSSL defaults, Python's
132
+ * `ssl` — open the distro's bundle file. The combined bundle is mounted
133
+ * over each known location, read-only, in place of the derivative
134
+ * image build this replaced. */
135
+ test("mounts the bundle over the system trust store paths", () => {
136
+ const args = runContainerArgs(base({ caBundle: "/etc/spectest/ca-bundle.crt" }));
137
+ expect(SYSTEM_TRUST_BUNDLE_PATHS).toContain("/etc/ssl/certs/ca-certificates.crt");
138
+ expect(SYSTEM_TRUST_BUNDLE_PATHS).toContain("/etc/pki/tls/certs/ca-bundle.crt");
139
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) {
140
+ expect(args).toContain(`--volume=/etc/spectest/ca-bundle.crt:${p}:ro`);
141
+ }
142
+ });
143
+
144
+ /** The bare CA over the system store would drop every public root, the
145
+ * same trap as the replacing variables — so no bundle means no mount. */
146
+ test("never mounts the bare CA over the system trust store", () => {
147
+ // No bundle at all, and the fallback where the bundle IS the bare CA.
148
+ for (const caBundle of [null, "/etc/spectest/ca.crt"]) {
149
+ const args = runContainerArgs(base({ caBundle }));
150
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) {
151
+ expect(args.some((a) => a.endsWith(`:${p}:ro`))).toBe(false);
152
+ }
153
+ }
154
+ });
125
155
  });
126
156
 
127
157
  describe("service configuration", () => {
@@ -35,6 +35,20 @@
35
35
  * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
36
  * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
37
  *
38
+ * **The system trust store is the fourth, and it is a mount, not a
39
+ * layer.** Go, curl, OpenSSL's defaults and Python's `ssl` with no
40
+ * variable set read the distro's bundle file directly, so the combined
41
+ * bundle is bind-mounted read-only over the paths those bundles live at
42
+ * ({@link SYSTEM_TRUST_BUNDLE_PATHS}). Until 2026-09-06 this was a
43
+ * derivative image build per service (`COPY` the CA, run
44
+ * `update-ca-certificates`) — a `docker build` for every image, built or
45
+ * pulled, on every cold start, and most of what a cached bring-up still
46
+ * paid. A mount costs nothing, needs no cache, and behaves the same on a
47
+ * warm, delta or cold start. What it does not cover: a Java keystore
48
+ * (`cacerts`), and an entrypoint that runs `update-ca-certificates`
49
+ * itself — its final `mv` over the bundle fails with EBUSY on a
50
+ * mountpoint.
51
+ *
38
52
  * **`command` and `args` are different overrides.** `command` replaces the
39
53
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
40
54
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -95,6 +109,22 @@ export interface ContainerRunInput {
95
109
  */
96
110
  export const TCP_RETRIES2 = 6;
97
111
 
112
+ /**
113
+ * Where a distro keeps the bundle that clients reading the system store
114
+ * open. Debian, Ubuntu and Alpine resolve every default path to the first
115
+ * through symlinks (`/etc/ssl/cert.pem`, `/usr/lib/ssl/cert.pem`); the
116
+ * RHEL family resolves to the second (itself a symlink into
117
+ * `/etc/pki/ca-trust/extracted/pem/`, which the runtime follows before it
118
+ * mounts). Go checks both names itself. An image with neither path gets
119
+ * the file created by the mount, which is what a scratch image with a
120
+ * Go binary wants. Read-only, so a container cannot change what its
121
+ * siblings trust.
122
+ */
123
+ export const SYSTEM_TRUST_BUNDLE_PATHS = [
124
+ "/etc/ssl/certs/ca-certificates.crt",
125
+ "/etc/pki/tls/certs/ca-bundle.crt",
126
+ ] as const;
127
+
98
128
  /** Build the full `docker run` argv. Pure: no I/O, no module state. */
99
129
  export function runContainerArgs(input: ContainerRunInput): string[] {
100
130
  const { svc, tag, network } = input;
@@ -151,6 +181,13 @@ export function runContainerArgs(input: ContainerRunInput): string[] {
151
181
  args.push("-e", `SSL_CERT_FILE=${bundle}`);
152
182
  args.push("-e", `REQUESTS_CA_BUNDLE=${bundle}`);
153
183
  args.push("-e", `AWS_CA_BUNDLE=${bundle}`);
184
+ // The system store itself, for clients that read no variable at all.
185
+ // Only a real bundle: the caller falls back to the bare CA when the
186
+ // guest's roots are unreadable, and the bare CA over the system store
187
+ // would leave Go and curl trusting spectest and nothing else.
188
+ if (bundle !== input.caPath) {
189
+ for (const p of SYSTEM_TRUST_BUNDLE_PATHS) args.push(`--volume=${bundle}:${p}:ro`);
190
+ }
154
191
  }
155
192
 
156
193
  // The service's own env comes after ours, so a project can override the
@@ -131,13 +131,13 @@ const testableAccessors = {
131
131
  "ownerDocument"
132
132
  ],
133
133
  ShadowRoot: ["host", "styleSheets"],
134
- Element: ["shadowRoot", "querySelector", "querySelectorAll"],
134
+ Element: ["shadowRoot"],
135
135
  MutationObserver: []
136
136
  };
137
137
  const testableMethods = {
138
138
  Node: ["contains", "getRootNode"],
139
139
  ShadowRoot: ["getSelection"],
140
- Element: [],
140
+ Element: ["querySelector", "querySelectorAll"],
141
141
  MutationObserver: ["constructor"]
142
142
  };
143
143
  const untaintedBasePrototype = {};
@@ -263,10 +263,10 @@ function shadowRoot(n) {
263
263
  return getUntaintedAccessor("Element", n, "shadowRoot");
264
264
  }
265
265
  function querySelector(n, selectors) {
266
- return getUntaintedAccessor("Element", n, "querySelector")(selectors);
266
+ return getUntaintedMethod("Element", n, "querySelector")(selectors);
267
267
  }
268
268
  function querySelectorAll(n, selectors) {
269
- return getUntaintedAccessor("Element", n, "querySelectorAll")(selectors);
269
+ return getUntaintedMethod("Element", n, "querySelectorAll")(selectors);
270
270
  }
271
271
  function mutationObserverCtor() {
272
272
  var _a2;