@bitfab/sdk 0.64.16 → 0.64.17

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.
Files changed (36) hide show
  1. package/cloudReplay.py +282 -24
  2. package/dist/{chunk-AXFKZYIY.js → chunk-G5BYSZLD.js} +11 -6
  3. package/dist/chunk-G5BYSZLD.js.map +1 -0
  4. package/dist/{chunk-PAWBCEI7.js → chunk-I3LSDFMN.js} +2 -2
  5. package/dist/{chunk-PAWBCEI7.js.map → chunk-I3LSDFMN.js.map} +1 -1
  6. package/dist/chunk-PQ6ABBRQ.js +1905 -0
  7. package/dist/chunk-PQ6ABBRQ.js.map +1 -0
  8. package/dist/{chunk-NNZAQSTW.js → chunk-QZFVHNQE.js} +2 -2
  9. package/dist/{chunk-LOIVELPI.js → chunk-WLGH45CM.js} +204 -89
  10. package/dist/chunk-WLGH45CM.js.map +1 -0
  11. package/dist/{chunk-I4TFQZCW.js → chunk-YHKFC7XZ.js} +16 -2
  12. package/dist/{chunk-I4TFQZCW.js.map → chunk-YHKFC7XZ.js.map} +1 -1
  13. package/dist/{http-QQ6VVHUZ.js → http-AYZZNM2L.js} +2 -2
  14. package/dist/{http-LV7OUBKA.js → http-DAQLSTUN.js} +2 -2
  15. package/dist/index.cjs +6 -1
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +3 -3
  20. package/dist/node.cjs +6 -1
  21. package/dist/node.cjs.map +1 -1
  22. package/dist/node.js +3 -3
  23. package/dist/replay-ARKSPOVK.js +28 -0
  24. package/dist/{replay-JNKXZPY7.js → replay-FFNEMPGO.js} +3 -3
  25. package/dist/replay-FFNEMPGO.js.map +1 -0
  26. package/dist/replayCli.js +3 -2
  27. package/dist/replayCli.js.map +1 -1
  28. package/dist/seedCli.js +3 -2
  29. package/dist/seedCli.js.map +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-AXFKZYIY.js.map +0 -1
  32. package/dist/chunk-LOIVELPI.js.map +0 -1
  33. /package/dist/{chunk-NNZAQSTW.js.map → chunk-QZFVHNQE.js.map} +0 -0
  34. /package/dist/{http-LV7OUBKA.js.map → http-AYZZNM2L.js.map} +0 -0
  35. /package/dist/{http-QQ6VVHUZ.js.map → http-DAQLSTUN.js.map} +0 -0
  36. /package/dist/{replay-JNKXZPY7.js.map → replay-ARKSPOVK.js.map} +0 -0
package/cloudReplay.py CHANGED
@@ -36,6 +36,16 @@ SDK_LANGUAGE_ENV = "BITFAB_SDK_LANGUAGE"
36
36
  CHECK_COMMAND_ENV = "BITFAB_REPLAY_CHECK"
37
37
  EXECUTION_TARGET_ENV = "BITFAB_REPLAY_EXECUTION_TARGET"
38
38
  HOSTED_EXECUTION_TARGET = "hosted"
39
+ API_KEY_ENV = "BITFAB_REPLAY_BITFAB_API_KEY"
40
+ PRINT_API_KEY_FLAG = "--print-api-key"
41
+ API_KEY_LINE = "@@bitfab:api-key "
42
+ KEY_INPUT = "key_id"
43
+ KEY_SECRET_PREFIX = "BITFAB_REPLAY_KEY_"
44
+ KEY_SECRET_REFERENCE = (
45
+ "${{ secrets[format('" + KEY_SECRET_PREFIX + "{0}', inputs." + KEY_INPUT + ")] }}"
46
+ )
47
+ API_KEY_PROBE_SECONDS = 120
48
+ RUN_NAME = "Bitfab replay ${{ inputs.execution_id || github.ref_name }}"
39
49
  REQUEST_VERSION = 4
40
50
  PLAIN_REQUEST_VERSION = 3
41
51
  OLDEST_DISPATCHED_VERSION = 2
@@ -95,7 +105,7 @@ ENVIRONMENT_SETTING = re.compile(
95
105
  PUSH_TRIGGER = re.compile(
96
106
  r"^[ \t]*\"?(?:on\"?[ \t]*:.*\bpush\b|push\"?[ \t]*:)", re.MULTILINE
97
107
  )
98
- DISPATCH_INPUTS = ("execution_id", "request")
108
+ DISPATCH_INPUTS = ("execution_id", "request", KEY_INPUT)
99
109
  BRANCH_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*")
100
110
  ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
101
111
  RESERVED_ENV_PREFIXES = ("GITHUB_", "RUNNER_", "ACTIONS_", "BITFAB_REPLAY_")
@@ -204,6 +214,14 @@ values --cloud-env sets. version and id are optional: version names the request
204
214
  format (4 when env is set, otherwise 3; 2 is still accepted), and id is the
205
215
  execution UUID, which must match an execution_id input when the workflow has one.
206
216
 
217
+ The replay runs with the Bitfab API key it would use on your machine, so the
218
+ experiment is recorded as yours. Before dispatch, the replay's registry is loaded
219
+ locally to read its client's key, which the GitHub CLI stores as a GitHub secret
220
+ named for this execution and deletes once the replay starts. Without the GitHub
221
+ CLI, without permission to manage the repository's Actions secrets, with no key
222
+ resolved locally, or with a workflow that declares no key_id input, the run uses
223
+ the workflow's BITFAB_API_KEY secret instead.
224
+
207
225
  The runner's job exits with the replay's exit code, so the Actions run fails when
208
226
  the replay does. Traces that errored fail it only under --fail-on-error.
209
227
 
@@ -216,7 +234,10 @@ Set up once:
216
234
  --cloud-init [--secret NAME ...] [--environment NAME] [--run COMMAND]
217
235
  [--runs-on LABEL] [--secret-prefix PREFIX] [--check COMMAND]
218
236
  Writes .github/workflows/bitfab-replay.yml, the only file cloud replay keeps
219
- in the repository, and lists what is left to do.
237
+ in the repository, and lists what is left to do. On an existing workflow it
238
+ adds only the lines a newer SDK needs and lists what it changed.
239
+ --cloud-update [same options]
240
+ The same command as --cloud-init, for bringing an existing setup up to date.
220
241
  --cloud-secrets --env-file FILE [NAME ...] [--environment NAME] [--dry-run]
221
242
  Copies local values into the GitHub secrets the workflow reads.
222
243
 
@@ -1189,6 +1210,7 @@ def print_outcome(found):
1189
1210
 
1190
1211
 
1191
1212
  def cleanup(root, record):
1213
+ remove_key(record)
1192
1214
  if record.get("cleaned"):
1193
1215
  return
1194
1216
  if record.get("snapshot") is False:
@@ -1335,10 +1357,110 @@ def submit(root, repo, workflow, parsed, path):
1335
1357
  ) from error
1336
1358
  record["state"] = "dispatch_unknown"
1337
1359
  save(path, record)
1338
- declared = declared_inputs(
1339
- within(root, f"{WORKFLOW_DIRECTORY}/{workflow}").read_text()
1360
+ text = within(root, f"{WORKFLOW_DIRECTORY}/{workflow}").read_text()
1361
+ declared = declared_inputs(text)
1362
+ key_inputs = store_key(root, record, declared, text)
1363
+ save(path, record)
1364
+ return dispatch(
1365
+ record,
1366
+ {"execution_id": execution_id, "request": "-", **key_inputs},
1367
+ declared,
1340
1368
  )
1341
- return dispatch(record, {"execution_id": execution_id, "request": "-"}, declared)
1369
+
1370
+
1371
+ def local_api_key(root, request):
1372
+ try:
1373
+ output = command(
1374
+ [*replay_command(), *request["args"], PRINT_API_KEY_FLAG],
1375
+ cwd=within(root, request["cwd"]),
1376
+ timeout=API_KEY_PROBE_SECONDS,
1377
+ input="",
1378
+ )
1379
+ except (ValueError, RuntimeError, OSError, subprocess.SubprocessError):
1380
+ return None
1381
+ for line in reversed(output.splitlines()):
1382
+ if not line.startswith(API_KEY_LINE):
1383
+ continue
1384
+ try:
1385
+ key = json.loads(line.removeprefix(API_KEY_LINE)).get("apiKey")
1386
+ except (ValueError, AttributeError):
1387
+ return None
1388
+ return key if isinstance(key, str) and key.strip() else None
1389
+ return None
1390
+
1391
+
1392
+ def store_key(root, record, declared, workflow_text):
1393
+ shared = "so the run uses the workflow's BITFAB_API_KEY secret"
1394
+ if KEY_INPUT not in declared:
1395
+ progress(
1396
+ f"The workflow takes no {KEY_INPUT} input, {shared}. To replay with your own Bitfab API key, run bitfab-replay --cloud-init, which adds the {KEY_INPUT} input and {API_KEY_ENV} to the workflow"
1397
+ )
1398
+ return {}
1399
+ if shutil.which("gh") is None:
1400
+ progress(
1401
+ f"Replaying with your own Bitfab API key needs the GitHub CLI (https://cli.github.com) to store it as a GitHub secret, {shared}"
1402
+ )
1403
+ return {}
1404
+ key = local_api_key(root, record["request"])
1405
+ if key is None:
1406
+ progress(
1407
+ f"The replay's Bitfab client resolved no API key on this machine, {shared}"
1408
+ )
1409
+ return {}
1410
+ key_id = record["id"].replace("-", "").upper()
1411
+ name = KEY_SECRET_PREFIX + key_id
1412
+ configured = ENVIRONMENT_SETTING.search(workflow_text)
1413
+ environment = configured[1] if configured else None
1414
+ try:
1415
+ command(
1416
+ [
1417
+ "gh",
1418
+ "secret",
1419
+ "set",
1420
+ name,
1421
+ "--repo",
1422
+ record["repository"],
1423
+ *(["--env", environment] if environment else []),
1424
+ ],
1425
+ input=key,
1426
+ env={**os.environ, "GH_TOKEN": github_access()["token"]},
1427
+ )
1428
+ except (ValueError, RuntimeError, OSError, subprocess.SubprocessError):
1429
+ progress(
1430
+ f"Could not store your Bitfab API key as the GitHub secret {name}, which needs permission to manage the repository's Actions secrets, {shared}"
1431
+ )
1432
+ return {}
1433
+ record["keySecret"] = {"name": name, "environment": environment}
1434
+ progress(
1435
+ f"Replaying with the Bitfab API key from this machine, stored as the GitHub secret {name} until the replay starts"
1436
+ )
1437
+ return {KEY_INPUT: key_id}
1438
+
1439
+
1440
+ def remove_key(record):
1441
+ secret = record.get("keySecret")
1442
+ if not secret or secret.get("removed"):
1443
+ return
1444
+ try:
1445
+ command(
1446
+ [
1447
+ "gh",
1448
+ "secret",
1449
+ "delete",
1450
+ secret["name"],
1451
+ "--repo",
1452
+ record["repository"],
1453
+ *(["--env", secret["environment"]] if secret["environment"] else []),
1454
+ ],
1455
+ env={**os.environ, "GH_TOKEN": github_access()["token"]},
1456
+ )
1457
+ except (ValueError, RuntimeError, OSError, subprocess.SubprocessError) as error:
1458
+ if getattr(error, "http_status", None) != 404:
1459
+ progress(
1460
+ f"Could not delete the GitHub secret {secret['name']}; delete it at {secrets_page(record['repository'], secret['environment'])}"
1461
+ )
1462
+ return
1463
+ secret["removed"] = True
1342
1464
 
1343
1465
 
1344
1466
  def declared_inputs(text):
@@ -1446,7 +1568,8 @@ def submit_as_is(root, repo, workflow, parsed, path):
1446
1568
  preflight(
1447
1569
  repo, workflow, pushes=False, full_output=request.get("fullOutput") is True
1448
1570
  )
1449
- declared = declared_inputs(branch_workflow(repo, workflow, sha))
1571
+ text = branch_workflow(repo, workflow, sha)
1572
+ declared = declared_inputs(text)
1450
1573
  if "request" not in declared:
1451
1574
  raise ValueError(
1452
1575
  f"{WORKFLOW_DIRECTORY}/{workflow} on {branch} declares no request input, so it cannot take a replay; add request (type: string, required: false) under on.workflow_dispatch.inputs and land it on {branch}"
@@ -1466,9 +1589,11 @@ def submit_as_is(root, repo, workflow, parsed, path):
1466
1589
  progress(
1467
1590
  f"Replaying {branch} at {sha[:12]} as it is on GitHub, without local changes. Execution {execution_id}; resume with --cloud-watch {execution_id}"
1468
1591
  )
1592
+ key_inputs = store_key(root, record, declared, text)
1593
+ save(path, record)
1469
1594
  return dispatch(
1470
1595
  record,
1471
- {"execution_id": execution_id, "request": json.dumps(request)},
1596
+ {"execution_id": execution_id, "request": json.dumps(request), **key_inputs},
1472
1597
  declared,
1473
1598
  run_details=True,
1474
1599
  )
@@ -1477,10 +1602,11 @@ def submit_as_is(root, repo, workflow, parsed, path):
1477
1602
  def report_steps(record, shown):
1478
1603
  job = replay_job(record)
1479
1604
  for step in (job or {}).get("steps", []):
1480
- if (
1481
- step.get("status") in ("in_progress", "completed")
1482
- and step.get("name") not in shown
1483
- ):
1605
+ if step.get("status") not in ("in_progress", "completed"):
1606
+ continue
1607
+ if "--cloud-execute" in (step.get("name") or ""):
1608
+ remove_key(record)
1609
+ if step.get("name") not in shown:
1484
1610
  shown.add(step.get("name"))
1485
1611
  progress(step.get("name"))
1486
1612
 
@@ -1701,6 +1827,11 @@ def execute():
1701
1827
  request = runner_request(root, commit)
1702
1828
  directory = within(root, request["cwd"])
1703
1829
  check_secrets(root)
1830
+ if os.environ.get(API_KEY_ENV, "").strip():
1831
+ print(
1832
+ "Replaying with the Bitfab API key of the person who started this run",
1833
+ flush=True,
1834
+ )
1704
1835
  environment = request.get("env") or {}
1705
1836
  if environment:
1706
1837
  workflow_environment_conflicts(root, environment)
@@ -2376,12 +2507,13 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
2376
2507
  ]
2377
2508
  return {
2378
2509
  "name": "Bitfab replay",
2379
- "run-name": "Bitfab replay ${{ inputs.execution_id || github.ref_name }}",
2510
+ "run-name": RUN_NAME,
2380
2511
  "on": {
2381
2512
  "workflow_dispatch": {
2382
2513
  "inputs": {
2383
2514
  "execution_id": {"type": "string", "required": False},
2384
2515
  "request": {"type": "string", "required": False},
2516
+ KEY_INPUT: {"type": "string", "required": False},
2385
2517
  }
2386
2518
  }
2387
2519
  },
@@ -2390,10 +2522,141 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
2390
2522
  }
2391
2523
 
2392
2524
 
2393
- def initialize(argv):
2525
+ def indentation(line):
2526
+ return len(line) - len(line.lstrip(" \t"))
2527
+
2528
+
2529
+ def block_end(lines, start, indent):
2530
+ end = start + 1
2531
+ while end < len(lines) and (
2532
+ not lines[end].strip() or indentation(lines[end]) > indent
2533
+ ):
2534
+ end += 1
2535
+ while end > start + 1 and not lines[end - 1].strip():
2536
+ end -= 1
2537
+ return end
2538
+
2539
+
2540
+ def add_input(lines, name):
2541
+ for index, line in enumerate(lines):
2542
+ if not re.fullmatch(r'[ \t]*"?request"?[ \t]*:[ \t]*', line):
2543
+ continue
2544
+ indent = indentation(line)
2545
+ child = " " * (
2546
+ indentation(lines[index + 1])
2547
+ if block_end(lines, index, indent) > index + 1
2548
+ else indent + 2
2549
+ )
2550
+ end = block_end(lines, index, indent - 1)
2551
+ lines[end:end] = [
2552
+ f"{line[:indent]}{name}:",
2553
+ f"{child}type: string",
2554
+ f"{child}required: false",
2555
+ ]
2556
+ return True
2557
+ return False
2558
+
2559
+
2560
+ def add_run_name(lines):
2561
+ named = next(
2562
+ (
2563
+ index
2564
+ for index, line in enumerate(lines)
2565
+ if re.match(r'"?name"?[ \t]*:', line)
2566
+ ),
2567
+ -1,
2568
+ )
2569
+ lines.insert(named + 1, f"run-name: {json.dumps(RUN_NAME)}")
2570
+
2571
+
2572
+ def replay_step_key(lines):
2573
+ for index, line in enumerate(lines):
2574
+ if "--cloud-execute" not in line:
2575
+ continue
2576
+ for above in range(index, -1, -1):
2577
+ match = re.match(r"([ \t]*)(- +)?run[ \t]*:", lines[above])
2578
+ if match and indentation(lines[above]) <= indentation(line):
2579
+ return above, len(match[1]) + len(match[2] or "")
2580
+ return None
2581
+
2582
+
2583
+ def add_key_env(lines):
2584
+ found = replay_step_key(lines)
2585
+ if found is None:
2586
+ return False
2587
+ run, key = found
2588
+ start = run
2589
+ while start > 0 and not re.match(r"[ \t]*- ", lines[start]):
2590
+ start -= 1
2591
+ end = block_end(lines, run, key - 1)
2592
+ entry = f'{API_KEY_ENV}: "{KEY_SECRET_REFERENCE}"'
2593
+ for index in range(start, end):
2594
+ line = lines[index]
2595
+ if not re.fullmatch(r"[ \t]*(- +)?env[ \t]*:[ \t]*", line):
2596
+ continue
2597
+ if len(line) - len(line.lstrip(" \t-")) != key:
2598
+ continue
2599
+ entries = block_end(lines, index, key)
2600
+ child = indentation(lines[index + 1]) if entries > index + 1 else key + 2
2601
+ lines.insert(entries, " " * child + entry)
2602
+ return True
2603
+ lines[end:end] = [" " * key + "env:", " " * (key + 2) + entry]
2604
+ return True
2605
+
2606
+
2607
+ def update_workflow(root, path):
2608
+ target = within(root, path)
2609
+ text = target.read_text()
2610
+ lines = text.split("\n")
2611
+ updated, by_hand = [], []
2612
+ declared = declared_inputs(text)
2613
+ for name in ("execution_id", KEY_INPUT):
2614
+ if name in declared:
2615
+ continue
2616
+ if add_input(lines, name):
2617
+ updated.append(f"Added the {name} input")
2618
+ else:
2619
+ by_hand.append(
2620
+ f"{name} (type: string, required: false) under on.workflow_dispatch.inputs"
2621
+ )
2622
+ if not re.search(r'^"?run-name"?[ \t]*:', text, re.MULTILINE):
2623
+ add_run_name(lines)
2624
+ updated.append("Named each run after its execution")
2625
+ if API_KEY_ENV not in text:
2626
+ if add_key_env(lines):
2627
+ updated.append(f"Added {API_KEY_ENV} to the replay step's env")
2628
+ else:
2629
+ by_hand.append(
2630
+ f'{API_KEY_ENV}: "{KEY_SECRET_REFERENCE}" in the replay step\'s env'
2631
+ )
2632
+ if updated:
2633
+ target.write_text("\n".join(lines))
2634
+ steps = []
2635
+ if updated:
2636
+ steps.append(
2637
+ f"Commit {path}. A --cloud replay runs the workflow in its own snapshot, so the next one uses it; --cloud-ref replays need it on the branch they replay"
2638
+ )
2639
+ if by_hand:
2640
+ steps.append(
2641
+ "Cloud replay could not place these lines, so add them by hand: "
2642
+ + "; ".join(by_hand)
2643
+ )
2644
+ if not steps:
2645
+ steps.append(
2646
+ "Already set up. Edit the workflow directly to change its install steps, secrets, runner, or Environment"
2647
+ )
2648
+ return {
2649
+ "files": [path] if updated else [],
2650
+ "workflow": path,
2651
+ "updated": updated,
2652
+ "next": steps,
2653
+ }
2654
+
2655
+
2656
+ def initialize(argv, name="--cloud-init"):
2394
2657
  parser = argparse.ArgumentParser(
2395
- prog="bitfab-replay --cloud-init",
2396
- description="Write .github/workflows/bitfab-replay.yml, the only file cloud replay keeps in the repository, and list what is left to do. It detects the runtime, package manager, install command, and replay command from the directory you run it in. An existing workflow is left alone; edit it directly to change it.",
2658
+ prog=f"bitfab-replay {name}",
2659
+ description="Write .github/workflows/bitfab-replay.yml, the only file cloud replay keeps in the repository, and list what is left to do. It detects the runtime, package manager, install command, and replay command from the directory you run it in. On an existing workflow it only adds what a newer SDK needs (the execution_id and key_id inputs, the run name, and BITFAB_REPLAY_BITFAB_API_KEY) and lists what it changed; edit it directly for anything else.",
2397
2660
  )
2398
2661
  parser.add_argument(
2399
2662
  "--secret",
@@ -2425,13 +2688,7 @@ def initialize(argv):
2425
2688
  refuse_old_setup(root)
2426
2689
  existing = find_workflow(root)
2427
2690
  if existing is not None:
2428
- return {
2429
- "files": [],
2430
- "workflow": f"{WORKFLOW_DIRECTORY}/{existing}",
2431
- "next": [
2432
- "Already set up. Edit the workflow directly to change its install steps, secrets, runner, or Environment"
2433
- ],
2434
- }
2691
+ return update_workflow(root, f"{WORKFLOW_DIRECTORY}/{existing}")
2435
2692
  prefix = args.secret_prefix
2436
2693
  if prefix and not re.fullmatch(r"[A-Z][A-Z0-9_]*_", prefix):
2437
2694
  raise ValueError(
@@ -2450,6 +2707,7 @@ def initialize(argv):
2450
2707
  'Pass --run with the command that starts your registry program on the runner, such as --run "go run ./cmd/registry"'
2451
2708
  )
2452
2709
  env = {name: "${{ secrets." + prefix + name + " }}" for name in names}
2710
+ env[API_KEY_ENV] = KEY_SECRET_REFERENCE
2453
2711
  if args.check:
2454
2712
  env[CHECK_COMMAND_ENV] = args.check
2455
2713
  path = f"{WORKFLOW_DIRECTORY}/{WORKFLOW}"
@@ -2506,8 +2764,8 @@ def print_json(value):
2506
2764
  def main():
2507
2765
  argv = sys.argv[1:]
2508
2766
  try:
2509
- if argv[:1] == ["--cloud-init"]:
2510
- print_json(initialize(argv[1:]))
2767
+ if argv[:1] in (["--cloud-init"], ["--cloud-update"]):
2768
+ print_json(initialize(argv[1:], argv[0]))
2511
2769
  return 0
2512
2770
  if argv[:1] == ["--cloud-secrets"]:
2513
2771
  print_json(configure_secrets(argv[1:]))
@@ -17,7 +17,7 @@ import {
17
17
  serializeValue,
18
18
  toJsonSafe,
19
19
  toJsonSafeReport
20
- } from "./chunk-NNZAQSTW.js";
20
+ } from "./chunk-QZFVHNQE.js";
21
21
  import {
22
22
  BitfabError,
23
23
  CONTENT_OFF_KEY,
@@ -36,7 +36,7 @@ import {
36
36
  readEnv,
37
37
  recordCallerTraceMetadata,
38
38
  retireTraceMetadata
39
- } from "./chunk-PAWBCEI7.js";
39
+ } from "./chunk-I3LSDFMN.js";
40
40
  import {
41
41
  __privateAdd,
42
42
  __privateGet,
@@ -4344,6 +4344,7 @@ function readEnv2(name) {
4344
4344
  return void 0;
4345
4345
  }
4346
4346
  var CAPTURE_ENABLED_ENV = "BITFAB_CAPTURE_ENABLED";
4347
+ var REPLAY_API_KEY_ENV = "BITFAB_REPLAY_BITFAB_API_KEY";
4347
4348
  var TRUE_ENV_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes"]);
4348
4349
  var FALSE_ENV_VALUES = /* @__PURE__ */ new Set(["0", "false", "no"]);
4349
4350
  function readBooleanEnv(name) {
@@ -4858,6 +4859,10 @@ var Bitfab = class {
4858
4859
  * early resolve that found nothing never poisons a later one.
4859
4860
  */
4860
4861
  findApiKey(callsKeyFunction) {
4862
+ const replayKey = readEnv2(REPLAY_API_KEY_ENV);
4863
+ if (replayKey && replayKey.trim() !== "") {
4864
+ return replayKey;
4865
+ }
4861
4866
  if (this.resolvedApiKey !== void 0) {
4862
4867
  return this.resolvedApiKey;
4863
4868
  }
@@ -6302,7 +6307,7 @@ var Bitfab = class {
6302
6307
  await runWithSeedContext({ traceId }, async () => target(...args));
6303
6308
  } finally {
6304
6309
  try {
6305
- const { flushTraces: flushTraces2 } = await import("./http-LV7OUBKA.js");
6310
+ const { flushTraces: flushTraces2 } = await import("./http-DAQLSTUN.js");
6306
6311
  await flushTraces2(3e4);
6307
6312
  } finally {
6308
6313
  unrecorded = activeTraceStates.delete(traceId);
@@ -6361,7 +6366,7 @@ var Bitfab = class {
6361
6366
  `Function is wrapped with trace function key '${wrappedKey}' but replay was called with '${traceFunctionKey}'. Pass matching keys, or pass the unwrapped function to replay it under the explicit key.`
6362
6367
  );
6363
6368
  }
6364
- const { replay: doReplay } = await import("./replay-JNKXZPY7.js");
6369
+ const { replay: doReplay } = await import("./replay-FFNEMPGO.js");
6365
6370
  return doReplay(
6366
6371
  this.httpClient,
6367
6372
  this.serviceUrl,
@@ -6707,7 +6712,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
6707
6712
  sessionId: seedCase.sessionId
6708
6713
  })
6709
6714
  );
6710
- const { flushTraces: flushTraces2 } = await import("./http-LV7OUBKA.js");
6715
+ const { flushTraces: flushTraces2 } = await import("./http-DAQLSTUN.js");
6711
6716
  await flushTraces2(3e4);
6712
6717
  return { pipeline, traceFunctionKey, traceIds };
6713
6718
  }
@@ -6752,4 +6757,4 @@ export {
6752
6757
  reseedFromRegistry,
6753
6758
  seedFromRegistry
6754
6759
  };
6755
- //# sourceMappingURL=chunk-AXFKZYIY.js.map
6760
+ //# sourceMappingURL=chunk-G5BYSZLD.js.map