browser-commander 0.32.0 → 0.32.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.32.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3b7563e: CI/CD: crates.io publishing tries trusted publishing (OIDC) first and falls back to the token secret, and the release preflight verifies that fallback.
8
+
9
+ Prepared profiles keep Chrome 155's "Continue where you left off" session restore infobar closed by writing `browser.session_restore_infobar_times_shown` to the profile's `Preferences`.
10
+
3
11
  ## 0.32.0
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -141,8 +141,15 @@ export async function configureUserDataDir(
141
141
  ) {
142
142
  throw new TypeError('profileDirectory must be one profile directory name');
143
143
  }
144
+ // Chrome 155 opens a "Continue where you left off" session restore infobar
145
+ // in a new profile until it has been shown 3 times.
144
146
  const profilePreferences = deepMerge(
145
- { browser: { check_default_browser: false } },
147
+ {
148
+ browser: {
149
+ check_default_browser: false,
150
+ session_restore_infobar_times_shown: 5,
151
+ },
152
+ },
146
153
  preferences
147
154
  );
148
155
  assertObject(profilePreferences.browser, 'preferences.browser');
@@ -135,7 +135,10 @@ describe('profile directory', () => {
135
135
  }
136
136
  );
137
137
  assert.deepEqual(await readPreferences(dir), {
138
- browser: { check_default_browser: false },
138
+ browser: {
139
+ check_default_browser: false,
140
+ session_restore_infobar_times_shown: 5,
141
+ },
139
142
  });
140
143
  } finally {
141
144
  await removeUserDataDir(dir);
@@ -181,7 +184,11 @@ describe('profile directory', () => {
181
184
  localState: { browser: { extra: 1 } },
182
185
  });
183
186
  assert.deepEqual(await readPreferences(dir), {
184
- browser: { check_default_browser: false, show_home_button: true },
187
+ browser: {
188
+ check_default_browser: false,
189
+ session_restore_infobar_times_shown: 5,
190
+ show_home_button: true,
191
+ },
185
192
  intl: { accept_languages: 'en' },
186
193
  });
187
194
  assert.deepEqual(
@@ -90,6 +90,7 @@ function environment(overrides) {
90
90
  NPM_REGISTRY: `${stub.origin}/npm`,
91
91
  CRATES_API: `${stub.origin}/crates`,
92
92
  NPM_PACKAGE: '@link-foundation/probe',
93
+ CRATES_PACKAGE: 'probe-crate',
93
94
  CARGO_REGISTRY_TOKEN: '',
94
95
  CARGO_TOKEN: '',
95
96
  GITHUB_STEP_SUMMARY: '',
@@ -133,6 +134,13 @@ const EXCHANGE =
133
134
  'POST /npm/-/npm/v1/oidc/token/exchange/package/@link-foundation%2Fprobe';
134
135
  const CRATES_MINT = 'POST /crates/api/v1/trusted_publishing/tokens';
135
136
  const CRATES_REVOKE = 'DELETE /crates/api/v1/trusted_publishing/tokens';
137
+ const CRATES_PUBLISH = 'PUT /crates/api/v1/crates/new';
138
+ // What crates.io answers to an authorised publish that omits the archive.
139
+ const TOKEN_ACCEPTED = [
140
+ 400,
141
+ { errors: [{ detail: 'invalid tarball length' }] },
142
+ ];
143
+ const TOKEN_REJECTED = [403, { errors: [{ detail: 'authentication failed' }] }];
136
144
  const INVALID_PUBLISHER = [
137
145
  422,
138
146
  {
@@ -157,6 +165,33 @@ function probeCrates(answer, overrides = {}) {
157
165
  return preflight({ PREFLIGHT_REGISTRIES: 'crates', ...overrides });
158
166
  }
159
167
 
168
+ /**
169
+ * Refuse the crates.io OIDC exchange and answer the token publish probe with
170
+ * `answer`, so the outcome rests on the token fallback.
171
+ */
172
+ function probeFallback(answer, overrides = {}) {
173
+ stub.routes[CRATES_PUBLISH] = answer;
174
+ return probeCrates([403, {}], { CARGO_TOKEN: CARGO_SECRET, ...overrides });
175
+ }
176
+
177
+ /** Run the crates.io probe in a job without `id-token: write`. */
178
+ function preflightWithoutOidc(overrides = {}) {
179
+ return preflight({
180
+ PREFLIGHT_REGISTRIES: 'crates',
181
+ ACTIONS_ID_TOKEN_REQUEST_URL: '',
182
+ ACTIONS_ID_TOKEN_REQUEST_TOKEN: '',
183
+ ...overrides,
184
+ });
185
+ }
186
+
187
+ /** The metadata of a token publish probe: a u32 length, then JSON. */
188
+ function probeMetadata(body) {
189
+ const bytes = Buffer.from(body, 'latin1');
190
+ const length = bytes.readUInt32LE(0);
191
+ assert.equal(bytes.length, 4 + length, 'the probe must carry no archive');
192
+ return JSON.parse(bytes.subarray(4).toString('utf8'));
193
+ }
194
+
160
195
  describe('scripts/preflight-credentials.sh', scriptOptions, () => {
161
196
  before(async () => {
162
197
  stub.server = createServer(respond);
@@ -349,11 +384,11 @@ describe('scripts/preflight-credentials.sh', scriptOptions, () => {
349
384
  assertNoSecrets(output);
350
385
  });
351
386
 
352
- it('fails a release when no trusted publisher matches, even with a legacy secret', async () => {
353
- const { status, output } = await probeCrates(
354
- [403, { errors: [{ detail: 'no matching publisher' }] }],
355
- { CARGO_TOKEN: CARGO_SECRET }
356
- );
387
+ it('fails a release when no trusted publisher matches and no token is set', async () => {
388
+ const { status, output } = await probeCrates([
389
+ 403,
390
+ { errors: [{ detail: 'no matching publisher' }] },
391
+ ]);
357
392
 
358
393
  assert.equal(status, 1, output);
359
394
  assert.match(
@@ -362,18 +397,80 @@ describe('scripts/preflight-credentials.sh', scriptOptions, () => {
362
397
  );
363
398
  assert.match(output, /rust\.yml/);
364
399
  assert.match(output, /docs\/PUBLISHING\.md/);
400
+ assert.match(output, /neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN/);
365
401
  assert.equal(stub.requests.length, 2);
402
+ });
403
+
404
+ it('passes a release on the token fallback when no trusted publisher matches', async () => {
405
+ const { status, output } = await probeFallback(TOKEN_ACCEPTED);
406
+
407
+ assert.equal(status, 0, output);
408
+ assert.match(output, /1 verified, 0 failed, 0 unknown/);
409
+ assert.match(
410
+ output,
411
+ /PASS: crates.io accepted the CARGO_REGISTRY_TOKEN\/CARGO_TOKEN fallback for probe-crate/
412
+ );
413
+ assert.match(
414
+ output,
415
+ /::warning::release-preflight: crates.io refused the OIDC token exchange \(403\).*falls back to the CARGO_REGISTRY_TOKEN\/CARGO_TOKEN secret/
416
+ );
417
+ assert.doesNotMatch(output, /::error::/);
418
+ const publish = stub.requests.at(-1);
419
+ assert.equal(publish.route, CRATES_PUBLISH);
420
+ assert.equal(publish.authorization, CARGO_SECRET);
421
+ assert.match(publish.userAgent, /^release-preflight /);
422
+ assert.deepEqual(probeMetadata(publish.body), {
423
+ name: 'probe-crate',
424
+ vers: '0.0.0-preflight',
425
+ });
426
+ assert.equal(stub.requests.length, 3);
366
427
  assertNoSecrets(output);
367
428
  });
368
429
 
369
- it('fails when the job has no OIDC permission, with no secret fallback', async () => {
370
- const { status, output } = await preflight({
371
- PREFLIGHT_REGISTRIES: 'crates',
372
- ACTIONS_ID_TOKEN_REQUEST_URL: '',
373
- ACTIONS_ID_TOKEN_REQUEST_TOKEN: '',
430
+ it('prefers CARGO_REGISTRY_TOKEN over CARGO_TOKEN for the fallback', async () => {
431
+ const { status, output } = await probeFallback(TOKEN_ACCEPTED, {
432
+ CARGO_REGISTRY_TOKEN: CARGO_SECRET,
433
+ CARGO_TOKEN: 'cio-other-token-must-not-be-used',
434
+ });
435
+
436
+ assert.equal(status, 0, output);
437
+ assert.equal(stub.requests.at(-1).authorization, CARGO_SECRET);
438
+ });
439
+
440
+ it('fails a release when trusted publishing and the token fallback are both refused', async () => {
441
+ const { status, output } = await probeFallback(TOKEN_REJECTED);
442
+
443
+ assert.equal(status, 1, output);
444
+ assert.match(output, /0 verified, 1 failed, 0 unknown/);
445
+ assert.match(
446
+ output,
447
+ /::error::release-preflight: crates.io refused the OIDC token exchange \(403\).*; crates.io rejected the CARGO_REGISTRY_TOKEN\/CARGO_TOKEN fallback for probe-crate \(403\)/
448
+ );
449
+ assert.equal(stub.requests.length, 3);
450
+ assertNoSecrets(output);
451
+ });
452
+
453
+ it('falls back to the token when the job has no OIDC permission', async () => {
454
+ stub.routes[CRATES_PUBLISH] = TOKEN_ACCEPTED;
455
+ const { status, output } = await preflightWithoutOidc({
374
456
  CARGO_REGISTRY_TOKEN: CARGO_SECRET,
375
457
  });
376
458
 
459
+ assert.equal(status, 0, output);
460
+ assert.match(
461
+ output,
462
+ /::warning::release-preflight: crates.io: no OIDC token available: .*id-token: write/
463
+ );
464
+ assert.deepEqual(
465
+ stub.requests.map(({ route }) => route),
466
+ [CRATES_PUBLISH]
467
+ );
468
+ assertNoSecrets(output);
469
+ });
470
+
471
+ it('fails when the job has no OIDC permission and no token is set', async () => {
472
+ const { status, output } = await preflightWithoutOidc();
473
+
377
474
  assert.equal(status, 1, output);
378
475
  assert.match(
379
476
  output,
@@ -382,6 +479,54 @@ describe('scripts/preflight-credentials.sh', scriptOptions, () => {
382
479
  assert.equal(stub.requests.length, 0);
383
480
  });
384
481
 
482
+ it('treats an unproven token fallback as unknown, never as a pass', async () => {
483
+ for (const answer of [
484
+ [400, { errors: [{ detail: 'invalid upload request' }] }],
485
+ [429, {}],
486
+ [500, { token: CARGO_SECRET }],
487
+ ]) {
488
+ const { status, output } = await probeFallback(answer);
489
+ assert.equal(status, 1, output);
490
+ assert.match(output, /0 verified, 0 failed, 1 unknown/);
491
+ assert.match(output, /verified nothing/);
492
+ assertNoSecrets(output);
493
+ }
494
+ });
495
+
496
+ it('reads the crate name from Cargo.toml when CRATES_PACKAGE is unset', async () => {
497
+ writeFileSync(
498
+ path.join(workDir, 'Cargo.toml'),
499
+ [
500
+ '[package]',
501
+ 'name = "from-manifest"',
502
+ 'version = "1.2.3"',
503
+ '',
504
+ '[dependencies.serde]',
505
+ 'name = "not-this-one"',
506
+ '',
507
+ ].join('\n')
508
+ );
509
+ const { status, output } = await probeFallback(TOKEN_ACCEPTED, {
510
+ CRATES_PACKAGE: '',
511
+ });
512
+
513
+ assert.equal(status, 0, output);
514
+ assert.equal(
515
+ probeMetadata(stub.requests.at(-1).body).name,
516
+ 'from-manifest'
517
+ );
518
+ });
519
+
520
+ it('cannot probe the token without a crate name', async () => {
521
+ const { status, output } = await probeFallback(TOKEN_ACCEPTED, {
522
+ CRATES_PACKAGE: '',
523
+ });
524
+
525
+ assert.equal(status, 1, output);
526
+ assert.match(output, /cannot tell which crate to probe/);
527
+ assert.equal(stub.requests.length, 2);
528
+ });
529
+
385
530
  it('keeps missing publishers advisory on pull requests', async () => {
386
531
  const { status, output } = await probeCrates([403, {}], {
387
532
  PREFLIGHT_MODE: 'report',
@@ -429,6 +574,7 @@ describe('scripts/preflight-credentials.sh', scriptOptions, () => {
429
574
  stub.routes[MINT] = INVALID_PUBLISHER;
430
575
  stub.routes[EXCHANGE] = [403, { message: 'forbidden' }];
431
576
  stub.routes[CRATES_MINT] = [401, { errors: [{ detail: 'bad token' }] }];
577
+ stub.routes[CRATES_PUBLISH] = TOKEN_REJECTED;
432
578
  const summary = path.join(workDir, 'summary.md');
433
579
  writeFileSync(summary, '');
434
580
 
@@ -7,8 +7,13 @@ import { readRepoText } from '../../helpers/repo.js';
7
7
  describe('trusted publishing configuration', () => {
8
8
  const rust = parse(readRepoText('.github/workflows/rust.yml'));
9
9
 
10
+ // Organization policy: trusted publishing (OIDC) first, the token secret
11
+ // only when OIDC is unavailable.
12
+ const FALLBACK =
13
+ '${{ steps.crates_auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}';
14
+
10
15
  for (const jobId of ['auto-release', 'manual-release']) {
11
- it(`${jobId} uses a fresh OIDC token only when publishing`, () => {
16
+ it(`${jobId} publishes with a fresh OIDC token, falling back to the token secret`, () => {
12
17
  const job = rust.jobs[jobId];
13
18
  assert.equal(job.permissions['id-token'], 'write');
14
19
  const auth = job.steps.find((step) => step.id === 'crates_auth');
@@ -20,9 +25,11 @@ describe('trusted publishing configuration', () => {
20
25
  );
21
26
  assert.equal(auth.if, publish.if);
22
27
  assert.equal(
23
- publish.env.CARGO_REGISTRY_TOKEN,
24
- '${{ steps.crates_auth.outputs.token }}'
28
+ auth['continue-on-error'],
29
+ true,
30
+ 'a trusted publishing failure must leave the token fallback reachable'
25
31
  );
32
+ assert.equal(publish.env.CARGO_REGISTRY_TOKEN, FALLBACK);
26
33
  assert.ok(job.steps.indexOf(auth) < job.steps.indexOf(publish));
27
34
  assert.ok(
28
35
  job.steps.indexOf(auth) >
@@ -31,13 +38,28 @@ describe('trusted publishing configuration', () => {
31
38
  });
32
39
  }
33
40
 
34
- it('never reads the long-lived Cargo secret in the Rust workflow', () => {
35
- assert.doesNotMatch(
36
- readRepoText('.github/workflows/rust.yml'),
37
- /secrets\.CARGO_TOKEN/
41
+ it('gives the release preflight the same token fallback', () => {
42
+ const probe = rust.jobs['release-preflight'].steps.find(
43
+ (step) => step.name === 'Probe crates.io trusted publishing'
44
+ );
45
+ assert.equal(
46
+ probe.env.CARGO_REGISTRY_TOKEN,
47
+ '${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}'
38
48
  );
39
49
  });
40
50
 
51
+ it('reads the Cargo secrets only as the trusted publishing fallback', () => {
52
+ const uses = readRepoText('.github/workflows/rust.yml')
53
+ .split('\n')
54
+ .filter((line) => /secrets\.CARGO_/.test(line))
55
+ .map((line) => line.trim());
56
+ assert.deepEqual(uses, [
57
+ 'CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}',
58
+ `CARGO_REGISTRY_TOKEN: ${FALLBACK}`,
59
+ `CARGO_REGISTRY_TOKEN: ${FALLBACK}`,
60
+ ]);
61
+ });
62
+
41
63
  it('keeps the registered PyPI identity identical across preflight and releases', () => {
42
64
  const python = parse(readRepoText('.github/workflows/python.yml'));
43
65
  for (const jobId of [