1// @effect-diagnostics nodeBuiltinImport:off - Encrypts fixtures with the same
2// OSCrypt primitives the module under test decrypts.
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { describe, expect, it } from "@effect/vitest";
5import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
6import * as NodeCrypto from "node:crypto";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as SqlClient from "effect/sql/SqlClient";
10
11import {
12 decryptChromiumValue,
13 readChromiumCookieDatabase,
14 readChromiumCookies,
15} from "./ChromiumCookies.ts";
16import { ChromiumKeyError } from "./ChromiumKeys.ts";
17import { LinuxBrowserSecretPath } from "./LinuxBrowserSecret.ts";
18import { cookieScope } from "./CookieDatabase.ts";
19
20const encryptChromium = (
21 prefix: "v10" | "v11",
22 value: string | Buffer,
23 key: Buffer,
24): Uint8Array => {
25 const cipher = NodeCrypto.createCipheriv("aes-128-cbc", key, Buffer.alloc(16, 0x20));
26 return Buffer.concat([Buffer.from(prefix), cipher.update(value), cipher.final()]);
27};
28
29const encryptV10 = (value: string | Buffer, key: Buffer): Uint8Array =>
30 encryptChromium("v10", value, key);
31
32const encryptWindowsV10 = (value: string | Buffer, key: Buffer): Uint8Array => {
33 const nonce = Buffer.from("0123456789ab");
34 const cipher = NodeCrypto.createCipheriv("aes-256-gcm", key, nonce);
35 const encrypted = Buffer.concat([cipher.update(value), cipher.final()]);
36 return Buffer.concat([Buffer.from("v10"), nonce, encrypted, cipher.getAuthTag()]);
37};
38
39describe("cookieScope", () => {
40 it("keeps a host-only cookie host-only", () => {
41 // Chromium stores a host-only cookie without a leading dot. Passing any
42 // `domain` to Electron makes it a domain cookie and re-adds the dot, which
43 // would expose the cookie to every subdomain it was never scoped to.
44 expect(cookieScope("example.test", "/", true)).toEqual({
45 url: "https://example.test/",
46 domain: undefined,
47 });
48 });
49
50 it("preserves a domain cookie's leading dot", () => {
51 expect(cookieScope(".example.test", "/app", true)).toEqual({
52 url: "https://example.test/app",
53 domain: ".example.test",
54 });
55 });
56
57 it("matches the scheme to the secure flag", () => {
58 expect(cookieScope("example.test", "/", false).url).toBe("http://example.test/");
59 });
60
61 it("brackets bare IPv6 hosts without duplicating existing brackets", () => {
62 expect(cookieScope("::1", "/", false)).toEqual({
63 url: "http://[::1]/",
64 domain: undefined,
65 });
66 expect(cookieScope("[::1]", "/app", true)).toEqual({
67 url: "https://[::1]/app",
68 domain: undefined,
69 });
70 });
71});
72
73describe("readChromiumCookieDatabase", () => {
74 it("decrypts Windows v10 AES-GCM records and rejects app-bound v20 records", () => {
75 const key = Buffer.from("0123456789abcdef0123456789abcdef");
76 const host = ".example.test";
77 const bound = Buffer.concat([
78 NodeCrypto.createHash("sha256").update(host).digest(),
79 Buffer.from("windows value"),
80 ]);
81
82 expect(
83 decryptChromiumValue(encryptWindowsV10(bound, key), { gcmV10: key }, host, 24, "win32"),
84 ).toBe("windows value");
85 expect(
86 decryptChromiumValue(Buffer.from("v20app-bound"), { gcmV10: key }, host, 24, "win32"),
87 ).toBeNull();
88 expect(
89 decryptChromiumValue(
90 encryptWindowsV10(bound, Buffer.alloc(32, 1)),
91 { gcmV10: key },
92 host,
93 24,
94 "win32",
95 ),
96 ).toBeNull();
97 });
98
99 it.effect(
100 "reports the missing key when no cookies can be read, while preserving partial imports",
101 () =>
102 Effect.gen(function* () {
103 const fileSystem = yield* FileSystem.FileSystem;
104 const directory = yield* fileSystem.makeTempDirectoryScoped({
105 prefix: "t3code-missing-key-",
106 });
107 const filename = `${directory}/Cookies`;
108 const key = Buffer.from("0123456789abcdef");
109 yield* Effect.gen(function* () {
110 const sql = yield* SqlClient.SqlClient;
111 yield* sql`create table meta (key text primary key, value text not null)`;
112 yield* sql`insert into meta values ('version', 23)`;
113 yield* sql`create table cookies (
114 host_key text not null, name text not null, value text not null,
115 encrypted_value blob not null, path text not null, expires_utc integer not null,
116 is_secure integer not null, is_httponly integer not null, samesite integer not null,
117 top_frame_site_key text not null default ''
118 )`;
119 yield* sql`insert into cookies values ('v11.example', 'session', '', ${encryptChromium("v11", "secret", key)}, '/', 0, 1, 1, 1, '')`;
120 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
121
122 const error = yield* readChromiumCookies({
123 cookieDatabasePath: filename,
124 platform: "linux",
125 linuxSecretApplication: "chromium",
126 keychainService: undefined,
127 keychainAccount: undefined,
128 }).pipe(Effect.provideService(LinuxBrowserSecretPath, undefined), Effect.flip);
129 expect(error.reason).toBe("keychainUnavailable");
130
131 yield* Effect.gen(function* () {
132 const sql = yield* SqlClient.SqlClient;
133 yield* sql`insert into cookies values ('v10.example', 'readable', '', ${encryptV10("kept", key)}, '/', 0, 1, 1, 1, '')`;
134 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
135 const keys = {
136 cbcV10: key,
137 cbcV11Error: new ChromiumKeyError({ reason: "keychainUnavailable" }),
138 };
139 const partial = yield* readChromiumCookieDatabase(filename, keys, "linux");
140 expect(partial.cookies.map((cookie) => cookie.value)).toEqual(["kept"]);
141 expect(partial.undecryptable).toBe(1);
142
143 // A partitioned-only jar does not need its key: it is skipped separately.
144 yield* Effect.gen(function* () {
145 const sql = yield* SqlClient.SqlClient;
146 yield* sql`delete from cookies where name = 'readable'`;
147 yield* sql`update cookies set top_frame_site_key = 'https://top.example'`;
148 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
149 const partitioned = yield* readChromiumCookieDatabase(filename, keys, "linux");
150 expect(partitioned.cookies).toEqual([]);
151 expect(partitioned.undecryptable).toBe(1);
152 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
153 );
154
155 it.effect("reads plaintext, encrypted, and genuinely empty cookie values", () =>
156 Effect.gen(function* () {
157 const fileSystem = yield* FileSystem.FileSystem;
158 const directory = yield* fileSystem.makeTempDirectoryScoped({
159 prefix: "t3code-chromium-cookies-",
160 });
161 const filename = `${directory}/Cookies`;
162 const key = Buffer.from("0123456789abcdef");
163
164 yield* Effect.gen(function* () {
165 const sql = yield* SqlClient.SqlClient;
166 yield* sql`create table meta (key text primary key, value text not null)`;
167 yield* sql`insert into meta values ('version', 23)`;
168 yield* sql`
169 create table cookies (
170 host_key text not null,
171 name text not null,
172 value text not null,
173 encrypted_value blob not null,
174 path text not null,
175 expires_utc integer not null,
176 is_secure integer not null,
177 is_httponly integer not null,
178 samesite integer not null,
179 top_frame_site_key text not null default ''
180 )
181 `;
182 yield* sql`
183 insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
184 ('plain.example', 'plain', 'stored plaintext', ${new Uint8Array()}, '/', 0, 0, 0, -1)
185 `;
186 yield* sql`
187 insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
188 ('secure.example', 'encrypted', '', ${encryptV10("stored encrypted", key)}, '/', 0, 1, 1, 2)
189 `;
190 yield* sql`
191 insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
192 ('empty.example', 'empty', '', ${new Uint8Array()}, '/', 0, 0, 0, 0)
193 `;
194 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
195
196 const result = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");
197
198 expect(result.undecryptable).toBe(0);
199 expect(result.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
200 { name: "plain", value: "stored plaintext" },
201 { name: "encrypted", value: "stored encrypted" },
202 { name: "empty", value: "" },
203 ]);
204 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
205 );
206
207 it.effect("enforces domain binding only for schema 24 and newer", () =>
208 Effect.gen(function* () {
209 const fileSystem = yield* FileSystem.FileSystem;
210 const directory = yield* fileSystem.makeTempDirectoryScoped({
211 prefix: "t3code-chromium-cookies-",
212 });
213 const filename = `${directory}/Cookies`;
214 const key = Buffer.from("0123456789abcdef");
215 const boundValue = (host: string, value: string) =>
216 Buffer.concat([NodeCrypto.createHash("sha256").update(host).digest(), Buffer.from(value)]);
217
218 yield* Effect.gen(function* () {
219 const sql = yield* SqlClient.SqlClient;
220 yield* sql`create table meta (key text primary key, value text not null)`;
221 yield* sql`insert into meta values ('version', 24)`;
222 yield* sql`
223 create table cookies (
224 host_key text not null, name text not null, value text not null,
225 encrypted_value blob not null, path text not null, expires_utc integer not null,
226 is_secure integer not null, is_httponly integer not null, samesite integer not null,
227 top_frame_site_key text not null default ''
228 )
229 `;
230 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
231 ('bound.example', 'valid', '', ${encryptV10(boundValue("bound.example", "kept"), key)}, '/', 0, 1, 0, 0)`;
232 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
233 ('wrong.example', 'mismatch', '', ${encryptV10(boundValue("another.example", "drop"), key)}, '/', 0, 1, 0, 0)`;
234 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
235 ('short.example', 'short', '', ${encryptV10("short value", key)}, '/', 0, 1, 0, 0)`;
236 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
237
238 const result = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");
239
240 expect(result.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
241 { name: "valid", value: "kept" },
242 ]);
243 expect(result.undecryptable).toBe(2);
244 expect(result.undecryptableHosts).toEqual(["wrong.example", "short.example"]);
245 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
246 );
247
248 it.effect("decrypts mixed v10 and v11 cookies with their respective keys", () =>
249 Effect.gen(function* () {
250 const fileSystem = yield* FileSystem.FileSystem;
251 const directory = yield* fileSystem.makeTempDirectoryScoped({
252 prefix: "t3code-chromium-cookies-",
253 });
254 const filename = `${directory}/Cookies`;
255 const cbcV10 = Buffer.from("0123456789abcdef");
256 const cbcV11 = Buffer.from("fedcba9876543210");
257 const boundValue = (host: string, value: string) =>
258 Buffer.concat([NodeCrypto.createHash("sha256").update(host).digest(), Buffer.from(value)]);
259
260 yield* Effect.gen(function* () {
261 const sql = yield* SqlClient.SqlClient;
262 yield* sql`create table meta (key text primary key, value text not null)`;
263 yield* sql`insert into meta values ('version', '24')`;
264 yield* sql`
265 create table cookies (
266 host_key text not null, name text not null, value text not null,
267 encrypted_value blob not null, path text not null, expires_utc integer not null,
268 is_secure integer not null, is_httponly integer not null, samesite integer not null,
269 top_frame_site_key text not null default ''
270 )
271 `;
272 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
273 ('v10.example', 'v10-cookie', '', ${encryptChromium("v10", boundValue("v10.example", "v10 value"), cbcV10)}, '/', 0, 1, 0, 0)`;
274 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
275 ('v11.example', 'v11-cookie', '', ${encryptChromium("v11", boundValue("v11.example", "v11 value"), cbcV11)}, '/', 0, 1, 0, 0)`;
276 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
277
278 const complete = yield* readChromiumCookieDatabase(filename, { cbcV10, cbcV11 }, "linux");
279 expect(complete.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
280 { name: "v10-cookie", value: "v10 value" },
281 { name: "v11-cookie", value: "v11 value" },
282 ]);
283 expect(complete.undecryptable).toBe(0);
284
285 const v10Only = yield* readChromiumCookieDatabase(filename, { cbcV10 }, "linux");
286 expect(v10Only.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
287 { name: "v10-cookie", value: "v10 value" },
288 ]);
289 expect(v10Only.undecryptable).toBe(1);
290 expect(v10Only.undecryptableHosts).toEqual(["v11.example"]);
291 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
292 );
293
294 it.effect("recovers records written with the empty-passphrase key", () =>
295 Effect.gen(function* () {
296 const fileSystem = yield* FileSystem.FileSystem;
297 const directory = yield* fileSystem.makeTempDirectoryScoped({
298 prefix: "t3code-chromium-cookies-",
299 });
300 const filename = `${directory}/Cookies`;
301 const cbcV10 = Buffer.from("0123456789abcdef");
302 const cbcV11 = Buffer.from("fedcba9876543210");
303 // The key some Linux clients actually encrypted with (crbug.com/1195256):
304 // OSCrypt's derivation over an empty passphrase.
305 const cbcEmpty = NodeCrypto.pbkdf2Sync("", "saltysalt", 1, 16, "sha1");
306
307 yield* Effect.gen(function* () {
308 const sql = yield* SqlClient.SqlClient;
309 yield* sql`create table meta (key text primary key, value text not null)`;
310 yield* sql`insert into meta values ('version', '23')`;
311 yield* sql`
312 create table cookies (
313 host_key text not null, name text not null, value text not null,
314 encrypted_value blob not null, path text not null, expires_utc integer not null,
315 is_secure integer not null, is_httponly integer not null, samesite integer not null,
316 top_frame_site_key text not null default ''
317 )
318 `;
319 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
320 ('ev10.example', 'empty-v10', '', ${encryptChromium("v10", "empty v10 value", cbcEmpty)}, '/', 0, 1, 0, 0)`;
321 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
322 ('ev11.example', 'empty-v11', '', ${encryptChromium("v11", "empty v11 value", cbcEmpty)}, '/', 0, 1, 0, 0)`;
323 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
324
325 // The records' own keys fail, and the empty key recovers both — the
326 // retry Chromium itself performs.
327 const recovered = yield* readChromiumCookieDatabase(
328 filename,
329 { cbcV10, cbcV11, cbcEmpty },
330 "linux",
331 );
332 expect(recovered.cookies.map(({ name, value }) => ({ name, value }))).toEqual([
333 { name: "empty-v10", value: "empty v10 value" },
334 { name: "empty-v11", value: "empty v11 value" },
335 ]);
336 expect(recovered.undecryptable).toBe(0);
337
338 // Matching Chromium: a record whose own key is missing entirely is not
339 // retried with the empty key.
340 const noV11 = yield* readChromiumCookieDatabase(filename, { cbcV10, cbcEmpty }, "linux");
341 expect(noV11.cookies.map(({ name }) => name)).toEqual(["empty-v10"]);
342 expect(noV11.undecryptableHosts).toEqual(["ev11.example"]);
343 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
344 );
345
346 it.effect("preserves arbitrary long encrypted values from pre-24 schemas", () =>
347 Effect.gen(function* () {
348 const fileSystem = yield* FileSystem.FileSystem;
349 const directory = yield* fileSystem.makeTempDirectoryScoped({
350 prefix: "t3code-chromium-cookies-",
351 });
352 const filename = `${directory}/Cookies`;
353 const key = Buffer.from("0123456789abcdef");
354 const value = "x".repeat(32) + " legacy value";
355
356 yield* Effect.gen(function* () {
357 const sql = yield* SqlClient.SqlClient;
358 yield* sql`create table meta (key text primary key, value text not null)`;
359 yield* sql`insert into meta values ('version', 23)`;
360 yield* sql`
361 create table cookies (
362 host_key text not null, name text not null, value text not null,
363 encrypted_value blob not null, path text not null, expires_utc integer not null,
364 is_secure integer not null, is_httponly integer not null, samesite integer not null,
365 top_frame_site_key text not null default ''
366 )
367 `;
368 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
369 ('legacy.example', 'legacy', '', ${encryptV10(value, key)}, '/', 0, 0, 0, 0)`;
370 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
371
372 const result = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");
373 expect(result.cookies[0]?.value).toBe(value);
374 expect(result.undecryptable).toBe(0);
375 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
376 );
377
378 it.effect("rejects a malformed text schema version", () =>
379 Effect.gen(function* () {
380 const fileSystem = yield* FileSystem.FileSystem;
381 const directory = yield* fileSystem.makeTempDirectoryScoped({
382 prefix: "t3code-chromium-cookies-",
383 });
384 const filename = `${directory}/Cookies`;
385
386 yield* Effect.gen(function* () {
387 const sql = yield* SqlClient.SqlClient;
388 yield* sql`create table meta (key text primary key, value text not null)`;
389 yield* sql`insert into meta values ('version', 'not-a-version')`;
390 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
391
392 const error = yield* readChromiumCookieDatabase(
393 filename,
394 { cbcV10: Buffer.from("0123456789abcdef") },
395 "darwin",
396 ).pipe(Effect.flip);
397
398 expect(error._tag).toBe("SchemaError");
399 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
400 );
401
402 it.effect("treats unversioned encrypted values as legacy plaintext on macOS and Linux", () =>
403 Effect.gen(function* () {
404 const fileSystem = yield* FileSystem.FileSystem;
405 const directory = yield* fileSystem.makeTempDirectoryScoped({
406 prefix: "t3code-chromium-cookies-",
407 });
408 const filename = `${directory}/Cookies`;
409 const key = Buffer.from("0123456789abcdef");
410
411 yield* Effect.gen(function* () {
412 const sql = yield* SqlClient.SqlClient;
413 yield* sql`create table meta (key text primary key, value integer not null)`;
414 yield* sql`insert into meta values ('version', 23)`;
415 yield* sql`
416 create table cookies (
417 host_key text not null, name text not null, value text not null,
418 encrypted_value blob not null, path text not null, expires_utc integer not null,
419 is_secure integer not null, is_httponly integer not null, samesite integer not null,
420 top_frame_site_key text not null default ''
421 )
422 `;
423 yield* sql`insert into cookies (host_key, name, value, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite) values
424 ('legacy.example', 'legacy', '', ${Buffer.from("legacy cleartext")}, '/', 0, 0, 0, 0)`;
425 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename })));
426
427 // Chromium's OSCrypt returns unprefixed data as-is on both platforms
428 // (os_crypt_mac.mm and os_crypt_linux.cc: "old data saved as clear
429 // text"), so neither counts it as undecryptable.
430 const mac = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "darwin");
431 const linux = yield* readChromiumCookieDatabase(filename, { cbcV10: key }, "linux");
432
433 expect(mac.cookies[0]?.value).toBe("legacy cleartext");
434 expect(mac.undecryptable).toBe(0);
435 expect(linux.cookies[0]?.value).toBe("legacy cleartext");
436 expect(linux.undecryptable).toBe(0);
437 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
438 );
439
440 it.effect("skips partitioned cookies without breaking pre-CHIPS schemas", () =>
441 Effect.gen(function* () {
442 const fileSystem = yield* FileSystem.FileSystem;
443 const directory = yield* fileSystem.makeTempDirectoryScoped({
444 prefix: "t3code-chromium-cookies-",
445 });
446 const legacyFilename = `${directory}/LegacyCookies`;
447 const chipsFilename = `${directory}/ChipsCookies`;
448 const key = Buffer.from("0123456789abcdef");
449
450 yield* Effect.gen(function* () {
451 const sql = yield* SqlClient.SqlClient;
452 yield* sql`create table meta (key text primary key, value integer not null)`;
453 yield* sql`insert into meta values ('version', 14)`;
454 yield* sql`
455 create table cookies (
456 host_key text not null, name text not null, value text not null,
457 encrypted_value blob not null, path text not null, expires_utc integer not null,
458 is_secure integer not null, is_httponly integer not null, samesite integer not null
459 )
460 `;
461 yield* sql`insert into cookies values
462 ('legacy.example', 'legacy', 'kept', ${new Uint8Array()}, '/', 0, 0, 0, 0)`;
463 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: legacyFilename })));
464
465 yield* Effect.gen(function* () {
466 const sql = yield* SqlClient.SqlClient;
467 yield* sql`create table meta (key text primary key, value integer not null)`;
468 yield* sql`insert into meta values ('version', 23)`;
469 yield* sql`
470 create table cookies (
471 host_key text not null, name text not null, value text not null,
472 encrypted_value blob not null, path text not null, expires_utc integer not null,
473 is_secure integer not null, is_httponly integer not null, samesite integer not null,
474 top_frame_site_key text not null
475 )
476 `;
477 yield* sql`insert into cookies values
478 ('plain.example', 'plain', 'kept', ${new Uint8Array()}, '/', 0, 0, 0, 0, '')`;
479 yield* sql`insert into cookies values
480 ('partitioned.example', 'partitioned', 'must skip', ${new Uint8Array()}, '/', 0, 1, 0, 0, 'https://top.example')`;
481 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: chipsFilename })));
482
483 const legacy = yield* readChromiumCookieDatabase(legacyFilename, { cbcV10: key }, "darwin");
484 const chips = yield* readChromiumCookieDatabase(chipsFilename, { cbcV10: key }, "darwin");
485
486 expect(legacy.cookies.map(({ name }) => name)).toEqual(["legacy"]);
487 expect(legacy.undecryptable).toBe(0);
488 expect(chips.cookies.map(({ name }) => name)).toEqual(["plain"]);
489 expect(chips.undecryptable).toBe(1);
490 expect(chips.undecryptableHosts).toEqual(["partitioned.example"]);
491 }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
492 );
493});