run402 4.105.6 → 4.106.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/lib/command-manifest.mjs +2 -0
- package/lib/repos.mjs +33 -0
- package/lib/transfer.mjs +78 -5
- package/package.json +1 -1
- package/sdk/dist/namespaces/repos.d.ts +50 -0
- package/sdk/dist/namespaces/repos.d.ts.map +1 -1
- package/sdk/dist/namespaces/repos.js +55 -0
- package/sdk/dist/namespaces/repos.js.map +1 -1
- package/sdk/dist/namespaces/transfers.d.ts +91 -0
- package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
- package/sdk/dist/namespaces/transfers.js +12 -0
- package/sdk/dist/namespaces/transfers.js.map +1 -1
- package/sdk/dist/node/vault-publication.d.ts +24 -0
- package/sdk/dist/node/vault-publication.d.ts.map +1 -1
- package/sdk/dist/node/vault-publication.js +127 -81
- package/sdk/dist/node/vault-publication.js.map +1 -1
- package/vault-surface.json +2 -1
|
@@ -2967,97 +2967,127 @@ export class Vault {
|
|
|
2967
2967
|
skipped.push({ principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint, reason: "pinned_key_mismatch", details: { pinned_fingerprint: pinned, directory_fingerprint: entry.ek_fingerprint } });
|
|
2968
2968
|
continue;
|
|
2969
2969
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2970
|
+
const wrapOutcome = await this.#wrapEnvelopeRecipient(entry, epoch, signer);
|
|
2971
|
+
if (wrapOutcome.outcome === "skipped") {
|
|
2972
|
+
skipped.push(wrapOutcome.skipped);
|
|
2972
2973
|
continue;
|
|
2973
2974
|
}
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2975
|
+
if (wrapOutcome.outcome === "wrapped")
|
|
2976
|
+
wrapped.push({ principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint });
|
|
2977
|
+
else
|
|
2978
|
+
alreadyCovered.push(entry.ek_fingerprint);
|
|
2979
|
+
pins[entry.principal_id] = entry.ek_fingerprint;
|
|
2980
|
+
pinsChanged = true;
|
|
2981
|
+
}
|
|
2982
|
+
if (pinsChanged)
|
|
2983
|
+
this.keystore.updateRepo(this.repoId, { envelope_recipient_pins: pins });
|
|
2984
|
+
return { repo_id: this.repoId, org_id: repo.org_id, epoch, wrapped, already_covered: alreadyCovered, skipped };
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Seal and store ONE recipient's `key_envelope` for the current epoch:
|
|
2988
|
+
* the per-recipient step of {@link reconcileEnvelopeRecipients}, also used
|
|
2989
|
+
* by {@link wrapEnvelopeForRecipient}. A recipient whose public key is
|
|
2990
|
+
* missing or does not derive its printed fingerprint is skipped, never
|
|
2991
|
+
* sealed; a concurrent wrap of the same recipient counts as covered only
|
|
2992
|
+
* once the stored winner verifies.
|
|
2993
|
+
*/
|
|
2994
|
+
async #wrapEnvelopeRecipient(entry, epoch, signer) {
|
|
2995
|
+
if (typeof entry.public_key !== "string" || entry.public_key.length === 0) {
|
|
2996
|
+
return { outcome: "skipped", skipped: { principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint, reason: "missing_public_key" } };
|
|
2997
|
+
}
|
|
2998
|
+
let recipientPublicKey;
|
|
2999
|
+
try {
|
|
3000
|
+
recipientPublicKey = fromBase64url(entry.public_key, "public_key");
|
|
3001
|
+
}
|
|
3002
|
+
catch {
|
|
3003
|
+
return { outcome: "skipped", skipped: { principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint, reason: "invalid_public_key" } };
|
|
3004
|
+
}
|
|
3005
|
+
// Defense in depth: a directory row whose printed fingerprint does not
|
|
3006
|
+
// derive from its own public_key is corrupt data, not a valid
|
|
3007
|
+
// recipient — fail loud here rather than let `sealKeyEnvelope` (which
|
|
3008
|
+
// derives the fingerprint itself and never trusts a caller-supplied
|
|
3009
|
+
// one) silently seal under a fingerprint that disagrees with the one
|
|
3010
|
+
// the directory printed.
|
|
3011
|
+
const derived = ekFingerprint(recipientPublicKey);
|
|
3012
|
+
if (derived !== entry.ek_fingerprint) {
|
|
3013
|
+
return { outcome: "skipped", skipped: { principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint, reason: "invalid_public_key", details: { derived_fingerprint: derived } } };
|
|
3014
|
+
}
|
|
3015
|
+
const sealed = await sealKeyEnvelope({
|
|
3016
|
+
k_repo: this.kRepo(),
|
|
3017
|
+
repo_id: this.repoId,
|
|
3018
|
+
epoch,
|
|
3019
|
+
recipient_public_key: recipientPublicKey,
|
|
3020
|
+
signer,
|
|
3021
|
+
created_at: formatVaultTimestamp(this.now()),
|
|
3022
|
+
});
|
|
3023
|
+
try {
|
|
3024
|
+
const byo = await this.resolveByoWriteTarget();
|
|
3025
|
+
await this.transport.putObject({
|
|
2995
3026
|
repo_id: this.repoId,
|
|
2996
|
-
epoch,
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3027
|
+
path: vaultPaths.envelope(epoch, sealed.receipt.recipient_fingerprint),
|
|
3028
|
+
bytes: sealed.stored_bytes,
|
|
3029
|
+
expected_sha256: sealed.stored_bytes_sha256,
|
|
3030
|
+
expected_size_bytes: sealed.size_bytes,
|
|
3031
|
+
...(byo ? { byo } : {}),
|
|
3000
3032
|
});
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
//
|
|
3015
|
-
//
|
|
3016
|
-
//
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
const winnerPath = vaultPaths.envelope(epoch, sealed.receipt.recipient_fingerprint);
|
|
3028
|
-
const winnerBytes = await this.transport.getObject({ repo_id: this.repoId, path: winnerPath }).catch(() => null);
|
|
3029
|
-
let winnerOk = false;
|
|
3030
|
-
if (winnerBytes) {
|
|
3031
|
-
try {
|
|
3032
|
-
const winner = parseVaultStrict(new TextDecoder().decode(winnerBytes));
|
|
3033
|
-
const g = (await this.genesis()).genesis;
|
|
3034
|
-
winnerOk = winner.repo_id === this.repoId && winner.epoch === epoch && winner.recipient_fingerprint === entry.ek_fingerprint
|
|
3035
|
-
&& winner.created_by === g.writer_key_id
|
|
3036
|
-
&& verifyVaultObject(winner, g.creator_signing_pubkey);
|
|
3037
|
-
}
|
|
3038
|
-
catch {
|
|
3039
|
-
winnerOk = false;
|
|
3040
|
-
}
|
|
3033
|
+
return { outcome: "wrapped" };
|
|
3034
|
+
}
|
|
3035
|
+
catch (e) {
|
|
3036
|
+
// A concurrent reconcile (another machine/session) may have wrapped
|
|
3037
|
+
// the SAME recipient first — HPKE seal is randomized, so two valid
|
|
3038
|
+
// wraps of the same K_repo to the same recipient produce DIFFERENT
|
|
3039
|
+
// ciphertext bytes, and the create-only path's read-and-compare
|
|
3040
|
+
// reports that as `VAULT_OBJECT_EXISTS_DIFFERENT` even though the
|
|
3041
|
+
// recipient is now genuinely covered. Treat exactly that code as a
|
|
3042
|
+
// benign race, not a failure; anything else propagates.
|
|
3043
|
+
if (isRun402Error(e) && e.code === "VAULT_OBJECT_EXISTS_DIFFERENT") {
|
|
3044
|
+
// vault-agent-envelopes (consult 7.6): a benign race is only
|
|
3045
|
+
// benign once the WINNING envelope has been read back and verified
|
|
3046
|
+
// — same repo, same epoch, this recipient, signed by the vault's
|
|
3047
|
+
// registered writer. Anything else is recorded as skipped, never as
|
|
3048
|
+
// coverage, and never pinned.
|
|
3049
|
+
const winnerPath = vaultPaths.envelope(epoch, sealed.receipt.recipient_fingerprint);
|
|
3050
|
+
const winnerBytes = await this.transport.getObject({ repo_id: this.repoId, path: winnerPath }).catch(() => null);
|
|
3051
|
+
let winnerOk = false;
|
|
3052
|
+
if (winnerBytes) {
|
|
3053
|
+
try {
|
|
3054
|
+
const winner = parseVaultStrict(new TextDecoder().decode(winnerBytes));
|
|
3055
|
+
const g = (await this.genesis()).genesis;
|
|
3056
|
+
winnerOk = winner.repo_id === this.repoId && winner.epoch === epoch && winner.recipient_fingerprint === entry.ek_fingerprint
|
|
3057
|
+
&& winner.created_by === g.writer_key_id
|
|
3058
|
+
&& verifyVaultObject(winner, g.creator_signing_pubkey);
|
|
3041
3059
|
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
// winner does not verify is evidence of tampering, equivocation,
|
|
3045
|
-
// or broken object identity — categorically different from a
|
|
3046
|
-
// stale recipient. FATAL, never an ordinary skip.
|
|
3047
|
-
fail("VAULT_ENVELOPE_ALTERED", `a conflicting key_envelope already stored at ${winnerPath} does not verify as this vault's writer-signed envelope for this recipient — refusing to treat the conflict as a benign race`, "reconciling vault envelope recipients", { repo_id: this.repoId, path: winnerPath, epoch, recipient_fingerprint: entry.ek_fingerprint });
|
|
3060
|
+
catch {
|
|
3061
|
+
winnerOk = false;
|
|
3048
3062
|
}
|
|
3049
|
-
alreadyCovered.push(entry.ek_fingerprint);
|
|
3050
3063
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3064
|
+
if (!winnerOk) {
|
|
3065
|
+
// Consult round 2 §7: a conflicting IMMUTABLE object whose stored
|
|
3066
|
+
// winner does not verify is evidence of tampering, equivocation,
|
|
3067
|
+
// or broken object identity — categorically different from a
|
|
3068
|
+
// stale recipient. FATAL, never an ordinary skip.
|
|
3069
|
+
fail("VAULT_ENVELOPE_ALTERED", `a conflicting key_envelope already stored at ${winnerPath} does not verify as this vault's writer-signed envelope for this recipient — refusing to treat the conflict as a benign race`, "reconciling vault envelope recipients", { repo_id: this.repoId, path: winnerPath, epoch, recipient_fingerprint: entry.ek_fingerprint });
|
|
3053
3070
|
}
|
|
3071
|
+
return { outcome: "already_covered" };
|
|
3072
|
+
}
|
|
3073
|
+
else {
|
|
3074
|
+
throw e;
|
|
3054
3075
|
}
|
|
3055
|
-
pins[entry.principal_id] = entry.ek_fingerprint;
|
|
3056
|
-
pinsChanged = true;
|
|
3057
3076
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Wrap the current epoch's key for ONE named recipient that is not in the
|
|
3080
|
+
* vault organization's key directory: the nominated recipient of a pending
|
|
3081
|
+
* project transfer (project-transfer-vault-handover). The caller passes the
|
|
3082
|
+
* recipient's public keys as the transfer read reports them; the gateway
|
|
3083
|
+
* accepts the envelope only for that transfer's nominee.
|
|
3084
|
+
*/
|
|
3085
|
+
async wrapEnvelopeForRecipient(recipient) {
|
|
3086
|
+
const outcome = await this.#wrapEnvelopeRecipient(recipient, this.epoch(), this.signingKeypair());
|
|
3087
|
+
if (outcome.outcome === "skipped") {
|
|
3088
|
+
fail("VAULT_ENVELOPE_RECIPIENT_INVALID", `the transfer recipient's published key cannot be wrapped to (${outcome.skipped.reason})`, "wrapping the source key for the transfer recipient", { principal_id: recipient.principal_id, ek_fingerprint: recipient.ek_fingerprint });
|
|
3089
|
+
}
|
|
3090
|
+
return outcome;
|
|
3061
3091
|
}
|
|
3062
3092
|
// ── object building ──
|
|
3063
3093
|
/**
|
|
@@ -4444,6 +4474,22 @@ export class Vault {
|
|
|
4444
4474
|
}
|
|
4445
4475
|
return this.rotateEpoch({ reason: "member_removed", recipient_state_version: state, recipient_revocation_version: revocation, client_idempotency_key: options.client_idempotency_key });
|
|
4446
4476
|
}
|
|
4477
|
+
/**
|
|
4478
|
+
* The rotation that completes a retirement of a previous owner's writers
|
|
4479
|
+
* after a project transfer (`reason:"writer_key_revoked"`, owner + step-up):
|
|
4480
|
+
* the gateway already blocked the keys outside the vault's organization;
|
|
4481
|
+
* this removes exactly that set. Counters come off the envelope-recipients
|
|
4482
|
+
* read, as for a member removal.
|
|
4483
|
+
*/
|
|
4484
|
+
async rotateEpochForWriterRetirement(options = {}) {
|
|
4485
|
+
const recipients = await this.transport.listEnvelopeRecipients({ repo_id: this.repoId });
|
|
4486
|
+
const state = recipients.recipient_state_version;
|
|
4487
|
+
const revocation = recipients.recipient_revocation_version;
|
|
4488
|
+
if (typeof state !== "string" || typeof revocation !== "string") {
|
|
4489
|
+
fail("VAULT_ROTATION_COUNTERS_UNAVAILABLE", "the gateway did not report the org's rotation counters on the envelope-recipients read — the retirement rotation cannot be fenced without them", "reading the rotation counters");
|
|
4490
|
+
}
|
|
4491
|
+
return this.rotateEpoch({ reason: "writer_key_revoked", recipient_state_version: state, recipient_revocation_version: revocation, client_idempotency_key: options.client_idempotency_key });
|
|
4492
|
+
}
|
|
4447
4493
|
/**
|
|
4448
4494
|
* The ONE fully self-contained rotation entry point: declares
|
|
4449
4495
|
* `reason:"recipient_key_revoked"` (owner + step-up) for `principal_id`,
|