run402 4.48.0 → 4.50.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/gitvault-surface.json +1 -1
- package/lib/credentials.mjs +44 -5
- package/lib/repos.mjs +29 -0
- package/package.json +1 -1
- package/sdk/dist/namespaces/deploy.js +64 -7
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +104 -1
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +124 -0
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.d.ts +96 -2
- package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +54 -14
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +119 -36
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
package/gitvault-surface.json
CHANGED
package/lib/credentials.mjs
CHANGED
|
@@ -26,8 +26,10 @@ Usage:
|
|
|
26
26
|
run402 credentials <subcommand> [args...]
|
|
27
27
|
|
|
28
28
|
Project credentials (on the gateway — named, revocable, rotatable):
|
|
29
|
-
issue --kind <anon|service> --name <name> [--project <id>] [--expires <iso8601>]
|
|
29
|
+
issue --kind <anon|service> --name <name> [--project <id>] [--expires <iso8601>] [--import]
|
|
30
30
|
Mint one. The secret is printed ONCE.
|
|
31
|
+
--import also writes it into this machine's
|
|
32
|
+
local key cache (the cold-restart re-key path).
|
|
31
33
|
list [--project <id>] [--include-revoked]
|
|
32
34
|
List credentials (metadata only, never secrets)
|
|
33
35
|
status [--project <id>] Are you still on the retiring legacy key?
|
|
@@ -87,7 +89,7 @@ const SUB_HELP = {
|
|
|
87
89
|
issue: `run402 credentials issue — mint a named project credential
|
|
88
90
|
|
|
89
91
|
Usage:
|
|
90
|
-
run402 credentials issue --kind <anon|service> --name <name> [--project <id>]
|
|
92
|
+
run402 credentials issue --kind <anon|service> --name <name> [--project <id>] [--import]
|
|
91
93
|
[--expires <iso8601>]
|
|
92
94
|
|
|
93
95
|
--kind "anon" is the tenant-facing key; "service" is the privileged one.
|
|
@@ -95,6 +97,11 @@ Usage:
|
|
|
95
97
|
returns 409 CREDENTIAL_NAME_TAKEN — that collision is the idempotency
|
|
96
98
|
story, so a retried create never mints a second credential by accident.
|
|
97
99
|
--expires Optional; must be in the future and within one year.
|
|
100
|
+
--import Also write the minted secret into this machine's local key cache
|
|
101
|
+
(what deploys and data-plane commands read) — the cold-restart
|
|
102
|
+
re-key path in ONE step instead of issue-then-project-keys-import.
|
|
103
|
+
A first --kind anon --import on a machine with no cached entry
|
|
104
|
+
still needs a service key first, same as project-keys import.
|
|
98
105
|
|
|
99
106
|
The secret is printed ONCE, on stdout, inside the JSON. Pipe it:
|
|
100
107
|
run402 credentials issue --kind service --name ci | jq -r .secret
|
|
@@ -407,7 +414,8 @@ const ISSUE_VALUE_FLAGS = ["--project", "--kind", "--name", "--expires"];
|
|
|
407
414
|
|
|
408
415
|
async function issue(args) {
|
|
409
416
|
const a = normalizeArgv(args);
|
|
410
|
-
assertKnownFlags(a, [...ISSUE_VALUE_FLAGS, "--help", "-h"], ISSUE_VALUE_FLAGS);
|
|
417
|
+
assertKnownFlags(a, [...ISSUE_VALUE_FLAGS, "--import", "--help", "-h"], ISSUE_VALUE_FLAGS);
|
|
418
|
+
const importToCache = a.includes("--import");
|
|
411
419
|
const kind = flagValue(a, "--kind");
|
|
412
420
|
const name = flagValue(a, "--name");
|
|
413
421
|
const expiresAt = flagValue(a, "--expires");
|
|
@@ -415,7 +423,7 @@ async function issue(args) {
|
|
|
415
423
|
requirePositionalCount(rest, ISSUE_VALUE_FLAGS, {
|
|
416
424
|
min: 0,
|
|
417
425
|
max: 0,
|
|
418
|
-
command: "run402 credentials issue --kind <anon|service> --name <name> [--project <id>]",
|
|
426
|
+
command: "run402 credentials issue --kind <anon|service> --name <name> [--project <id>] [--import]",
|
|
419
427
|
missing: "",
|
|
420
428
|
});
|
|
421
429
|
if (kind !== "anon" && kind !== "service") {
|
|
@@ -432,8 +440,39 @@ async function issue(args) {
|
|
|
432
440
|
hint: "The name identifies this credential in 'list' and is how you rotate it later, e.g. --name ci-deploy",
|
|
433
441
|
});
|
|
434
442
|
}
|
|
443
|
+
// Validate the --import precondition BEFORE minting: refusing after the
|
|
444
|
+
// mint would burn a show-once secret on a usage error.
|
|
445
|
+
const existing = importToCache ? getProject(projectId) : undefined;
|
|
446
|
+
if (importToCache && kind === "anon" && !existing?.service_key) {
|
|
447
|
+
fail({
|
|
448
|
+
code: "BAD_USAGE",
|
|
449
|
+
message: `--import for an anon key writes the whole cache entry, and no service key is cached for ${projectId} yet.`,
|
|
450
|
+
hint: "Run 'run402 credentials issue --kind service --name <name> --import' first (same rule as project-keys import).",
|
|
451
|
+
details: { project_id: projectId },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
435
454
|
try {
|
|
436
|
-
|
|
455
|
+
const res = await getSdk().credentials.issue(projectId, { kind, name, expiresAt: expiresAt || undefined });
|
|
456
|
+
if (importToCache && res?.secret) {
|
|
457
|
+
// The cold-restart re-key path (gitvault-deploy-lane 6.5a): the minted
|
|
458
|
+
// secret goes straight into the local cache the deploy and data-plane
|
|
459
|
+
// commands read, so a fresh machine re-keys in one command per kind
|
|
460
|
+
// instead of the four-command issue-then-project-keys-import dance.
|
|
461
|
+
// The secret never rides argv; it came back on the mint response.
|
|
462
|
+
saveProject(projectId, {
|
|
463
|
+
anon_key: kind === "anon" ? res.secret : existing?.anon_key ?? "",
|
|
464
|
+
service_key: kind === "service" ? res.secret : existing?.service_key ?? "",
|
|
465
|
+
site_url: existing?.site_url,
|
|
466
|
+
deployed_at: existing?.deployed_at,
|
|
467
|
+
last_deployment_id: existing?.last_deployment_id,
|
|
468
|
+
org_id: existing?.org_id,
|
|
469
|
+
source: "credentials_issue_import",
|
|
470
|
+
cached_at: new Date().toISOString(),
|
|
471
|
+
});
|
|
472
|
+
emitIssued({ ...res, imported_to_local_cache: true });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
emitIssued(res);
|
|
437
476
|
} catch (err) {
|
|
438
477
|
reportSdkError(err);
|
|
439
478
|
}
|
package/lib/repos.mjs
CHANGED
|
@@ -174,6 +174,12 @@ Subcommands:
|
|
|
174
174
|
normal writing mode, so a budget-exceeded run resumes). \`--mirror\`
|
|
175
175
|
additionally runs the keyless mirror integrity probe — it proves
|
|
176
176
|
the mirror's VALIDITY, never its FRESHNESS, and says so.
|
|
177
|
+
In write mode (not \`--no-write\`), when this keystore holds a
|
|
178
|
+
local encryption identity, fsck ALSO submits its own
|
|
179
|
+
chain-verified/decryptable generations as a proof-of-open receipt
|
|
180
|
+
(\`recipient_open_receipt\`, D210) — best-effort: it never changes
|
|
181
|
+
fsck's own verdict, and its outcome rides the result's
|
|
182
|
+
\`open_proof\` block (also surfaced on \`--human\`).
|
|
177
183
|
\`--human\` renders a short summary instead of JSON.
|
|
178
184
|
gc \`git gc\`'s own two halves — checkpoint publication (compact) and
|
|
179
185
|
prune planning — in one verb, NOT described as "exactly git gc":
|
|
@@ -1437,6 +1443,21 @@ async function mirror(args) {
|
|
|
1437
1443
|
|
|
1438
1444
|
// ─── fsck (verify the head chain + materialize refs) ──────────────────
|
|
1439
1445
|
|
|
1446
|
+
/**
|
|
1447
|
+
* D210 (rev 44): one line describing `fsck`'s best-effort proof-of-open
|
|
1448
|
+
* submission outcome — never blank, so a reader always knows whether
|
|
1449
|
+
* `open_proof` is silence (audit mode / no local identity) or a real
|
|
1450
|
+
* attempt (submitted, or a caught failure). Shared by `--human` and the
|
|
1451
|
+
* default JSON-mode stderr summary.
|
|
1452
|
+
*/
|
|
1453
|
+
function formatOpenProofLine(openProof) {
|
|
1454
|
+
if (!openProof || !openProof.attempted) return null;
|
|
1455
|
+
if (openProof.submitted) {
|
|
1456
|
+
return `proof-of-open: ${openProof.deduplicated ? "already on file" : "submitted"} (${openProof.receipt.object_id}, decryptable through ${openProof.receipt.decryptable_to_generation}).`;
|
|
1457
|
+
}
|
|
1458
|
+
return `proof-of-open: not submitted — ${openProof.error.code}: ${openProof.error.message}`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1440
1461
|
/** `repos fsck --human`: the same verdict the stderr lines already carry, condensed into one block. */
|
|
1441
1462
|
function formatFsckHuman(result, mirrorRequested) {
|
|
1442
1463
|
const lines = [`Repo: ${result.repo_id}`];
|
|
@@ -1471,6 +1492,8 @@ function formatFsckHuman(result, mirrorRequested) {
|
|
|
1471
1492
|
} else if (result.retained_refs) {
|
|
1472
1493
|
lines.push(`refs/r402/retain: ${result.retained_refs.retained_count} retained tip(s) referenced (+${result.retained_refs.written.length} -${result.retained_refs.deleted.length} this run).`);
|
|
1473
1494
|
}
|
|
1495
|
+
const openProofLine = formatOpenProofLine(result.open_proof);
|
|
1496
|
+
if (openProofLine) lines.push(openProofLine);
|
|
1474
1497
|
return lines.join("\n");
|
|
1475
1498
|
}
|
|
1476
1499
|
|
|
@@ -1522,6 +1545,12 @@ async function fsck(args) {
|
|
|
1522
1545
|
} else if (result.retained_refs && (result.retained_refs.written.length > 0 || result.retained_refs.deleted.length > 0)) {
|
|
1523
1546
|
console.error(`refs/r402/retain: +${result.retained_refs.written.length} -${result.retained_refs.deleted.length} (${result.retained_refs.retained_count} retained tip(s) total).`);
|
|
1524
1547
|
}
|
|
1548
|
+
// D210 (rev 44): the best-effort proof-of-open submission's own outcome
|
|
1549
|
+
// — informational only, printed regardless of whether it succeeded (a
|
|
1550
|
+
// failure here is exactly as uninteresting to this command's own exit
|
|
1551
|
+
// status as a mirror probe's failure would be).
|
|
1552
|
+
const openProofLine = formatOpenProofLine(result.open_proof);
|
|
1553
|
+
if (openProofLine) console.error(openProofLine);
|
|
1525
1554
|
if (mirrorRequested && result.mirror) {
|
|
1526
1555
|
console.error(`mirror: recoverable generation ${result.mirror.recovered_generation}${result.mirror.chain_break ? ` (chain break at ${result.mirror.chain_break.generation}: ${result.mirror.chain_break.reason})` : ""}.`);
|
|
1527
1556
|
if (result.mirror.data_loss_detected) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.50.0",
|
|
4
4
|
"description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -3873,10 +3873,21 @@ async function uploadInlineCas(client, projectId, bytes, contentType) {
|
|
|
3873
3873
|
* `/apply/v1/plans*` use SIWX, which the kernel's getAuth provides
|
|
3874
3874
|
* automatically — only the apikey-gated paths need this helper.
|
|
3875
3875
|
*
|
|
3876
|
-
*
|
|
3877
|
-
*
|
|
3878
|
-
*
|
|
3879
|
-
*
|
|
3876
|
+
* COLD-RESTART RECOVERY (gitvault-deploy-lane 6.5a, the #624 class one layer
|
|
3877
|
+
* in): when the local credential cache has no entry for the project — the
|
|
3878
|
+
* returning-agent case, where the wallet survives but the once-issued project
|
|
3879
|
+
* keys did not — this helper mints a SHORT-LIVED anon token via
|
|
3880
|
+
* `POST /projects/v1/:project_id/tokens` (the route the platform built as
|
|
3881
|
+
* exactly this recovery path; the gateway's own 401 envelope names it as the
|
|
3882
|
+
* `mint_token` next_action) and uses it as the apikey, memoized in-process
|
|
3883
|
+
* until shortly before it expires. A wallet-holding agent on a fresh machine
|
|
3884
|
+
* deploys with zero extra commands; nothing durable is written anywhere.
|
|
3885
|
+
*
|
|
3886
|
+
* The mint is attempted only on a cache miss and never retried within a
|
|
3887
|
+
* process on failure; a client that cannot mint (no signer, no authority)
|
|
3888
|
+
* falls through to the pre-existing behavior — the request goes out without
|
|
3889
|
+
* an apikey and the gateway rejects with 401, whose envelope names the
|
|
3890
|
+
* recovery.
|
|
3880
3891
|
*/
|
|
3881
3892
|
async function apikeyHeaders(client, projectId) {
|
|
3882
3893
|
// A CI session and a delegate both already authorize these routes via
|
|
@@ -3886,9 +3897,55 @@ async function apikeyHeaders(client, projectId) {
|
|
|
3886
3897
|
if (isCiClient(client) || isDelegateClient(client))
|
|
3887
3898
|
return {};
|
|
3888
3899
|
const project = await client.getProject(projectId);
|
|
3889
|
-
if (
|
|
3890
|
-
return {};
|
|
3891
|
-
|
|
3900
|
+
if (project?.anon_key)
|
|
3901
|
+
return { apikey: project.anon_key };
|
|
3902
|
+
const minted = await mintedAnonToken(client, projectId);
|
|
3903
|
+
return minted ? { apikey: minted } : {};
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Per-client, per-project memo of short-lived minted anon tokens (plus a
|
|
3907
|
+
* one-shot "minting failed, don't hammer" marker). Held only in process
|
|
3908
|
+
* memory — a short-lived token must never land in a durable cache.
|
|
3909
|
+
*/
|
|
3910
|
+
const mintedTokenMemo = new WeakMap();
|
|
3911
|
+
/** Refresh margin: stop using a minted token this long before it expires. */
|
|
3912
|
+
const MINTED_TOKEN_REFRESH_MARGIN_MS = 30_000;
|
|
3913
|
+
async function mintedAnonToken(client, projectId) {
|
|
3914
|
+
let perProject = mintedTokenMemo.get(client);
|
|
3915
|
+
if (!perProject) {
|
|
3916
|
+
perProject = new Map();
|
|
3917
|
+
mintedTokenMemo.set(client, perProject);
|
|
3918
|
+
}
|
|
3919
|
+
const memo = perProject.get(projectId);
|
|
3920
|
+
if (memo) {
|
|
3921
|
+
if ("failed" in memo)
|
|
3922
|
+
return null;
|
|
3923
|
+
if (Date.now() < memo.expiresAtMs - MINTED_TOKEN_REFRESH_MARGIN_MS)
|
|
3924
|
+
return memo.token;
|
|
3925
|
+
}
|
|
3926
|
+
try {
|
|
3927
|
+
const issued = await client.request(`/projects/v1/${encodeURIComponent(projectId)}/tokens`, {
|
|
3928
|
+
method: "POST",
|
|
3929
|
+
// Least authority: the apikey-gated deploy legs need exactly what the
|
|
3930
|
+
// anon key carries, never the service key's power.
|
|
3931
|
+
body: { kind: "anon" },
|
|
3932
|
+
context: "minting a short-lived anon token for deploy (local project keys not cached)",
|
|
3933
|
+
});
|
|
3934
|
+
if (!issued?.secret) {
|
|
3935
|
+
perProject.set(projectId, { failed: true });
|
|
3936
|
+
return null;
|
|
3937
|
+
}
|
|
3938
|
+
const ttlMs = (typeof issued.expires_in === "number" && issued.expires_in > 0 ? issued.expires_in : 300) * 1000;
|
|
3939
|
+
perProject.set(projectId, { token: issued.secret, expiresAtMs: Date.now() + ttlMs });
|
|
3940
|
+
return issued.secret;
|
|
3941
|
+
}
|
|
3942
|
+
catch {
|
|
3943
|
+
// No signer, no authority, or the route is unavailable: fall through to
|
|
3944
|
+
// the pre-existing no-apikey behavior. Marked so one deploy's many
|
|
3945
|
+
// apikey-gated legs don't each re-attempt a doomed mint.
|
|
3946
|
+
perProject.set(projectId, { failed: true });
|
|
3947
|
+
return null;
|
|
3948
|
+
}
|
|
3892
3949
|
}
|
|
3893
3950
|
function isCiClient(client) {
|
|
3894
3951
|
return isCiSessionCredentials(client.credentials);
|