@dragonmastery/tamer 0.36.0 → 0.36.2

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/tamer.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  import { c as TAMER_OVERLAY_ENV_KEY, f as getDispatchNamespaces, n as materializeTamerResolvable, r as materializeVars, t as materializeCloudflareBindings } from "./normalize-DVSTRZhO.mjs";
3
3
  import { basename, dirname, relative, resolve } from "path";
4
4
  import { existsSync, readFileSync } from "fs";
@@ -5579,6 +5579,50 @@ async function deleteEnvSecretRows(api, env) {
5579
5579
  await api.d1Query(uuid$1, `DELETE FROM secret_history WHERE name LIKE ?`, [`${env}:%`]);
5580
5580
  return true;
5581
5581
  }
5582
+ /**
5583
+ * Copy all secret rows from one env namespace to another in the shared D1.
5584
+ * Copies ciphertext + hashes directly — no decryption/re-encryption. Requires
5585
+ * both envs to use the same master key (or the target to accept the source's
5586
+ * wrapped DEK, which it does since each row carries its own `wrapped_dek`).
5587
+ *
5588
+ * Used by `tamer secrets copy --from dev --to pr-42` for ephemeral PR envs.
5589
+ * Returns the number of secrets copied.
5590
+ */
5591
+ async function copyEnvSecretRows(api, fromEnv, toEnv) {
5592
+ const uuid$1 = await findTamerSecretsDatabaseUuid(api);
5593
+ if (!uuid$1) throw new Error(`secrets copy: vault not found. Run "tamer bootstrap" first.`);
5594
+ const { rows } = await api.d1Query(uuid$1, `SELECT name, ciphertext, iv, wrapped_dek, dek_iv, value_hash, updated_at, updated_by
5595
+ FROM secrets WHERE name LIKE ?`, [`${fromEnv}:%`]);
5596
+ if (rows.length === 0) return 0;
5597
+ const fromPrefix = `${fromEnv}:`;
5598
+ const toPrefix = `${toEnv}:`;
5599
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5600
+ for (const row of rows) {
5601
+ const sourceName = String(row.name);
5602
+ const targetName = `${toPrefix}${sourceName.startsWith(fromPrefix) ? sourceName.slice(fromPrefix.length) : sourceName}`;
5603
+ await api.d1Query(uuid$1, `INSERT INTO secrets (
5604
+ name, ciphertext, iv, wrapped_dek, dek_iv, value_hash, updated_at, updated_by
5605
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
5606
+ ON CONFLICT(name) DO UPDATE SET
5607
+ ciphertext = excluded.ciphertext,
5608
+ iv = excluded.iv,
5609
+ wrapped_dek = excluded.wrapped_dek,
5610
+ dek_iv = excluded.dek_iv,
5611
+ value_hash = excluded.value_hash,
5612
+ updated_at = excluded.updated_at,
5613
+ updated_by = excluded.updated_by`, [
5614
+ targetName,
5615
+ row.ciphertext,
5616
+ row.iv,
5617
+ row.wrapped_dek,
5618
+ row.dek_iv,
5619
+ row.value_hash,
5620
+ now,
5621
+ `copy:${fromEnv}`
5622
+ ]);
5623
+ }
5624
+ return rows.length;
5625
+ }
5582
5626
 
5583
5627
  //#endregion
5584
5628
  //#region src/core/secrets/masterKey.ts
@@ -8363,6 +8407,29 @@ async function runSecretsPush(options) {
8363
8407
  console.log(`secrets push (${ctx.env}): ${pushed} pushed, ${skipped} already current`);
8364
8408
  }
8365
8409
 
8410
+ //#endregion
8411
+ //#region src/cli/commands/secrets/copy.ts
8412
+ /**
8413
+ * Copy all secrets from one env's vault namespace to another. Copies
8414
+ * ciphertext directly — no decryption/re-encryption. Each row carries its
8415
+ * own wrapped DEK, so the target env doesn't even need the same master key.
8416
+ *
8417
+ * Usage: `tamer secrets copy --from dev --to pr-42`
8418
+ */
8419
+ async function runSecretsCopy(options) {
8420
+ const { from, to } = options;
8421
+ if (!from || !to) throw new Error("secrets copy: --from and --to are required (e.g. --from dev --to pr-42)");
8422
+ if (from === to) throw new Error("secrets copy: --from and --to must be different");
8423
+ const accountId = cloudflareAccountIdFromEnv();
8424
+ if (!accountId) throw new Error("CLOUDFLARE_ACCOUNT_ID required (env var or config account_id)");
8425
+ const count = await copyEnvSecretRows(new CFApiClient(accountId), from, to);
8426
+ if (count === 0) {
8427
+ console.log(`secrets copy: no secrets found in vault for env "${from}".`);
8428
+ return;
8429
+ }
8430
+ console.log(`secrets copy: copied ${count} secret(s) from "${from}" to "${to}".`);
8431
+ }
8432
+
8366
8433
  //#endregion
8367
8434
  //#region src/cli/commands/secrets/index.ts
8368
8435
  const SECRETS_USAGE = `usage:
@@ -8370,6 +8437,7 @@ const SECRETS_USAGE = `usage:
8370
8437
  tamer secrets set <NAME> --env <env> [--config <path>] # value on stdin (pipe only)
8371
8438
  tamer secrets load --env <env> [--worker <name>] [--file <path>] [--config <path>]
8372
8439
  # each worker: {workerDir}/.dev.vars.{env}; all declared workers when --worker omitted
8440
+ tamer secrets copy --from <env> --to <env> # copy vault between envs (e.g. dev → pr-42)
8373
8441
  tamer secrets get <NAME> --env <env> [--config <path>] # confirmation + audit log
8374
8442
  tamer secrets list --env <env> [--config <path>]
8375
8443
  tamer secrets rm <NAME> --env <env> [--config <path>]
@@ -8396,7 +8464,9 @@ function parseSecretsArgs(argv) {
8396
8464
  configPath: opts.config,
8397
8465
  file: opts.file,
8398
8466
  worker: opts.worker,
8399
- yes: opts.yes === true
8467
+ yes: opts.yes === true,
8468
+ from: opts.from,
8469
+ to: opts.to
8400
8470
  };
8401
8471
  }
8402
8472
  async function runSecrets(argv) {
@@ -8456,6 +8526,13 @@ async function runSecrets(argv) {
8456
8526
  worker: parsed.worker
8457
8527
  });
8458
8528
  return 0;
8529
+ case "copy":
8530
+ await runSecretsCopy({
8531
+ from: parsed.from,
8532
+ to: parsed.to,
8533
+ configPath: parsed.configPath
8534
+ });
8535
+ return 0;
8459
8536
  default:
8460
8537
  console.error(SECRETS_USAGE);
8461
8538
  return 1;