@specific.dev/spectest 0.46.0 → 0.47.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/dist/daemon.js CHANGED
@@ -933,8 +933,15 @@ async function ensureCaTrustedImage(serviceName, tag) {
933
933
  const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
934
934
  await fs.mkdir(ctxDir, { recursive: true });
935
935
  await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
936
+ // An image that declares a non-root `USER` runs this step as that user,
937
+ // and writing the trust store then fails ("cannot create
938
+ // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
939
+ // silently never lands. Take root for the one command, then hand the
940
+ // image back its own user so containers still run as it.
941
+ const declaredUser = (await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)).stdout.trim();
942
+ const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
936
943
  const dockerfile = `FROM ${tag}
937
- COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
944
+ ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
938
945
  RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
939
946
  update-ca-certificates; \\
940
947
  elif command -v update-ca-trust >/dev/null 2>&1; then \\
@@ -942,7 +949,7 @@ RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
942
949
  else \\
943
950
  echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
944
951
  fi
945
- `;
952
+ ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
946
953
  await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
947
954
  const build = await docker(["build", "-t", tag, ctxDir], 300_000);
948
955
  if (build.code !== 0) {
@@ -29,6 +29,12 @@
29
29
  * showed the problem, which is why it went unnoticed: only the appending
30
30
  * variable was in play.
31
31
  *
32
+ * **Deno is a third mechanism.** It reads none of those variables and
33
+ * trusts only its bundled Mozilla roots, so an edge function calling a
34
+ * fake fails with `UnknownIssuer` however the image is set up.
35
+ * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
+ * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
+ *
32
38
  * **`command` and `args` are different overrides.** `command` replaces the
33
39
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
34
40
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -29,6 +29,12 @@
29
29
  * showed the problem, which is why it went unnoticed: only the appending
30
30
  * variable was in play.
31
31
  *
32
+ * **Deno is a third mechanism.** It reads none of those variables and
33
+ * trusts only its bundled Mozilla roots, so an edge function calling a
34
+ * fake fails with `UnknownIssuer` however the image is set up.
35
+ * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
+ * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
+ *
32
38
  * **`command` and `args` are different overrides.** `command` replaces the
33
39
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
34
40
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -87,6 +93,13 @@ export function runContainerArgs(input) {
87
93
  // replacing ones take the combined bundle.
88
94
  args.push(`--volume=${input.caPath}:${input.caPath}:ro`);
89
95
  args.push("-e", `NODE_EXTRA_CA_CERTS=${input.caPath}`);
96
+ // Deno reads none of the above, and its default store is the bundled
97
+ // Mozilla roots alone — so a Deno runtime (a Supabase edge function is
98
+ // the usual one) rejects every certificate this environment mints, with
99
+ // or without the CA layered into the image. `system` adds the platform
100
+ // store, which resolves through `SSL_CERT_FILE`: the bundle below.
101
+ // `mozilla` stays first so the public roots survive.
102
+ args.push("-e", "DENO_TLS_CA_STORE=mozilla,system");
90
103
  const bundle = input.caBundle;
91
104
  if (bundle) {
92
105
  if (bundle !== input.caPath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/daemon.ts CHANGED
@@ -1221,8 +1221,17 @@ async function ensureCaTrustedImage(serviceName: string, tag: string): Promise<v
1221
1221
  const ctxDir = path.join(WORKSPACE, ".spectest", "ca-trust", serviceName);
1222
1222
  await fs.mkdir(ctxDir, { recursive: true });
1223
1223
  await fs.copyFile(CA_PATH, path.join(ctxDir, "spectest-ca.crt"));
1224
+ // An image that declares a non-root `USER` runs this step as that user,
1225
+ // and writing the trust store then fails ("cannot create
1226
+ // /etc/ssl/certs/ca-certificates.crt.new: Permission denied") — the CA
1227
+ // silently never lands. Take root for the one command, then hand the
1228
+ // image back its own user so containers still run as it.
1229
+ const declaredUser = (
1230
+ await docker(["image", "inspect", "--format", "{{.Config.User}}", tag], 60_000)
1231
+ ).stdout.trim();
1232
+ const needsRoot = declaredUser !== "" && declaredUser !== "root" && declaredUser !== "0";
1224
1233
  const dockerfile = `FROM ${tag}
1225
- COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
1234
+ ${needsRoot ? "USER root\n" : ""}COPY spectest-ca.crt /usr/local/share/ca-certificates/spectest-ca.crt
1226
1235
  RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
1227
1236
  update-ca-certificates; \\
1228
1237
  elif command -v update-ca-trust >/dev/null 2>&1; then \\
@@ -1230,7 +1239,7 @@ RUN if command -v update-ca-certificates >/dev/null 2>&1; then \\
1230
1239
  else \\
1231
1240
  echo "[spectest] no system CA trust tool in image; env-var trust only"; \\
1232
1241
  fi
1233
- `;
1242
+ ${needsRoot ? `USER ${declaredUser}\n` : ""}`;
1234
1243
  await fs.writeFile(path.join(ctxDir, "Dockerfile"), dockerfile);
1235
1244
  const build = await docker(["build", "-t", tag, ctxDir], 300_000);
1236
1245
  if (build.code !== 0) {
@@ -101,6 +101,14 @@ describe("certificate trust", () => {
101
101
  );
102
102
  });
103
103
 
104
+ /** Deno ignores every variable above and ships its own root store, so
105
+ * without this an edge function calling a fake gets `UnknownIssuer`
106
+ * no matter what the image's trust store says. */
107
+ test("points Deno at the platform store as well as its own", () => {
108
+ const args = runContainerArgs(base({ caBundle: "/etc/spectest/ca-bundle.crt" }));
109
+ expect(envOf(args, "DENO_TLS_CA_STORE")).toBe("mozilla,system");
110
+ });
111
+
104
112
  test("with no bundle, the replacing variables are left unset", () => {
105
113
  const args = runContainerArgs(base({ caBundle: null }));
106
114
  expect(envOf(args, "SSL_CERT_FILE")).toBeUndefined();
@@ -29,6 +29,12 @@
29
29
  * showed the problem, which is why it went unnoticed: only the appending
30
30
  * variable was in play.
31
31
  *
32
+ * **Deno is a third mechanism.** It reads none of those variables and
33
+ * trusts only its bundled Mozilla roots, so an edge function calling a
34
+ * fake fails with `UnknownIssuer` however the image is set up.
35
+ * `DENO_TLS_CA_STORE=mozilla,system` is what adds the platform store,
36
+ * and the platform store is `SSL_CERT_FILE` — the combined bundle.
37
+ *
32
38
  * **`command` and `args` are different overrides.** `command` replaces the
33
39
  * entrypoint and runs through `sh -c`; `args` keeps the entrypoint and
34
40
  * overrides CMD, which is what init-wrapped images like postgres need to
@@ -132,6 +138,13 @@ export function runContainerArgs(input: ContainerRunInput): string[] {
132
138
  // replacing ones take the combined bundle.
133
139
  args.push(`--volume=${input.caPath}:${input.caPath}:ro`);
134
140
  args.push("-e", `NODE_EXTRA_CA_CERTS=${input.caPath}`);
141
+ // Deno reads none of the above, and its default store is the bundled
142
+ // Mozilla roots alone — so a Deno runtime (a Supabase edge function is
143
+ // the usual one) rejects every certificate this environment mints, with
144
+ // or without the CA layered into the image. `system` adds the platform
145
+ // store, which resolves through `SSL_CERT_FILE`: the bundle below.
146
+ // `mozilla` stays first so the public roots survive.
147
+ args.push("-e", "DENO_TLS_CA_STORE=mozilla,system");
135
148
  const bundle = input.caBundle;
136
149
  if (bundle) {
137
150
  if (bundle !== input.caPath) args.push(`--volume=${bundle}:${bundle}:ro`);