@i4ctime/q-ring 0.17.5 → 0.18.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/README.md +52 -10
- package/dist/{chunk-VMDD5QTJ.js → chunk-NTCTVLSK.js} +126 -38
- package/dist/chunk-NTCTVLSK.js.map +1 -0
- package/dist/{chunk-5ZCQSBVS.js → chunk-ZBPMEJRO.js} +142 -38
- package/dist/chunk-ZBPMEJRO.js.map +1 -0
- package/dist/dashboard-LICF6OZ5.js +699 -0
- package/dist/dashboard-LICF6OZ5.js.map +1 -0
- package/dist/dashboard-OEHI6OOD.js +699 -0
- package/dist/dashboard-OEHI6OOD.js.map +1 -0
- package/dist/index.js +833 -82
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +681 -172
- package/dist/mcp.js.map +1 -1
- package/package.json +10 -10
- package/dist/chunk-5ZCQSBVS.js.map +0 -1
- package/dist/chunk-VMDD5QTJ.js.map +0 -1
- package/dist/dashboard-K7LYTWSL.js +0 -685
- package/dist/dashboard-K7LYTWSL.js.map +0 -1
- package/dist/dashboard-ZN3FPR73.js +0 -685
- package/dist/dashboard-ZN3FPR73.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
Entry,
|
|
4
|
+
MAX_ROTATE_EVERY_DAYS,
|
|
3
5
|
PACKAGE_VERSION,
|
|
4
6
|
addCanaryAlert,
|
|
5
7
|
checkDecay,
|
|
@@ -8,9 +10,12 @@ import {
|
|
|
8
10
|
checkWrapToolPolicy,
|
|
9
11
|
clearMemory,
|
|
10
12
|
collapseEnvironment,
|
|
13
|
+
compareRotationUrgency,
|
|
14
|
+
configDir,
|
|
11
15
|
countLegacyApprovals,
|
|
12
16
|
deleteSecret,
|
|
13
17
|
describeAlertUrl,
|
|
18
|
+
describeRotation,
|
|
14
19
|
detectAnomalies,
|
|
15
20
|
disableHook,
|
|
16
21
|
disarmCanary,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
hasSecret,
|
|
32
37
|
hashProjectPath,
|
|
33
38
|
httpRequest,
|
|
39
|
+
isValidRotateEveryDays,
|
|
34
40
|
listAgentSessions,
|
|
35
41
|
listApprovals,
|
|
36
42
|
listCanaryAlerts,
|
|
@@ -47,6 +53,7 @@ import {
|
|
|
47
53
|
removeCanaryAlert,
|
|
48
54
|
removeHook,
|
|
49
55
|
revokeApproval,
|
|
56
|
+
rotationStatus,
|
|
50
57
|
sendCanaryAlerts,
|
|
51
58
|
serviceForScope,
|
|
52
59
|
setAuditAgentLabel,
|
|
@@ -60,7 +67,7 @@ import {
|
|
|
60
67
|
verifyAuditChain,
|
|
61
68
|
wrapRedactsResults,
|
|
62
69
|
wrapToolRequiresApproval
|
|
63
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-ZBPMEJRO.js";
|
|
64
71
|
|
|
65
72
|
// src/cli/commands.ts
|
|
66
73
|
import { Command, Help } from "commander";
|
|
@@ -339,6 +346,12 @@ function filterSecretsByKeyGlob(entries, filter) {
|
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
// src/cli/commands/secrets.ts
|
|
349
|
+
function rotationBadge(state) {
|
|
350
|
+
if (state === "overdue") return c.red(`${SYMBOLS.cross} overdue`);
|
|
351
|
+
if (state === "due-soon") return c.yellow(`${SYMBOLS.warning} due soon`);
|
|
352
|
+
if (state === "ok") return c.green(`${SYMBOLS.check} ok`);
|
|
353
|
+
return c.dim("unscheduled");
|
|
354
|
+
}
|
|
342
355
|
function registerSecretsCommands(program2) {
|
|
343
356
|
program2.command("set <key> [value]").description("Store a secret (with optional quantum metadata)").option("-g, --global", "Store in global scope").option("-p, --project", "Store in project scope (uses cwd)").option("--team <id>", "Store in team scope").option("--org <id>", "Store in org scope").option("--project-path <path>", "Explicit project path").option(
|
|
344
357
|
"-e, --env <env>",
|
|
@@ -351,6 +364,9 @@ function registerSecretsCommands(program2) {
|
|
|
351
364
|
"--rotation-format <format>",
|
|
352
365
|
"Format for auto-rotation (api-key, password, uuid, hex, base64, alphanumeric, token)"
|
|
353
366
|
).option("--rotation-prefix <prefix>", "Prefix for auto-rotation (e.g. sk-)").option(
|
|
367
|
+
"--rotate-every <days>",
|
|
368
|
+
`Remind to rotate every N days (1-${MAX_ROTATE_EVERY_DAYS}); see \`qring rotate:due\``
|
|
369
|
+
).option(
|
|
354
370
|
"--requires-approval",
|
|
355
371
|
"Require explicit user approval for MCP agents to read"
|
|
356
372
|
).option(
|
|
@@ -367,6 +383,19 @@ function registerSecretsCommands(program2) {
|
|
|
367
383
|
process.exit(1);
|
|
368
384
|
}
|
|
369
385
|
}
|
|
386
|
+
let rotateEveryDays;
|
|
387
|
+
if (cmd.rotateEvery !== void 0) {
|
|
388
|
+
const parsed = /^\d+$/.test(String(cmd.rotateEvery).trim()) ? Number(cmd.rotateEvery) : Number.NaN;
|
|
389
|
+
if (!isValidRotateEveryDays(parsed)) {
|
|
390
|
+
console.error(
|
|
391
|
+
c.red(
|
|
392
|
+
`${SYMBOLS.cross} --rotate-every must be a whole number of days between 1 and ${MAX_ROTATE_EVERY_DAYS} (got "${cmd.rotateEvery}")`
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
rotateEveryDays = parsed;
|
|
398
|
+
}
|
|
370
399
|
const setOpts = {
|
|
371
400
|
...opts,
|
|
372
401
|
ttlSeconds: cmd.ttl,
|
|
@@ -375,6 +404,7 @@ function registerSecretsCommands(program2) {
|
|
|
375
404
|
tags: cmd.tags?.split(",").map((t) => t.trim()),
|
|
376
405
|
rotationFormat: cmd.rotationFormat,
|
|
377
406
|
rotationPrefix: cmd.rotationPrefix,
|
|
407
|
+
rotateEveryDays,
|
|
378
408
|
requiresApproval: cmd.requiresApproval,
|
|
379
409
|
jitProvider: cmd.jitProvider
|
|
380
410
|
};
|
|
@@ -403,6 +433,7 @@ function registerSecretsCommands(program2) {
|
|
|
403
433
|
setSecret(key, value, setOpts);
|
|
404
434
|
const extras = [];
|
|
405
435
|
if (cmd.ttl) extras.push(`${SYMBOLS.clock} ttl=${cmd.ttl}s`);
|
|
436
|
+
if (rotateEveryDays) extras.push(c.dim(`rotate every ${rotateEveryDays}d`));
|
|
406
437
|
if (cmd.description) extras.push(c.dim(cmd.description));
|
|
407
438
|
console.log(
|
|
408
439
|
`${SYMBOLS.check} ${c.green("saved")} ${c.bold(key)} ${c.dim(`[${scopeColor(opts.scope ?? "global")}]`)} ${extras.join(" ")}`
|
|
@@ -541,6 +572,7 @@ function registerSecretsCommands(program2) {
|
|
|
541
572
|
}
|
|
542
573
|
const { envelope, scope } = result;
|
|
543
574
|
const decay = checkDecay(envelope);
|
|
575
|
+
const rotation = rotationStatus(envelope.meta);
|
|
544
576
|
if (emitJson(program2, cmd, {
|
|
545
577
|
key: safeStr(key),
|
|
546
578
|
scope: safeStr(scope),
|
|
@@ -557,7 +589,8 @@ function registerSecretsCommands(program2) {
|
|
|
557
589
|
service: safeStr(l.service),
|
|
558
590
|
key: safeStr(l.key)
|
|
559
591
|
})),
|
|
560
|
-
decay
|
|
592
|
+
decay,
|
|
593
|
+
rotation
|
|
561
594
|
})) {
|
|
562
595
|
return;
|
|
563
596
|
}
|
|
@@ -611,6 +644,9 @@ function registerSecretsCommands(program2) {
|
|
|
611
644
|
` ${c.dim("decay:")} ${decayIndicator(decayPct, expired)} ${decayTime}`
|
|
612
645
|
);
|
|
613
646
|
}
|
|
647
|
+
const rotAge = `${rotation.ageDays}d ago`;
|
|
648
|
+
const rotLine = rotation.state === "unscheduled" ? c.dim(`last rotated ${rotAge} \xB7 no reminder set`) : `${rotationBadge(rotation.state)} ${describeRotation(rotation)} ${c.dim(`(last rotated ${rotAge}, every ${rotation.rotateEveryDays}d)`)}`;
|
|
649
|
+
console.log(` ${c.dim("rotation:")} ${rotLine}`);
|
|
614
650
|
if (entangled.length > 0) {
|
|
615
651
|
console.log(` ${c.dim("entangled:")}`);
|
|
616
652
|
for (const link of entangled) {
|
|
@@ -1450,11 +1486,181 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
|
|
|
1450
1486
|
});
|
|
1451
1487
|
}
|
|
1452
1488
|
|
|
1489
|
+
// src/core/promote.ts
|
|
1490
|
+
var PromoteConflictError = class extends Error {
|
|
1491
|
+
code = "ERR_PROMOTE_CONFLICT";
|
|
1492
|
+
constructor(key, to) {
|
|
1493
|
+
super(
|
|
1494
|
+
`"${key}" already has a different value for env "${to}" \u2014 pass force to overwrite it`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
function assertEnvName(env, label) {
|
|
1499
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(env)) {
|
|
1500
|
+
throw new Error(`${label} environment name "${env}" is invalid (letters, digits, _ . - only)`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function promoteSecret(key, opts) {
|
|
1504
|
+
assertEnvName(opts.from, "source");
|
|
1505
|
+
assertEnvName(opts.to, "target");
|
|
1506
|
+
if (opts.from === opts.to) {
|
|
1507
|
+
throw new Error(`source and target environment are both "${opts.from}"`);
|
|
1508
|
+
}
|
|
1509
|
+
const found = getEnvelope(key, opts);
|
|
1510
|
+
if (!found) throw new Error(`Secret "${key}" not found`);
|
|
1511
|
+
const { envelope, scope } = found;
|
|
1512
|
+
const states = envelope.states;
|
|
1513
|
+
if (!states) {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`"${key}" has a single value, not per-environment states \u2014 set one with: qring set ${key} --env ${opts.from}`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
const source = states[opts.from];
|
|
1519
|
+
if (source === void 0) {
|
|
1520
|
+
const available = Object.keys(states).join(", ") || "none";
|
|
1521
|
+
throw new Error(`"${key}" has no value for env "${opts.from}" (available: ${available})`);
|
|
1522
|
+
}
|
|
1523
|
+
const current = states[opts.to];
|
|
1524
|
+
const previous = current === void 0 ? "absent" : current === source ? "same" : "different";
|
|
1525
|
+
if (previous === "same") {
|
|
1526
|
+
return { key, scope, from: opts.from, to: opts.to, previous, changed: false };
|
|
1527
|
+
}
|
|
1528
|
+
if (previous === "different" && !opts.force) {
|
|
1529
|
+
throw new PromoteConflictError(key, opts.to);
|
|
1530
|
+
}
|
|
1531
|
+
const nextStates = { ...states, [opts.to]: source };
|
|
1532
|
+
setSecret(key, "", {
|
|
1533
|
+
...opts,
|
|
1534
|
+
scope,
|
|
1535
|
+
states: nextStates,
|
|
1536
|
+
defaultEnv: envelope.defaultEnv
|
|
1537
|
+
});
|
|
1538
|
+
return { key, scope, from: opts.from, to: opts.to, previous, changed: true };
|
|
1539
|
+
}
|
|
1540
|
+
function diffEnvironments(opts) {
|
|
1541
|
+
assertEnvName(opts.a, "first");
|
|
1542
|
+
assertEnvName(opts.b, "second");
|
|
1543
|
+
const wanted = opts.keys ? new Set(opts.keys) : null;
|
|
1544
|
+
const entries = [];
|
|
1545
|
+
for (const entry of listSecrets(opts)) {
|
|
1546
|
+
if (wanted && !wanted.has(entry.key)) continue;
|
|
1547
|
+
const states = entry.envelope?.states;
|
|
1548
|
+
let status;
|
|
1549
|
+
if (!states) {
|
|
1550
|
+
status = "collapsed";
|
|
1551
|
+
} else {
|
|
1552
|
+
const va = states[opts.a];
|
|
1553
|
+
const vb = states[opts.b];
|
|
1554
|
+
if (va === void 0 && vb === void 0) continue;
|
|
1555
|
+
if (va === void 0) status = "only-b";
|
|
1556
|
+
else if (vb === void 0) status = "only-a";
|
|
1557
|
+
else status = va === vb ? "same" : "different";
|
|
1558
|
+
}
|
|
1559
|
+
entries.push({ key: entry.key, scope: entry.scope, status });
|
|
1560
|
+
}
|
|
1561
|
+
const summary = {
|
|
1562
|
+
same: 0,
|
|
1563
|
+
different: 0,
|
|
1564
|
+
"only-a": 0,
|
|
1565
|
+
"only-b": 0,
|
|
1566
|
+
collapsed: 0
|
|
1567
|
+
};
|
|
1568
|
+
for (const e of entries) summary[e.status] += 1;
|
|
1569
|
+
const drift = summary.different + summary["only-a"] + summary["only-b"] > 0;
|
|
1570
|
+
return { a: opts.a, b: opts.b, entries, summary, drift };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/cli/commands/environments.ts
|
|
1574
|
+
var STATUS_LABEL = {
|
|
1575
|
+
same: c.green("same"),
|
|
1576
|
+
different: c.yellow("different"),
|
|
1577
|
+
"only-a": c.red("missing in B"),
|
|
1578
|
+
"only-b": c.red("missing in A"),
|
|
1579
|
+
collapsed: c.dim("single value")
|
|
1580
|
+
};
|
|
1581
|
+
function registerEnvironmentCommands(program2) {
|
|
1582
|
+
program2.command("promote <key>").description("Copy a secret's value from one environment to another (superposition)").requiredOption("--from <env>", "Source environment").requiredOption("--to <env>", "Target environment").option("-g, --global", "Global scope").option("-p, --project", "Project scope (uses cwd)").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Explicit project path").option("-f, --force", "Overwrite a differing target value without asking").option("-y, --yes", "Alias for --force").action(async (key, cmd) => {
|
|
1583
|
+
const opts = buildOpts(cmd);
|
|
1584
|
+
const json = wantsJsonOutput(program2, cmd);
|
|
1585
|
+
const run = (force) => promoteSecret(key, { ...opts, from: cmd.from, to: cmd.to, force });
|
|
1586
|
+
try {
|
|
1587
|
+
let result;
|
|
1588
|
+
try {
|
|
1589
|
+
result = run(Boolean(cmd.force || cmd.yes));
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
if (!(err instanceof PromoteConflictError) || json) throw err;
|
|
1592
|
+
const ok = await confirm(
|
|
1593
|
+
`${SYMBOLS.warning} ${c.bold(key)} already has a different value for ${envBadge(cmd.to)}. Overwrite it with the ${envBadge(cmd.from)} value?`
|
|
1594
|
+
);
|
|
1595
|
+
if (!ok) {
|
|
1596
|
+
console.error(c.dim("Cancelled."));
|
|
1597
|
+
process.exitCode = 1;
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
result = run(true);
|
|
1601
|
+
}
|
|
1602
|
+
if (emitJson(program2, cmd, result)) return;
|
|
1603
|
+
const verb = result.changed ? c.green("promoted") : c.dim("already in sync");
|
|
1604
|
+
console.log(
|
|
1605
|
+
`${SYMBOLS.check} ${verb} ${c.bold(key)} ${envBadge(result.from)} ${c.dim("\u2192")} ${envBadge(result.to)} ${c.dim(`[${scopeColor(result.scope)}]`)}`
|
|
1606
|
+
);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1609
|
+
if (json) console.log(JSON.stringify({ ok: false, error: message }));
|
|
1610
|
+
else console.error(c.red(`${SYMBOLS.cross} ${message}`));
|
|
1611
|
+
process.exitCode = 1;
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
program2.command("diff <envA> <envB>").description("Compare two environments key by key \u2014 statuses only, never values (exit 1 on drift)").option("-k, --keys <keys>", "Comma-separated keys to compare (default: all visible)").option("-g, --global", "Global scope").option("-p, --project", "Project scope (uses cwd)").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Explicit project path").action((envA, envB, cmd) => {
|
|
1615
|
+
const opts = buildOpts(cmd);
|
|
1616
|
+
const keys = cmd.keys ? String(cmd.keys).split(",").map((k) => k.trim()).filter(Boolean) : void 0;
|
|
1617
|
+
try {
|
|
1618
|
+
const result = diffEnvironments({ ...opts, a: envA, b: envB, keys });
|
|
1619
|
+
if (result.drift) process.exitCode = 1;
|
|
1620
|
+
if (emitJson(program2, cmd, result)) return;
|
|
1621
|
+
if (result.entries.length === 0) {
|
|
1622
|
+
console.log(c.dim(`No secrets carry ${envA} or ${envB} states.`));
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
console.log(`${envBadge(envA)} ${c.dim("A")} ${envBadge(envB)} ${c.dim("B")}`);
|
|
1626
|
+
const width = Math.max(...result.entries.map((e) => e.key.length));
|
|
1627
|
+
for (const e of result.entries) {
|
|
1628
|
+
console.log(
|
|
1629
|
+
` ${c.bold(e.key.padEnd(width))} ${STATUS_LABEL[e.status] ?? e.status} ${c.dim(`[${scopeColor(e.scope)}]`)}`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
const s = result.summary;
|
|
1633
|
+
console.log(
|
|
1634
|
+
c.dim(
|
|
1635
|
+
`
|
|
1636
|
+
${s.same} same \xB7 ${s.different} different \xB7 ${s["only-a"]} only in A \xB7 ${s["only-b"]} only in B \xB7 ${s.collapsed} single-value`
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1639
|
+
if (result.drift) {
|
|
1640
|
+
console.log(c.yellow(`${SYMBOLS.warning} drift detected \u2014 promote with: qring promote KEY --from ${envA} --to ${envB}`));
|
|
1641
|
+
} else {
|
|
1642
|
+
console.log(c.green(`${SYMBOLS.check} environments aligned`));
|
|
1643
|
+
}
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1646
|
+
if (wantsJsonOutput(program2, cmd)) console.log(JSON.stringify({ ok: false, error: message }));
|
|
1647
|
+
else console.error(c.red(`${SYMBOLS.cross} ${message}`));
|
|
1648
|
+
process.exitCode = 1;
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1453
1653
|
// src/core/teleport.ts
|
|
1454
1654
|
import {
|
|
1455
1655
|
randomBytes as randomBytes2,
|
|
1456
1656
|
createCipheriv,
|
|
1457
1657
|
createDecipheriv,
|
|
1658
|
+
createHash,
|
|
1659
|
+
createPrivateKey,
|
|
1660
|
+
createPublicKey,
|
|
1661
|
+
diffieHellman,
|
|
1662
|
+
generateKeyPairSync,
|
|
1663
|
+
hkdfSync,
|
|
1458
1664
|
pbkdf2Sync
|
|
1459
1665
|
} from "crypto";
|
|
1460
1666
|
import { z } from "zod";
|
|
@@ -1488,6 +1694,34 @@ var TeleportPayloadSchema = z.object({
|
|
|
1488
1694
|
function deriveKey(passphrase, salt, iterations = PBKDF2_ITERATIONS) {
|
|
1489
1695
|
return pbkdf2Sync(passphrase, salt, iterations, KEY_LENGTH, "sha512");
|
|
1490
1696
|
}
|
|
1697
|
+
function decodeBundle(encoded) {
|
|
1698
|
+
let bundleJson;
|
|
1699
|
+
try {
|
|
1700
|
+
bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
1701
|
+
} catch {
|
|
1702
|
+
throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
return JSON.parse(bundleJson);
|
|
1706
|
+
} catch {
|
|
1707
|
+
throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function parsePayload(decrypted) {
|
|
1711
|
+
let rawPayload;
|
|
1712
|
+
try {
|
|
1713
|
+
rawPayload = JSON.parse(decrypted.toString("utf8"));
|
|
1714
|
+
} catch {
|
|
1715
|
+
throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
|
|
1716
|
+
}
|
|
1717
|
+
const payload = TeleportPayloadSchema.safeParse(rawPayload);
|
|
1718
|
+
if (!payload.success) {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
return payload.data;
|
|
1724
|
+
}
|
|
1491
1725
|
function teleportPack(secrets, passphrase) {
|
|
1492
1726
|
const payload = {
|
|
1493
1727
|
secrets,
|
|
@@ -1516,18 +1750,7 @@ function teleportPack(secrets, passphrase) {
|
|
|
1516
1750
|
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
1517
1751
|
}
|
|
1518
1752
|
function teleportUnpack(encoded, passphrase) {
|
|
1519
|
-
|
|
1520
|
-
try {
|
|
1521
|
-
bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
1522
|
-
} catch {
|
|
1523
|
-
throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
|
|
1524
|
-
}
|
|
1525
|
-
let rawBundle;
|
|
1526
|
-
try {
|
|
1527
|
-
rawBundle = JSON.parse(bundleJson);
|
|
1528
|
-
} catch {
|
|
1529
|
-
throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
|
|
1530
|
-
}
|
|
1753
|
+
const rawBundle = decodeBundle(encoded);
|
|
1531
1754
|
const parsedBundle = TeleportBundleSchema.safeParse(rawBundle);
|
|
1532
1755
|
if (!parsedBundle.success) {
|
|
1533
1756
|
throw new Error(
|
|
@@ -1551,19 +1774,301 @@ function teleportUnpack(encoded, passphrase) {
|
|
|
1551
1774
|
} catch {
|
|
1552
1775
|
throw new Error("ERR_TELEPORT_BAD_PASSPHRASE: decryption failed (wrong passphrase or corrupt data)");
|
|
1553
1776
|
}
|
|
1554
|
-
|
|
1777
|
+
return parsePayload(decrypted);
|
|
1778
|
+
}
|
|
1779
|
+
var RECIPIENT_PREFIX = "qring1";
|
|
1780
|
+
var RECIPIENT_RE = /^qring1([A-Za-z0-9_-]{43})$/;
|
|
1781
|
+
var X25519_RAW_LENGTH = 32;
|
|
1782
|
+
var V2_AAD = "qring-teleport-v2";
|
|
1783
|
+
var V2_WRAP_INFO = "qring-teleport-v2-wrap";
|
|
1784
|
+
var TELEPORT_KEYRING_SERVICE = "q-ring-teleport";
|
|
1785
|
+
var TELEPORT_KEYRING_ACCOUNT = "identity";
|
|
1786
|
+
var TeleportBundleV2Schema = z.object({
|
|
1787
|
+
v: z.literal(2),
|
|
1788
|
+
createdAt: z.string(),
|
|
1789
|
+
count: z.number(),
|
|
1790
|
+
ephemeral: z.string(),
|
|
1791
|
+
recipients: z.array(
|
|
1792
|
+
z.object({
|
|
1793
|
+
id: z.string(),
|
|
1794
|
+
wrap: z.string(),
|
|
1795
|
+
iv: z.string(),
|
|
1796
|
+
tag: z.string()
|
|
1797
|
+
})
|
|
1798
|
+
).min(1),
|
|
1799
|
+
iv: z.string(),
|
|
1800
|
+
tag: z.string(),
|
|
1801
|
+
data: z.string()
|
|
1802
|
+
});
|
|
1803
|
+
function rawPublicKey(key) {
|
|
1804
|
+
const jwk = key.export({ format: "jwk" });
|
|
1805
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "X25519" || typeof jwk.x !== "string") {
|
|
1806
|
+
throw new Error("ERR_TELEPORT_BAD_RECIPIENT: not an X25519 public key");
|
|
1807
|
+
}
|
|
1808
|
+
const raw = Buffer.from(jwk.x, "base64url");
|
|
1809
|
+
if (raw.length !== X25519_RAW_LENGTH) {
|
|
1810
|
+
throw new Error("ERR_TELEPORT_BAD_RECIPIENT: not an X25519 public key");
|
|
1811
|
+
}
|
|
1812
|
+
return raw;
|
|
1813
|
+
}
|
|
1814
|
+
function publicKeyFromRaw(raw) {
|
|
1815
|
+
return createPublicKey({
|
|
1816
|
+
key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") },
|
|
1817
|
+
format: "jwk"
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
function formatRecipient(publicKey) {
|
|
1821
|
+
const raw = Buffer.isBuffer(publicKey) ? publicKey : rawPublicKey(publicKey);
|
|
1822
|
+
if (raw.length !== X25519_RAW_LENGTH) {
|
|
1823
|
+
throw new Error("ERR_TELEPORT_BAD_RECIPIENT: public key must be 32 raw bytes");
|
|
1824
|
+
}
|
|
1825
|
+
return `${RECIPIENT_PREFIX}${raw.toString("base64url")}`;
|
|
1826
|
+
}
|
|
1827
|
+
function parseRecipient(str) {
|
|
1828
|
+
const trimmed = typeof str === "string" ? str.trim() : "";
|
|
1829
|
+
const match = RECIPIENT_RE.exec(trimmed);
|
|
1830
|
+
if (!match) {
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`ERR_TELEPORT_BAD_RECIPIENT: expected "${RECIPIENT_PREFIX}" followed by a base64url X25519 public key (run \`qring teleport identity\` on the recipient's machine to get one)`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
const raw = Buffer.from(match[1], "base64url");
|
|
1836
|
+
if (raw.length !== X25519_RAW_LENGTH) {
|
|
1837
|
+
throw new Error(
|
|
1838
|
+
"ERR_TELEPORT_BAD_RECIPIENT: recipient key does not decode to 32 bytes"
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
return raw;
|
|
1842
|
+
}
|
|
1843
|
+
function recipientId(rawPub) {
|
|
1844
|
+
return createHash("sha256").update(rawPub).digest("hex").slice(0, 8);
|
|
1845
|
+
}
|
|
1846
|
+
function deriveWrapKey(shared, ephemeralPub, recipientPub) {
|
|
1847
|
+
return Buffer.from(
|
|
1848
|
+
hkdfSync(
|
|
1849
|
+
"sha256",
|
|
1850
|
+
shared,
|
|
1851
|
+
Buffer.concat([ephemeralPub, recipientPub]),
|
|
1852
|
+
V2_WRAP_INFO,
|
|
1853
|
+
KEY_LENGTH
|
|
1854
|
+
)
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
function teleportPackFor(secrets, recipients) {
|
|
1858
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1859
|
+
for (const r of recipients) {
|
|
1860
|
+
const raw = parseRecipient(r);
|
|
1861
|
+
seen.set(recipientId(raw), raw);
|
|
1862
|
+
}
|
|
1863
|
+
if (seen.size === 0) {
|
|
1864
|
+
throw new Error("ERR_TELEPORT_NO_RECIPIENTS: at least one recipient is required");
|
|
1865
|
+
}
|
|
1866
|
+
const payload = {
|
|
1867
|
+
secrets,
|
|
1868
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1869
|
+
};
|
|
1870
|
+
const plaintext = JSON.stringify(payload);
|
|
1871
|
+
const cek = randomBytes2(KEY_LENGTH);
|
|
1872
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
1873
|
+
const cipher = createCipheriv(ALGORITHM, cek, iv);
|
|
1874
|
+
cipher.setAAD(Buffer.from(V2_AAD, "utf8"));
|
|
1875
|
+
const encrypted = Buffer.concat([
|
|
1876
|
+
cipher.update(plaintext, "utf8"),
|
|
1877
|
+
cipher.final()
|
|
1878
|
+
]);
|
|
1879
|
+
const tag = cipher.getAuthTag();
|
|
1880
|
+
const ephemeral = generateKeyPairSync("x25519");
|
|
1881
|
+
const ephemeralPub = rawPublicKey(ephemeral.publicKey);
|
|
1882
|
+
const wrapped = [];
|
|
1883
|
+
for (const [id, recipientPub] of seen) {
|
|
1884
|
+
const shared = diffieHellman({
|
|
1885
|
+
privateKey: ephemeral.privateKey,
|
|
1886
|
+
publicKey: publicKeyFromRaw(recipientPub)
|
|
1887
|
+
});
|
|
1888
|
+
const wrapKey = deriveWrapKey(shared, ephemeralPub, recipientPub);
|
|
1889
|
+
shared.fill(0);
|
|
1890
|
+
const wrapIv = randomBytes2(IV_LENGTH);
|
|
1891
|
+
const wrapCipher = createCipheriv(ALGORITHM, wrapKey, wrapIv);
|
|
1892
|
+
wrapCipher.setAAD(Buffer.from(id, "utf8"));
|
|
1893
|
+
const wrap2 = Buffer.concat([wrapCipher.update(cek), wrapCipher.final()]);
|
|
1894
|
+
const wrapTag = wrapCipher.getAuthTag();
|
|
1895
|
+
wrapKey.fill(0);
|
|
1896
|
+
wrapped.push({
|
|
1897
|
+
id,
|
|
1898
|
+
wrap: wrap2.toString("base64"),
|
|
1899
|
+
iv: wrapIv.toString("base64"),
|
|
1900
|
+
tag: wrapTag.toString("base64")
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
cek.fill(0);
|
|
1904
|
+
const bundle = {
|
|
1905
|
+
v: 2,
|
|
1906
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1907
|
+
count: secrets.length,
|
|
1908
|
+
ephemeral: ephemeralPub.toString("base64url"),
|
|
1909
|
+
recipients: wrapped,
|
|
1910
|
+
iv: iv.toString("base64"),
|
|
1911
|
+
tag: tag.toString("base64"),
|
|
1912
|
+
data: encrypted.toString("base64")
|
|
1913
|
+
};
|
|
1914
|
+
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
1915
|
+
}
|
|
1916
|
+
function toPrivateKey(identity) {
|
|
1917
|
+
if (typeof identity !== "string") return identity;
|
|
1555
1918
|
try {
|
|
1556
|
-
|
|
1919
|
+
return createPrivateKey(identity);
|
|
1557
1920
|
} catch {
|
|
1558
|
-
throw new Error("
|
|
1921
|
+
throw new Error("ERR_TELEPORT_BAD_IDENTITY: private key is not a valid PEM");
|
|
1559
1922
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1923
|
+
}
|
|
1924
|
+
function teleportUnpackWith(encoded, identity) {
|
|
1925
|
+
const privateKey = toPrivateKey(identity);
|
|
1926
|
+
const rawBundle = decodeBundle(encoded);
|
|
1927
|
+
const parsedBundle = TeleportBundleV2Schema.safeParse(rawBundle);
|
|
1928
|
+
if (!parsedBundle.success) {
|
|
1562
1929
|
throw new Error(
|
|
1563
|
-
`ERR_TELEPORT_CORRUPT: invalid
|
|
1930
|
+
`ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
|
|
1564
1931
|
);
|
|
1565
1932
|
}
|
|
1566
|
-
|
|
1933
|
+
const bundle = parsedBundle.data;
|
|
1934
|
+
const myPub = rawPublicKey(createPublicKey(privateKey));
|
|
1935
|
+
const myId = recipientId(myPub);
|
|
1936
|
+
const mine = bundle.recipients.filter((r) => r.id === myId);
|
|
1937
|
+
if (mine.length === 0) {
|
|
1938
|
+
const ids = bundle.recipients.map((r) => r.id).join(", ");
|
|
1939
|
+
throw new Error(
|
|
1940
|
+
`ERR_TELEPORT_NOT_A_RECIPIENT: bundle is addressed to [${ids}], not to ${myId}`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
const ephemeralPub = Buffer.from(bundle.ephemeral, "base64url");
|
|
1944
|
+
if (ephemeralPub.length !== X25519_RAW_LENGTH) {
|
|
1945
|
+
throw new Error("ERR_TELEPORT_CORRUPT: ephemeral key is not 32 bytes");
|
|
1946
|
+
}
|
|
1947
|
+
let ephemeralKey;
|
|
1948
|
+
try {
|
|
1949
|
+
ephemeralKey = publicKeyFromRaw(ephemeralPub);
|
|
1950
|
+
} catch {
|
|
1951
|
+
throw new Error("ERR_TELEPORT_CORRUPT: ephemeral key is not a valid X25519 point");
|
|
1952
|
+
}
|
|
1953
|
+
const shared = diffieHellman({ privateKey, publicKey: ephemeralKey });
|
|
1954
|
+
const wrapKey = deriveWrapKey(shared, ephemeralPub, myPub);
|
|
1955
|
+
shared.fill(0);
|
|
1956
|
+
let cek = null;
|
|
1957
|
+
for (const entry of mine) {
|
|
1958
|
+
try {
|
|
1959
|
+
const decipher = createDecipheriv(
|
|
1960
|
+
ALGORITHM,
|
|
1961
|
+
wrapKey,
|
|
1962
|
+
Buffer.from(entry.iv, "base64")
|
|
1963
|
+
);
|
|
1964
|
+
decipher.setAAD(Buffer.from(entry.id, "utf8"));
|
|
1965
|
+
decipher.setAuthTag(Buffer.from(entry.tag, "base64"));
|
|
1966
|
+
cek = Buffer.concat([
|
|
1967
|
+
decipher.update(Buffer.from(entry.wrap, "base64")),
|
|
1968
|
+
decipher.final()
|
|
1969
|
+
]);
|
|
1970
|
+
break;
|
|
1971
|
+
} catch {
|
|
1972
|
+
cek = null;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
wrapKey.fill(0);
|
|
1976
|
+
if (cek === null || cek.length !== KEY_LENGTH) {
|
|
1977
|
+
cek?.fill(0);
|
|
1978
|
+
throw new Error(
|
|
1979
|
+
"ERR_TELEPORT_CORRUPT: could not unwrap content key (tampered bundle or mismatched identity)"
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
let decrypted;
|
|
1983
|
+
try {
|
|
1984
|
+
const decipher = createDecipheriv(
|
|
1985
|
+
ALGORITHM,
|
|
1986
|
+
cek,
|
|
1987
|
+
Buffer.from(bundle.iv, "base64")
|
|
1988
|
+
);
|
|
1989
|
+
decipher.setAAD(Buffer.from(V2_AAD, "utf8"));
|
|
1990
|
+
decipher.setAuthTag(Buffer.from(bundle.tag, "base64"));
|
|
1991
|
+
decrypted = Buffer.concat([
|
|
1992
|
+
decipher.update(Buffer.from(bundle.data, "base64")),
|
|
1993
|
+
decipher.final()
|
|
1994
|
+
]);
|
|
1995
|
+
} catch {
|
|
1996
|
+
throw new Error("ERR_TELEPORT_CORRUPT: payload authentication failed (tampered bundle)");
|
|
1997
|
+
} finally {
|
|
1998
|
+
cek.fill(0);
|
|
1999
|
+
}
|
|
2000
|
+
return parsePayload(decrypted);
|
|
2001
|
+
}
|
|
2002
|
+
function inspectTeleportBundle(encoded) {
|
|
2003
|
+
const raw = decodeBundle(encoded);
|
|
2004
|
+
const v1 = TeleportBundleSchema.safeParse(raw);
|
|
2005
|
+
if (v1.success) return { v: 1, count: v1.data.count };
|
|
2006
|
+
const v2 = TeleportBundleV2Schema.safeParse(raw);
|
|
2007
|
+
if (v2.success) {
|
|
2008
|
+
return {
|
|
2009
|
+
v: 2,
|
|
2010
|
+
count: v2.data.count,
|
|
2011
|
+
recipients: v2.data.recipients.map((r) => r.id)
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
const version = raw !== null && typeof raw === "object" && "v" in raw ? String(raw.v) : "unknown";
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
`ERR_TELEPORT_CORRUPT: unsupported or malformed bundle (v=${version})`
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
function teleportUnpackAuto(encoded, creds) {
|
|
2020
|
+
const info = inspectTeleportBundle(encoded);
|
|
2021
|
+
if (info.v === 1) {
|
|
2022
|
+
if (creds.passphrase === void 0) {
|
|
2023
|
+
throw new Error(
|
|
2024
|
+
"ERR_TELEPORT_PASSPHRASE_REQUIRED: this is a passphrase (v1) bundle"
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return teleportUnpack(encoded, creds.passphrase);
|
|
2028
|
+
}
|
|
2029
|
+
if (creds.identity === void 0) {
|
|
2030
|
+
throw new Error(
|
|
2031
|
+
"ERR_TELEPORT_NO_IDENTITY: this bundle is addressed to recipient keys \u2014 run `qring teleport keygen` to create yours"
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
return teleportUnpackWith(encoded, creds.identity);
|
|
2035
|
+
}
|
|
2036
|
+
function identityFrom(privateKey) {
|
|
2037
|
+
const raw = rawPublicKey(createPublicKey(privateKey));
|
|
2038
|
+
return { privateKey, recipient: formatRecipient(raw), id: recipientId(raw) };
|
|
2039
|
+
}
|
|
2040
|
+
function generateTeleportIdentity(options = {}) {
|
|
2041
|
+
const entry = new Entry(TELEPORT_KEYRING_SERVICE, TELEPORT_KEYRING_ACCOUNT);
|
|
2042
|
+
if (!options.force && entry.getPassword()) {
|
|
2043
|
+
throw new Error(
|
|
2044
|
+
"ERR_TELEPORT_IDENTITY_EXISTS: a teleport identity already exists \u2014 pass --force to replace it (bundles sent to the old key become unreadable)"
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
const { privateKey } = generateKeyPairSync("x25519");
|
|
2048
|
+
const der = privateKey.export({ format: "der", type: "pkcs8" });
|
|
2049
|
+
entry.setPassword(der.toString("base64"));
|
|
2050
|
+
der.fill(0);
|
|
2051
|
+
return identityFrom(privateKey);
|
|
2052
|
+
}
|
|
2053
|
+
function loadTeleportIdentity() {
|
|
2054
|
+
const stored = new Entry(
|
|
2055
|
+
TELEPORT_KEYRING_SERVICE,
|
|
2056
|
+
TELEPORT_KEYRING_ACCOUNT
|
|
2057
|
+
).getPassword();
|
|
2058
|
+
if (!stored) return null;
|
|
2059
|
+
let privateKey;
|
|
2060
|
+
try {
|
|
2061
|
+
privateKey = createPrivateKey({
|
|
2062
|
+
key: Buffer.from(stored, "base64"),
|
|
2063
|
+
format: "der",
|
|
2064
|
+
type: "pkcs8"
|
|
2065
|
+
});
|
|
2066
|
+
} catch {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
"ERR_TELEPORT_BAD_IDENTITY: stored teleport identity is unreadable \u2014 run `qring teleport keygen --force`"
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
return identityFrom(privateKey);
|
|
1567
2072
|
}
|
|
1568
2073
|
|
|
1569
2074
|
// src/cli/commands/quantum.ts
|
|
@@ -1693,15 +2198,71 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
1693
2198
|
console.log();
|
|
1694
2199
|
});
|
|
1695
2200
|
const tp = program2.command("teleport").alias("tp").description("Encrypted secret sharing (quantum teleportation)");
|
|
1696
|
-
tp.command("
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
2201
|
+
tp.command("keygen").description(
|
|
2202
|
+
"Create your teleport identity (X25519 keypair, private key in the OS keyring)"
|
|
2203
|
+
).option("--force", "Replace an existing identity").action((cmd) => {
|
|
2204
|
+
let identity;
|
|
2205
|
+
try {
|
|
2206
|
+
identity = generateTeleportIdentity({ force: Boolean(cmd.force) });
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2209
|
+
console.error(c.red(`${SYMBOLS.cross} ${msg}`));
|
|
2210
|
+
process.exit(1);
|
|
2211
|
+
}
|
|
2212
|
+
console.log(identity.recipient);
|
|
2213
|
+
if (process.stdout.isTTY) {
|
|
2214
|
+
console.log(
|
|
2215
|
+
`${SYMBOLS.key} ${c.green("identity created")} ${c.dim(`(id ${identity.id})`)} \u2014 share the line above with anyone who should \`teleport pack --to\` you`
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
tp.command("identity").description("Print your public teleport recipient string").action(() => {
|
|
2220
|
+
let identity;
|
|
2221
|
+
try {
|
|
2222
|
+
identity = loadTeleportIdentity();
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2225
|
+
console.error(c.red(`${SYMBOLS.cross} ${msg}`));
|
|
2226
|
+
process.exit(1);
|
|
2227
|
+
}
|
|
2228
|
+
if (!identity) {
|
|
2229
|
+
console.error(
|
|
2230
|
+
c.red(
|
|
2231
|
+
`${SYMBOLS.cross} No teleport identity found \u2014 run ${c.bold("qring teleport keygen")} first`
|
|
2232
|
+
)
|
|
2233
|
+
);
|
|
1703
2234
|
process.exit(1);
|
|
1704
2235
|
}
|
|
2236
|
+
console.log(identity.recipient);
|
|
2237
|
+
if (process.stdout.isTTY) {
|
|
2238
|
+
console.log(c.dim(`recipient id: ${identity.id}`));
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
tp.command("pack").description("Pack secrets into an encrypted bundle").option("-k, --keys <keys>", "Comma-separated key names to pack").option(
|
|
2242
|
+
"--to <recipient...>",
|
|
2243
|
+
"Recipient(s) from `qring teleport identity` (repeatable or comma-separated); skips the passphrase"
|
|
2244
|
+
).option("-g, --global", "Pack global scope").option("-p, --project", "Pack project scope").option("--project-path <path>", "Explicit project path").action(async (cmd) => {
|
|
2245
|
+
const opts = buildOpts(cmd);
|
|
2246
|
+
const recipients = (cmd.to ?? []).flatMap((r) => r.split(",")).map((r) => r.trim()).filter((r) => r.length > 0);
|
|
2247
|
+
for (const r of recipients) {
|
|
2248
|
+
try {
|
|
2249
|
+
parseRecipient(r);
|
|
2250
|
+
} catch (err) {
|
|
2251
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2252
|
+
console.error(c.red(`${SYMBOLS.cross} Bad recipient "${r}": ${msg}`));
|
|
2253
|
+
process.exit(1);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
let passphrase;
|
|
2257
|
+
if (recipients.length === 0) {
|
|
2258
|
+
passphrase = await promptSecret(
|
|
2259
|
+
`${SYMBOLS.lock} Enter passphrase for encryption: `
|
|
2260
|
+
);
|
|
2261
|
+
if (!passphrase) {
|
|
2262
|
+
console.error(c.red("Passphrase required (or pass --to <recipient>)"));
|
|
2263
|
+
process.exit(1);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
1705
2266
|
const entries = listSecrets(opts);
|
|
1706
2267
|
let keys;
|
|
1707
2268
|
if (cmd.keys) {
|
|
@@ -1719,12 +2280,13 @@ ${c.dim(`format: ${cmd.format} | entropy: ~${entropy} bits`)}`
|
|
|
1719
2280
|
console.error(c.red("No secrets to pack"));
|
|
1720
2281
|
process.exit(1);
|
|
1721
2282
|
}
|
|
1722
|
-
const bundle = teleportPack(secrets, passphrase);
|
|
2283
|
+
const bundle = recipients.length > 0 ? teleportPackFor(secrets, recipients) : teleportPack(secrets, passphrase);
|
|
1723
2284
|
process.stdout.write(bundle);
|
|
1724
2285
|
if (process.stdout.isTTY) {
|
|
2286
|
+
const to = recipients.length > 0 ? c.dim(` for ${recipients.length} recipient(s)`) : "";
|
|
1725
2287
|
console.log(
|
|
1726
2288
|
`
|
|
1727
|
-
${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
|
|
2289
|
+
${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)${to}`
|
|
1728
2290
|
);
|
|
1729
2291
|
}
|
|
1730
2292
|
});
|
|
@@ -1736,20 +2298,73 @@ ${SYMBOLS.package} ${c.green("packed")} ${secrets.length} secret(s)`
|
|
|
1736
2298
|
}
|
|
1737
2299
|
bundle = Buffer.concat(chunks).toString("utf8").trim();
|
|
1738
2300
|
}
|
|
1739
|
-
|
|
1740
|
-
`${SYMBOLS.lock} Enter passphrase for decryption: `
|
|
1741
|
-
);
|
|
1742
|
-
let payload;
|
|
2301
|
+
let info;
|
|
1743
2302
|
try {
|
|
1744
|
-
|
|
2303
|
+
info = inspectTeleportBundle(bundle);
|
|
1745
2304
|
} catch {
|
|
1746
2305
|
console.error(
|
|
1747
|
-
c.red(
|
|
1748
|
-
`${SYMBOLS.cross} Failed to unpack: wrong passphrase or corrupted bundle`
|
|
1749
|
-
)
|
|
2306
|
+
c.red(`${SYMBOLS.cross} Failed to unpack: corrupted bundle`)
|
|
1750
2307
|
);
|
|
1751
2308
|
process.exit(1);
|
|
1752
2309
|
}
|
|
2310
|
+
let passphrase;
|
|
2311
|
+
let identity = null;
|
|
2312
|
+
if (info.v === 1) {
|
|
2313
|
+
passphrase = await promptSecret(
|
|
2314
|
+
`${SYMBOLS.lock} Enter passphrase for decryption: `
|
|
2315
|
+
);
|
|
2316
|
+
} else {
|
|
2317
|
+
try {
|
|
2318
|
+
identity = loadTeleportIdentity();
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2321
|
+
console.error(c.red(`${SYMBOLS.cross} ${msg}`));
|
|
2322
|
+
process.exit(1);
|
|
2323
|
+
}
|
|
2324
|
+
if (cmd.dryRun) {
|
|
2325
|
+
const mine = identity ? info.recipients.includes(identity.id) : false;
|
|
2326
|
+
console.log(
|
|
2327
|
+
`
|
|
2328
|
+
${SYMBOLS.package} ${c.bold("Recipient bundle")} addressed to ${info.recipients.length} recipient id(s):`
|
|
2329
|
+
);
|
|
2330
|
+
for (const id of info.recipients) {
|
|
2331
|
+
const marker = identity && id === identity.id ? c.green(" (you)") : "";
|
|
2332
|
+
console.log(` ${SYMBOLS.key} ${id}${marker}`);
|
|
2333
|
+
}
|
|
2334
|
+
if (!identity) {
|
|
2335
|
+
console.log(
|
|
2336
|
+
c.dim(
|
|
2337
|
+
` no local identity \u2014 run ${c.bold("qring teleport keygen")} to create one`
|
|
2338
|
+
)
|
|
2339
|
+
);
|
|
2340
|
+
} else if (!mine) {
|
|
2341
|
+
console.log(
|
|
2342
|
+
c.dim(` your id ${identity.id} is not among them; unpack will fail`)
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
console.log();
|
|
2346
|
+
}
|
|
2347
|
+
if (!identity) {
|
|
2348
|
+
console.error(
|
|
2349
|
+
c.red(
|
|
2350
|
+
`${SYMBOLS.cross} This bundle is addressed to recipient keys and you have no teleport identity \u2014 run ${c.bold("qring teleport keygen")} first`
|
|
2351
|
+
)
|
|
2352
|
+
);
|
|
2353
|
+
process.exit(1);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
let payload;
|
|
2357
|
+
try {
|
|
2358
|
+
payload = teleportUnpackAuto(bundle, {
|
|
2359
|
+
passphrase,
|
|
2360
|
+
identity: identity?.privateKey
|
|
2361
|
+
});
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2364
|
+
const reason = msg.startsWith("ERR_TELEPORT_NOT_A_RECIPIENT") ? `this bundle is not addressed to your identity (${identity?.id})` : info.v === 1 ? "wrong passphrase or corrupted bundle" : "corrupted bundle or mismatched identity";
|
|
2365
|
+
console.error(c.red(`${SYMBOLS.cross} Failed to unpack: ${reason}`));
|
|
2366
|
+
process.exit(1);
|
|
2367
|
+
}
|
|
1753
2368
|
if (cmd.dryRun) {
|
|
1754
2369
|
console.log(
|
|
1755
2370
|
`
|
|
@@ -1937,6 +2552,51 @@ function registerValidationCommands(program2) {
|
|
|
1937
2552
|
console.log(c.yellow(`${SYMBOLS.warning} ${result.message}`));
|
|
1938
2553
|
}
|
|
1939
2554
|
});
|
|
2555
|
+
program2.command("rotate:due").description(
|
|
2556
|
+
"List secrets whose rotation reminder is due soon or overdue (--all: every scheduled secret)"
|
|
2557
|
+
).option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--team <id>", "Team scope only").option("--org <id>", "Org scope only").option("--project-path <path>", "Explicit project path").option("--all", "Include scheduled secrets that are not yet due").option("--json", "Output as JSON").action((cmd) => {
|
|
2558
|
+
const opts = buildOpts(cmd);
|
|
2559
|
+
const entries = listSecrets(opts);
|
|
2560
|
+
const scheduled = entries.filter((e) => e.envelope).map((e) => ({
|
|
2561
|
+
key: safeStr(e.key),
|
|
2562
|
+
scope: safeStr(e.scope),
|
|
2563
|
+
rotation: rotationStatus(e.envelope.meta)
|
|
2564
|
+
})).filter((e) => e.rotation.state !== "unscheduled").sort((a, b) => compareRotationUrgency(a.rotation, b.rotation));
|
|
2565
|
+
const due = scheduled.filter(
|
|
2566
|
+
(e) => e.rotation.state === "due-soon" || e.rotation.state === "overdue"
|
|
2567
|
+
);
|
|
2568
|
+
const shown = cmd.all ? scheduled : due;
|
|
2569
|
+
if (emitJson(program2, cmd, {
|
|
2570
|
+
entries: shown,
|
|
2571
|
+
dueCount: due.length,
|
|
2572
|
+
scheduledCount: scheduled.length
|
|
2573
|
+
})) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (shown.length === 0) {
|
|
2577
|
+
console.log(
|
|
2578
|
+
c.dim(
|
|
2579
|
+
scheduled.length === 0 ? "No secrets have a rotation reminder (set one with: qring set KEY --rotate-every 90)" : `Nothing due \u2014 ${scheduled.length} scheduled secret(s) are on track`
|
|
2580
|
+
)
|
|
2581
|
+
);
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
console.log(
|
|
2585
|
+
c.bold(
|
|
2586
|
+
`
|
|
2587
|
+
${SYMBOLS.shield} Rotation reminders (${due.length} due of ${scheduled.length} scheduled)
|
|
2588
|
+
`
|
|
2589
|
+
)
|
|
2590
|
+
);
|
|
2591
|
+
const maxKeyLen = Math.max(...shown.map((e) => e.key.length));
|
|
2592
|
+
for (const e of shown) {
|
|
2593
|
+
const r = e.rotation;
|
|
2594
|
+
console.log(
|
|
2595
|
+
` ${c.dim("[")}${scopeColor(e.scope)}${c.dim("]")} ${c.bold(e.key.padEnd(maxKeyLen))} ${rotationBadge(r.state)} ${describeRotation(r)} ${c.dim(`(last rotated ${r.ageDays}d ago, every ${r.rotateEveryDays}d)`)}`
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
console.log();
|
|
2599
|
+
});
|
|
1940
2600
|
program2.command("ci:validate").description(
|
|
1941
2601
|
"CI-oriented batch validation of all secrets (exit code 1 on failure)"
|
|
1942
2602
|
).option("-g, --global", "Global scope").option("-p, --project", "Project scope").option("--project-path <path>", "Explicit project path").option("--json", "Output as JSON").action(async (cmd) => {
|
|
@@ -2611,7 +3271,7 @@ function registerToolingCommands(program2) {
|
|
|
2611
3271
|
}
|
|
2612
3272
|
});
|
|
2613
3273
|
program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
|
|
2614
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
3274
|
+
const { startDashboardServer } = await import("./dashboard-LICF6OZ5.js");
|
|
2615
3275
|
const { exec } = await import("child_process");
|
|
2616
3276
|
const { platform } = await import("os");
|
|
2617
3277
|
const port = Number(cmd.port);
|
|
@@ -3865,7 +4525,29 @@ function setupEditor(opts) {
|
|
|
3865
4525
|
|
|
3866
4526
|
// src/core/push.ts
|
|
3867
4527
|
import { spawnSync } from "child_process";
|
|
3868
|
-
|
|
4528
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync6 } from "fs";
|
|
4529
|
+
import { tmpdir } from "os";
|
|
4530
|
+
import { join as join5 } from "path";
|
|
4531
|
+
var PUSH_TARGETS = [
|
|
4532
|
+
"github",
|
|
4533
|
+
"vercel",
|
|
4534
|
+
"cloudflare",
|
|
4535
|
+
"fly",
|
|
4536
|
+
"railway",
|
|
4537
|
+
"netlify"
|
|
4538
|
+
];
|
|
4539
|
+
function flyImportLine(key, value) {
|
|
4540
|
+
return `${key}="""${value}"""
|
|
4541
|
+
`;
|
|
4542
|
+
}
|
|
4543
|
+
function dotenvLine(key, value) {
|
|
4544
|
+
if (!/['\r\n]/.test(value)) return `${key}='${value}'
|
|
4545
|
+
`;
|
|
4546
|
+
if (!/["\r\n]/.test(value) && !/\\[nr]/.test(value)) return `${key}="${value}"
|
|
4547
|
+
`;
|
|
4548
|
+
return `${key}=${value}
|
|
4549
|
+
`;
|
|
4550
|
+
}
|
|
3869
4551
|
var TARGETS = {
|
|
3870
4552
|
github: {
|
|
3871
4553
|
binary: "gh",
|
|
@@ -3882,12 +4564,62 @@ var TARGETS = {
|
|
|
3882
4564
|
binary: "wrangler",
|
|
3883
4565
|
args: (key) => [["secret", "put", key]],
|
|
3884
4566
|
installHint: "install Wrangler: npm i -g wrangler (then `wrangler login`)"
|
|
4567
|
+
},
|
|
4568
|
+
fly: {
|
|
4569
|
+
binary: "flyctl",
|
|
4570
|
+
// `flyctl secrets set KEY=VALUE` is argv-only; `secrets import` reads
|
|
4571
|
+
// NAME=VALUE lines from stdin instead, so the key travels there too.
|
|
4572
|
+
args: (_key, opts) => [["secrets", "import", ...opts.app ? ["--app", opts.app] : []]],
|
|
4573
|
+
stdin: flyImportLine,
|
|
4574
|
+
installHint: "install flyctl: https://fly.io/docs/flyctl/install/ (then `flyctl auth login`)"
|
|
4575
|
+
},
|
|
4576
|
+
railway: {
|
|
4577
|
+
binary: "railway",
|
|
4578
|
+
// Railway CLI >= 4 reads the value from stdin with `variable set KEY --stdin`
|
|
4579
|
+
// (trailing newline trimmed); `--set KEY=VALUE` is the argv-only legacy form.
|
|
4580
|
+
args: (key, opts) => [
|
|
4581
|
+
[
|
|
4582
|
+
"variable",
|
|
4583
|
+
"set",
|
|
4584
|
+
key,
|
|
4585
|
+
"--stdin",
|
|
4586
|
+
...opts.service ? ["--service", opts.service] : [],
|
|
4587
|
+
...opts.railwayEnv ? ["--environment", opts.railwayEnv] : []
|
|
4588
|
+
]
|
|
4589
|
+
],
|
|
4590
|
+
installHint: "install the Railway CLI: https://docs.railway.com/guides/cli (then `railway login`)"
|
|
4591
|
+
},
|
|
4592
|
+
netlify: {
|
|
4593
|
+
binary: "netlify",
|
|
4594
|
+
// `netlify env:set KEY VALUE` is argv-only and the CLI reads nothing from
|
|
4595
|
+
// stdin, so the value goes through a 0600 dotenv temp file consumed by
|
|
4596
|
+
// `env:import <file>` (which merges into the site's existing variables,
|
|
4597
|
+
// in every deploy context — env:import has no --context flag).
|
|
4598
|
+
args: (_key, opts, secretFile) => [
|
|
4599
|
+
["env:import", secretFile ?? "", ...opts.site ? ["--site", opts.site] : []]
|
|
4600
|
+
],
|
|
4601
|
+
file: dotenvLine,
|
|
4602
|
+
installHint: "install the Netlify CLI: https://docs.netlify.com/cli/get-started/ (then `netlify login` and `netlify link`)"
|
|
3885
4603
|
}
|
|
3886
4604
|
};
|
|
3887
4605
|
function binaryAvailable(binary) {
|
|
3888
4606
|
const probe = spawnSync(binary, ["--version"], { stdio: "ignore", shell: false });
|
|
3889
4607
|
return !probe.error;
|
|
3890
4608
|
}
|
|
4609
|
+
function withSecretFile(content, fn) {
|
|
4610
|
+
const dir = mkdtempSync(join5(tmpdir(), "qring-push-"));
|
|
4611
|
+
try {
|
|
4612
|
+
const file = join5(dir, "secret.env");
|
|
4613
|
+
writeFileSync6(file, content, { mode: 384, flag: "wx" });
|
|
4614
|
+
return fn(file);
|
|
4615
|
+
} finally {
|
|
4616
|
+
rmSync(dir, { recursive: true, force: true });
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
function destination(opts) {
|
|
4620
|
+
const where = opts.repo ?? opts.app ?? opts.site ?? opts.service;
|
|
4621
|
+
return where ? ` (${where})` : "";
|
|
4622
|
+
}
|
|
3891
4623
|
function resolvePushKeys(opts) {
|
|
3892
4624
|
if (opts.keys?.length) return opts.keys;
|
|
3893
4625
|
const config = readProjectConfig(opts.projectPath);
|
|
@@ -3925,19 +4657,22 @@ function pushSecrets(opts) {
|
|
|
3925
4657
|
result.pushed.push(key);
|
|
3926
4658
|
continue;
|
|
3927
4659
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
const
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
4660
|
+
const input = target.file ? "" : target.stdin?.(key, value) ?? value;
|
|
4661
|
+
const invoke = (secretFile) => {
|
|
4662
|
+
for (const args of target.args(key, opts, secretFile)) {
|
|
4663
|
+
const child = spawnSync(target.binary, args, {
|
|
4664
|
+
input,
|
|
4665
|
+
cwd: opts.projectPath,
|
|
4666
|
+
encoding: "utf8",
|
|
4667
|
+
shell: false
|
|
4668
|
+
});
|
|
4669
|
+
if (child.status !== 0) {
|
|
4670
|
+
return child.stderr?.trim() || child.error?.message || `exit ${child.status}`;
|
|
4671
|
+
}
|
|
3939
4672
|
}
|
|
3940
|
-
|
|
4673
|
+
return null;
|
|
4674
|
+
};
|
|
4675
|
+
const failedInvocation = target.file ? withSecretFile(target.file(key, value), invoke) : invoke();
|
|
3941
4676
|
if (failedInvocation) {
|
|
3942
4677
|
result.failed.push({ key, error: failedInvocation });
|
|
3943
4678
|
continue;
|
|
@@ -3949,7 +4684,7 @@ function pushSecrets(opts) {
|
|
|
3949
4684
|
key,
|
|
3950
4685
|
env: opts.env,
|
|
3951
4686
|
source: opts.source ?? "cli",
|
|
3952
|
-
detail: `${opts.canaryKeys?.includes(key) ? "canary honeytoken " : ""}pushed to ${opts.target}${
|
|
4687
|
+
detail: `${opts.canaryKeys?.includes(key) ? "canary honeytoken " : ""}pushed to ${opts.target}${destination(opts)}`
|
|
3953
4688
|
});
|
|
3954
4689
|
}
|
|
3955
4690
|
}
|
|
@@ -4013,8 +4748,11 @@ ${SYMBOLS.zap} ${c.bold("qring run")} ${c.dim("(dry run)")}`);
|
|
|
4013
4748
|
`Push manifest secrets to a deployment platform via its own CLI (${PUSH_TARGETS.join(", ")}) \u2014 values travel over stdin, never argv`
|
|
4014
4749
|
).option("-k, --keys <keys>", "Comma-separated keys (default: .q-ring.json manifest)").option("--project-path <path>", "Explicit project path").option("-e, --env <env>", "Environment context for superposition collapse").option("--repo <owner/name>", "GitHub repository (github target)").option(
|
|
4015
4750
|
"--vercel-env <envs>",
|
|
4016
|
-
"Comma-separated Vercel environments (default: production)"
|
|
4017
|
-
).option("--
|
|
4751
|
+
"Comma-separated Vercel environments (vercel target; default: production)"
|
|
4752
|
+
).option("--app <name>", "fly.io app (fly target; default: the app in fly.toml)").option("--service <name>", "Railway service name or ID (railway target; default: linked service)").option(
|
|
4753
|
+
"--railway-env <name>",
|
|
4754
|
+
"Railway environment name or ID (railway target; default: linked environment)"
|
|
4755
|
+
).option("--site <id>", "Netlify site name or ID (netlify target; default: linked site)").option("--dry-run", "Show what would be pushed without pushing").option("--json", "Output as JSON").action((targetArg, cmd) => {
|
|
4018
4756
|
if (!PUSH_TARGETS.includes(targetArg)) {
|
|
4019
4757
|
console.error(
|
|
4020
4758
|
c.red(`${SYMBOLS.cross} Unknown target "${targetArg}" \u2014 expected one of: ${PUSH_TARGETS.join(", ")}`)
|
|
@@ -4029,6 +4767,10 @@ ${SYMBOLS.zap} ${c.bold("qring run")} ${c.dim("(dry run)")}`);
|
|
|
4029
4767
|
env: cmd.env,
|
|
4030
4768
|
repo: cmd.repo,
|
|
4031
4769
|
vercelEnvs: cmd.vercelEnv?.split(",").map((e) => e.trim()),
|
|
4770
|
+
app: cmd.app,
|
|
4771
|
+
service: cmd.service,
|
|
4772
|
+
railwayEnv: cmd.railwayEnv,
|
|
4773
|
+
site: cmd.site,
|
|
4032
4774
|
dryRun: cmd.dryRun === true,
|
|
4033
4775
|
source: "cli"
|
|
4034
4776
|
});
|
|
@@ -4119,71 +4861,80 @@ function pick(charset, length) {
|
|
|
4119
4861
|
for (let i = 0; i < length; i++) out += charset[randomInt2(charset.length)];
|
|
4120
4862
|
return out;
|
|
4121
4863
|
}
|
|
4864
|
+
function dodgingPlaceholders(generate) {
|
|
4865
|
+
return () => {
|
|
4866
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
4867
|
+
const value = generate();
|
|
4868
|
+
if (!isPlaceholderValue(value)) return value;
|
|
4869
|
+
}
|
|
4870
|
+
throw new Error("canary generator kept producing placeholder-like values");
|
|
4871
|
+
};
|
|
4872
|
+
}
|
|
4122
4873
|
var CANARY_FORMATS = {
|
|
4123
4874
|
aws: {
|
|
4124
4875
|
name: "aws",
|
|
4125
4876
|
description: "AWS access key id (AKIA\u2026)",
|
|
4126
|
-
generate: () => `AKIA${pick(UPPER_NUM, 16)}`
|
|
4877
|
+
generate: dodgingPlaceholders(() => `AKIA${pick(UPPER_NUM, 16)}`)
|
|
4127
4878
|
},
|
|
4128
4879
|
"aws-secret": {
|
|
4129
4880
|
name: "aws-secret",
|
|
4130
4881
|
description: "AWS secret access key (40-char base64)",
|
|
4131
|
-
generate: () => pick(BASE64, 40)
|
|
4882
|
+
generate: dodgingPlaceholders(() => pick(BASE64, 40))
|
|
4132
4883
|
},
|
|
4133
4884
|
github: {
|
|
4134
4885
|
name: "github",
|
|
4135
4886
|
description: "GitHub classic personal access token (ghp_\u2026)",
|
|
4136
|
-
generate: () => `ghp_${pick(ALNUM, 36)}`
|
|
4887
|
+
generate: dodgingPlaceholders(() => `ghp_${pick(ALNUM, 36)}`)
|
|
4137
4888
|
},
|
|
4138
4889
|
"github-pat": {
|
|
4139
4890
|
name: "github-pat",
|
|
4140
4891
|
description: "GitHub fine-grained personal access token (github_pat_\u2026)",
|
|
4141
|
-
generate: () => `github_pat_${pick(ALNUM, 22)}_${pick(ALNUM, 59)}`
|
|
4892
|
+
generate: dodgingPlaceholders(() => `github_pat_${pick(ALNUM, 22)}_${pick(ALNUM, 59)}`)
|
|
4142
4893
|
},
|
|
4143
4894
|
openai: {
|
|
4144
4895
|
name: "openai",
|
|
4145
4896
|
description: "OpenAI API key (sk-\u2026)",
|
|
4146
|
-
generate: () => `sk-${pick(ALNUM, 48)}`
|
|
4897
|
+
generate: dodgingPlaceholders(() => `sk-${pick(ALNUM, 48)}`)
|
|
4147
4898
|
},
|
|
4148
4899
|
"openai-project": {
|
|
4149
4900
|
name: "openai-project",
|
|
4150
4901
|
description: "OpenAI project API key (sk-proj-\u2026)",
|
|
4151
|
-
generate: () => `sk-proj-${pick(URLSAFE, 74)}T3BlbkFJ${pick(URLSAFE, 74)}`
|
|
4902
|
+
generate: dodgingPlaceholders(() => `sk-proj-${pick(URLSAFE, 74)}T3BlbkFJ${pick(URLSAFE, 74)}`)
|
|
4152
4903
|
},
|
|
4153
4904
|
anthropic: {
|
|
4154
4905
|
name: "anthropic",
|
|
4155
4906
|
description: "Anthropic API key (sk-ant-api03-\u2026)",
|
|
4156
|
-
generate: () => `sk-ant-api03-${pick(URLSAFE, 91)}AA`
|
|
4907
|
+
generate: dodgingPlaceholders(() => `sk-ant-api03-${pick(URLSAFE, 91)}AA`)
|
|
4157
4908
|
},
|
|
4158
4909
|
stripe: {
|
|
4159
4910
|
name: "stripe",
|
|
4160
4911
|
description: "Stripe live secret key (sk_live_\u2026)",
|
|
4161
|
-
generate: () => `sk_live_${pick(ALNUM, 24)}`
|
|
4912
|
+
generate: dodgingPlaceholders(() => `sk_live_${pick(ALNUM, 24)}`)
|
|
4162
4913
|
},
|
|
4163
4914
|
gitlab: {
|
|
4164
4915
|
name: "gitlab",
|
|
4165
4916
|
description: "GitLab personal access token (glpat-\u2026)",
|
|
4166
|
-
generate: () => `glpat-${pick(URLSAFE, 20)}`
|
|
4917
|
+
generate: dodgingPlaceholders(() => `glpat-${pick(URLSAFE, 20)}`)
|
|
4167
4918
|
},
|
|
4168
4919
|
slack: {
|
|
4169
4920
|
name: "slack",
|
|
4170
4921
|
description: "Slack bot token (xoxb-\u2026)",
|
|
4171
|
-
generate: () => `xoxb-${pick(DIGITS, 12)}-${pick(DIGITS, 13)}-${pick(ALNUM, 24)}`
|
|
4922
|
+
generate: dodgingPlaceholders(() => `xoxb-${pick(DIGITS, 12)}-${pick(DIGITS, 13)}-${pick(ALNUM, 24)}`)
|
|
4172
4923
|
},
|
|
4173
4924
|
google: {
|
|
4174
4925
|
name: "google",
|
|
4175
4926
|
description: "Google API key (AIza\u2026)",
|
|
4176
|
-
generate: () => `AIza${pick(URLSAFE, 35)}`
|
|
4927
|
+
generate: dodgingPlaceholders(() => `AIza${pick(URLSAFE, 35)}`)
|
|
4177
4928
|
},
|
|
4178
4929
|
npm: {
|
|
4179
4930
|
name: "npm",
|
|
4180
4931
|
description: "npm access token (npm_\u2026)",
|
|
4181
|
-
generate: () => `npm_${pick(ALNUM, 36)}`
|
|
4932
|
+
generate: dodgingPlaceholders(() => `npm_${pick(ALNUM, 36)}`)
|
|
4182
4933
|
},
|
|
4183
4934
|
generic: {
|
|
4184
4935
|
name: "generic",
|
|
4185
4936
|
description: "Generic high-entropy API key",
|
|
4186
|
-
generate: () => generateSecret({ format: "api-key", prefix: "qk_", length: 40 })
|
|
4937
|
+
generate: dodgingPlaceholders(() => generateSecret({ format: "api-key", prefix: "qk_", length: 40 }))
|
|
4187
4938
|
}
|
|
4188
4939
|
};
|
|
4189
4940
|
var DEFAULT_CANARY_FORMAT = "generic";
|
|
@@ -4924,11 +5675,10 @@ function registerMcpCommands(program2) {
|
|
|
4924
5675
|
}
|
|
4925
5676
|
|
|
4926
5677
|
// src/cli/commands/doctor.ts
|
|
4927
|
-
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as
|
|
4928
|
-
import { join as
|
|
4929
|
-
import { homedir as homedir2 } from "os";
|
|
5678
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "fs";
|
|
5679
|
+
import { join as join6, delimiter } from "path";
|
|
4930
5680
|
function auditDir() {
|
|
4931
|
-
return process.env.QRING_AUDIT_DIR ??
|
|
5681
|
+
return process.env.QRING_AUDIT_DIR ?? configDir();
|
|
4932
5682
|
}
|
|
4933
5683
|
function checkNode() {
|
|
4934
5684
|
const major = Number(process.versions.node.split(".")[0]);
|
|
@@ -4940,8 +5690,8 @@ function checkNode() {
|
|
|
4940
5690
|
}
|
|
4941
5691
|
async function checkKeyringBackend() {
|
|
4942
5692
|
try {
|
|
4943
|
-
const { Entry } = await import("@napi-rs/keyring");
|
|
4944
|
-
const probe = new
|
|
5693
|
+
const { Entry: Entry2 } = await import("@napi-rs/keyring");
|
|
5694
|
+
const probe = new Entry2("q-ring-doctor-probe", "PROBE");
|
|
4945
5695
|
probe.setPassword("ok");
|
|
4946
5696
|
const read = probe.getPassword();
|
|
4947
5697
|
probe.deletePassword();
|
|
@@ -4968,9 +5718,9 @@ async function checkKeyringBackend() {
|
|
|
4968
5718
|
function checkAuditLog() {
|
|
4969
5719
|
const dir = auditDir();
|
|
4970
5720
|
try {
|
|
4971
|
-
const probeFile =
|
|
4972
|
-
|
|
4973
|
-
|
|
5721
|
+
const probeFile = join6(dir, ".doctor-probe");
|
|
5722
|
+
writeFileSync7(probeFile, "ok");
|
|
5723
|
+
rmSync2(probeFile);
|
|
4974
5724
|
} catch (err) {
|
|
4975
5725
|
return {
|
|
4976
5726
|
name: "audit log",
|
|
@@ -4996,7 +5746,7 @@ function checkAuditLog() {
|
|
|
4996
5746
|
};
|
|
4997
5747
|
}
|
|
4998
5748
|
function checkManifest(projectPath) {
|
|
4999
|
-
const manifestPath =
|
|
5749
|
+
const manifestPath = join6(projectPath, ".q-ring.json");
|
|
5000
5750
|
if (!existsSync6(manifestPath)) {
|
|
5001
5751
|
return {
|
|
5002
5752
|
name: "project manifest",
|
|
@@ -5054,11 +5804,11 @@ function checkMcpBinary() {
|
|
|
5054
5804
|
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
5055
5805
|
if (!dir) continue;
|
|
5056
5806
|
for (const ext of exts) {
|
|
5057
|
-
if (existsSync6(
|
|
5807
|
+
if (existsSync6(join6(dir, `qring-mcp${ext}`))) {
|
|
5058
5808
|
return {
|
|
5059
5809
|
name: "qring-mcp binary",
|
|
5060
5810
|
status: "ok",
|
|
5061
|
-
detail: `found at ${
|
|
5811
|
+
detail: `found at ${join6(dir, `qring-mcp${ext}`)}`
|
|
5062
5812
|
};
|
|
5063
5813
|
}
|
|
5064
5814
|
}
|
|
@@ -5242,7 +5992,7 @@ var COMMAND_GROUPS = [
|
|
|
5242
5992
|
{
|
|
5243
5993
|
name: "Project",
|
|
5244
5994
|
symbol: SYMBOLS.package,
|
|
5245
|
-
commands: ["context", "check", "env", "env:generate", "wizard"]
|
|
5995
|
+
commands: ["context", "check", "env", "env:generate", "promote", "diff", "wizard"]
|
|
5246
5996
|
},
|
|
5247
5997
|
{
|
|
5248
5998
|
name: "Quantum",
|
|
@@ -5258,7 +6008,7 @@ var COMMAND_GROUPS = [
|
|
|
5258
6008
|
{
|
|
5259
6009
|
name: "Validation & Rotation",
|
|
5260
6010
|
symbol: SYMBOLS.shield,
|
|
5261
|
-
commands: ["validate", "rotate", "ci:validate"]
|
|
6011
|
+
commands: ["validate", "rotate", "rotate:due", "ci:validate"]
|
|
5262
6012
|
},
|
|
5263
6013
|
{
|
|
5264
6014
|
name: "Dev Tooling",
|
|
@@ -5403,6 +6153,7 @@ function createProgram() {
|
|
|
5403
6153
|
program2.createHelp = () => new GroupedHelp();
|
|
5404
6154
|
registerSecretsCommands(program2);
|
|
5405
6155
|
registerProjectCommands(program2);
|
|
6156
|
+
registerEnvironmentCommands(program2);
|
|
5406
6157
|
registerQuantumCommands(program2);
|
|
5407
6158
|
registerValidationCommands(program2);
|
|
5408
6159
|
registerToolingCommands(program2);
|