run402 4.69.3 → 4.69.5
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/org.mjs +62 -1
- package/package.json +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +11 -0
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +11 -2
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +11 -0
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +23 -2
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.d.ts +4 -0
- 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-deploy.js +1 -1
- package/sdk/dist/node/gitvault-deploy.js.map +1 -1
- package/sdk/dist/node/gitvault-handoff.d.ts +8 -0
- package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-handoff.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +24 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +90 -7
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
|
@@ -3507,6 +3507,7 @@ export class GitvaultVault {
|
|
|
3507
3507
|
*/
|
|
3508
3508
|
async push(options) {
|
|
3509
3509
|
let conflicts = 0;
|
|
3510
|
+
let removalRotated = false;
|
|
3510
3511
|
let base = options.base ?? (await this.materialize());
|
|
3511
3512
|
for (;;) {
|
|
3512
3513
|
// gitvault-multi-writer (task 5.8) — checked fresh on EVERY attempt
|
|
@@ -3518,10 +3519,34 @@ export class GitvaultVault {
|
|
|
3518
3519
|
// the ordinary GITVAULT_WRITER_NOT_ADMITTED on the first attempt.
|
|
3519
3520
|
this.assertCallerIsWriter("publishing gitvault head", conflicts > 0);
|
|
3520
3521
|
const evaluation = await evaluateRefTransaction(base.refs, options.transaction, { isAncestor: (a, d) => isAncestor(this.git(), a, d), protocol_refs: options.protocol_refs });
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3522
|
+
let published;
|
|
3523
|
+
try {
|
|
3524
|
+
published = await this.publishGeneration({
|
|
3525
|
+
base, refs: evaluation.refs, dropped: evaluation.dropped, head_target: options.head_target ?? base.head_target,
|
|
3526
|
+
force_checkpoint: options.checkpoint === true, cutoff: options.cutoff ?? null, capture_binding: options.capture_binding,
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
catch (e) {
|
|
3530
|
+
// gitvault-human-envelopes D5 ("Remove: next capture rotates the
|
|
3531
|
+
// epoch key to the remaining set") / gitvault-multi-writer D6: an
|
|
3532
|
+
// outstanding MEMBERSHIP REMOVAL is completed by the next push of any
|
|
3533
|
+
// surviving writer — rotate under the writer-capable
|
|
3534
|
+
// reason:"member_removed", re-materialize on the rotated chain, and
|
|
3535
|
+
// retry this push once. Every OTHER cause the gate names (a
|
|
3536
|
+
// migration bootstrap, an epoch-secret exposure) stays THROWN, with
|
|
3537
|
+
// the enriched next_actions naming its owner + step-up remedy.
|
|
3538
|
+
const details = isRun402Error(e) && e.code === "EPOCH_ROTATION_REQUIRED"
|
|
3539
|
+
? (e.details ?? {})
|
|
3540
|
+
: null;
|
|
3541
|
+
const removalOnly = details !== null && (details.revocation_outstanding === true || details.writer_removal_outstanding === true)
|
|
3542
|
+
&& details.migration_required !== true && details.exposure_outstanding !== true;
|
|
3543
|
+
if (!removalOnly || removalRotated)
|
|
3544
|
+
throw e;
|
|
3545
|
+
removalRotated = true;
|
|
3546
|
+
await this.rotateEpochForMemberRemoval();
|
|
3547
|
+
base = await this.materialize();
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3525
3550
|
if (published.outcome === "conflict") {
|
|
3526
3551
|
conflicts += 1;
|
|
3527
3552
|
if (conflicts > this.retries)
|
|
@@ -3960,6 +3985,20 @@ export class GitvaultVault {
|
|
|
3960
3985
|
fail("GITVAULT_DESIRED_STATE_UNAVAILABLE", "the gateway did not report desired-recipient state (desired[]) for this vault — an epoch rotation cannot compute its H-partition without it", "computing the epoch-rotation H-partition");
|
|
3961
3986
|
}
|
|
3962
3987
|
const pinManifest = await this.loadEffectivePinManifest(base.generation);
|
|
3988
|
+
// The vault record: `writer_set` (the chain's active writers, by
|
|
3989
|
+
// principal) and `ineligible_members` (the gateway-blocked writers this
|
|
3990
|
+
// rotation's writer_set_update must remove). Read ONCE here; both the
|
|
3991
|
+
// H-partition below and the writer_set_update fold-in further down use it.
|
|
3992
|
+
const vaultRecord = await this.transport.getVaultRecord({ repo_id: this.repoId });
|
|
3993
|
+
const ineligible = vaultRecord.ineligible_members ?? [];
|
|
3994
|
+
// kygit-handoff's member-removal decision: a keyed desired recipient
|
|
3995
|
+
// whose signing key is an active writer AND survives this rotation is
|
|
3996
|
+
// included on its current directory fingerprint, pin or no pin (the
|
|
3997
|
+
// writer set is client-signed chain state — the same authority that
|
|
3998
|
+
// lets it sign heads). Mirrors the gateway's own rule under its fence.
|
|
3999
|
+
const blockedWriterPrincipalIds = new Set(ineligible.map((m) => m.principal_id));
|
|
4000
|
+
const survivingWriterPrincipalIds = new Set((vaultRecord.writer_set?.writers ?? []).map((w) => w.principal_id).filter((id) => !blockedWriterPrincipalIds.has(id)));
|
|
4001
|
+
const directoryFingerprintOf = new Map(desired.filter((d) => d.status === "active" && d.ek_fingerprint).map((d) => [d.principal_id, d.ek_fingerprint]));
|
|
3963
4002
|
// D196's H-partition: included / excluded_keyless / excluded_unconfirmed.
|
|
3964
4003
|
// A `pending_removal` desired-recipient row is EXCLUDED from H entirely
|
|
3965
4004
|
// (the gateway's own H is `status='active'` only) — that omission is
|
|
@@ -3977,6 +4016,21 @@ export class GitvaultVault {
|
|
|
3977
4016
|
}
|
|
3978
4017
|
const pinned = pinManifest.pinnedFingerprintOf.get(d.principal_id);
|
|
3979
4018
|
if (pinned === undefined) {
|
|
4019
|
+
if (survivingWriterPrincipalIds.has(d.principal_id)) {
|
|
4020
|
+
// A surviving writer without a pin: included on the directory key.
|
|
4021
|
+
let pub;
|
|
4022
|
+
try {
|
|
4023
|
+
pub = fromBase64url(d.public_key, "public_key");
|
|
4024
|
+
}
|
|
4025
|
+
catch {
|
|
4026
|
+
fail("VALIDATION_FAILED", `surviving writer ${d.principal_id}'s directory public key is not valid base64url`, "computing the epoch-rotation H-partition");
|
|
4027
|
+
}
|
|
4028
|
+
if (ekFingerprint(pub) !== d.ek_fingerprint) {
|
|
4029
|
+
fail("VALIDATION_FAILED", `surviving writer ${d.principal_id}'s directory public key does not hash to its declared fingerprint`, "computing the epoch-rotation H-partition");
|
|
4030
|
+
}
|
|
4031
|
+
included.push({ principal_id: d.principal_id, ek_fingerprint: d.ek_fingerprint, public_key: pub });
|
|
4032
|
+
continue;
|
|
4033
|
+
}
|
|
3980
4034
|
excludedUnconfirmed.push(d.principal_id);
|
|
3981
4035
|
continue;
|
|
3982
4036
|
}
|
|
@@ -4017,7 +4071,7 @@ export class GitvaultVault {
|
|
|
4017
4071
|
// Self-consistency (defense in depth — the gateway's own recomputation under its live lock is authoritative).
|
|
4018
4072
|
const desiredIds = new Set(desired.filter((d) => d.status === "active").map((d) => d.principal_id));
|
|
4019
4073
|
const keyedIds = new Set(desired.filter((d) => d.status === "active" && d.ek_fingerprint).map((d) => d.principal_id));
|
|
4020
|
-
const hCheck = checkHPartition({ desiredPrincipalIds: desiredIds, keyedPrincipalIds: keyedIds, pinnedFingerprintOf: pinManifest.pinnedFingerprintOf, included, excludedKeylessPrincipalIds: excludedKeyless, excludedUnconfirmedPrincipalIds: excludedUnconfirmed });
|
|
4074
|
+
const hCheck = checkHPartition({ desiredPrincipalIds: desiredIds, keyedPrincipalIds: keyedIds, pinnedFingerprintOf: pinManifest.pinnedFingerprintOf, included, excludedKeylessPrincipalIds: excludedKeyless, excludedUnconfirmedPrincipalIds: excludedUnconfirmed, survivingWriterPrincipalIds, directoryFingerprintOf });
|
|
4021
4075
|
if (!hCheck.ok)
|
|
4022
4076
|
fail("VALIDATION_FAILED", `internal: the H-partition this producer built is not a valid bijection over H (${hCheck.detail}) — refusing to submit a rotation the gateway would refuse`, "computing the epoch-rotation H-partition");
|
|
4023
4077
|
const targetPartitionDigest = computeTargetPartitionDigest({
|
|
@@ -4057,8 +4111,6 @@ export class GitvaultVault {
|
|
|
4057
4111
|
// with zero client changes — the fold-in doesn't care WHY a key
|
|
4058
4112
|
// is blocked, only THAT it is.
|
|
4059
4113
|
const writerPin = this.repoFile().writer_set_pin;
|
|
4060
|
-
const vaultRecord = await this.transport.getVaultRecord({ repo_id: this.repoId });
|
|
4061
|
-
const ineligible = vaultRecord.ineligible_members ?? [];
|
|
4062
4114
|
let writerSetUpdate = null;
|
|
4063
4115
|
if (ineligible.length > 0) {
|
|
4064
4116
|
if (!writerPin) {
|
|
@@ -4090,10 +4142,19 @@ export class GitvaultVault {
|
|
|
4090
4142
|
}
|
|
4091
4143
|
// D195: build + sign the rotation_attempt_descriptor, derive rotation_id, submit the create-only CAS BEFORE any envelope upload.
|
|
4092
4144
|
const clientIdempotencyKey = options.client_idempotency_key ?? newHex32();
|
|
4145
|
+
// D204 (rev 43): `migration_bootstrap` is schema-optional but the
|
|
4146
|
+
// gateway's fence requires the boolean on BOTH the descriptor and the
|
|
4147
|
+
// payload, and requires the two to agree with its own locked branch
|
|
4148
|
+
// value (the vault's migration flag AND no predecessor manifest). This
|
|
4149
|
+
// producer never drives the bootstrap branch — that is the pre-rev-42
|
|
4150
|
+
// repair ceremony, which co-rides a first manifest — so it declares
|
|
4151
|
+
// `false`; omitting the field is refused RECIPIENT_SET_MISMATCH.
|
|
4152
|
+
const migrationBootstrap = false;
|
|
4093
4153
|
const descriptorFields = {
|
|
4094
4154
|
format: GITVAULT_FORMAT, object_kind: "rotation_attempt_descriptor", suite: GITVAULT_SUITE,
|
|
4095
4155
|
repo_id: this.repoId, base_head_sha256: base.head_sha256, new_epoch: newEpoch,
|
|
4096
4156
|
recipient_state_version: options.recipient_state_version, recipient_revocation_version: options.recipient_revocation_version,
|
|
4157
|
+
migration_bootstrap: migrationBootstrap,
|
|
4097
4158
|
pin_manifest_sha256: pinManifest.pinManifestSha256, target_partition_digest: targetPartitionDigest,
|
|
4098
4159
|
client_idempotency_key: clientIdempotencyKey, writer_key_id: this.writerKeyId(),
|
|
4099
4160
|
// D227 (rev 47): present together IFF this rotation carries a writer_set_update — frozen at THIS attempt's own admission fence, mirroring recipient_state_version/recipient_revocation_version's existing discipline.
|
|
@@ -4122,6 +4183,7 @@ export class GitvaultVault {
|
|
|
4122
4183
|
const payloadBase = {
|
|
4123
4184
|
new_epoch: newEpoch, rotation_id: rotationId, reason: options.reason,
|
|
4124
4185
|
recipient_state_version: options.recipient_state_version, recipient_revocation_version: options.recipient_revocation_version,
|
|
4186
|
+
migration_bootstrap: migrationBootstrap,
|
|
4125
4187
|
pin_manifest_sha256: pinManifest.pinManifestSha256, target_partition_digest: targetPartitionDigest,
|
|
4126
4188
|
epoch_key_commitment: epochKeyCommitmentValue, excluded_keyless_principal_ids: excludedKeyless, excluded_unconfirmed_principal_ids: excludedUnconfirmed,
|
|
4127
4189
|
recipient_authority_attestation: null, envelopes: sealedReceipts,
|
|
@@ -4266,6 +4328,27 @@ export class GitvaultVault {
|
|
|
4266
4328
|
};
|
|
4267
4329
|
}
|
|
4268
4330
|
}
|
|
4331
|
+
/**
|
|
4332
|
+
* The writer-capable rotation that completes an org membership removal
|
|
4333
|
+
* (gitvault-multi-writer D6: "`member_removed` keeps its automatic
|
|
4334
|
+
* writer-capable path"). The removal itself already advanced the org's
|
|
4335
|
+
* D194 counters and flipped the member to `pending_removal`, so there is
|
|
4336
|
+
* nothing to declare: read the counters off the envelope-recipients read
|
|
4337
|
+
* (the same read the H-partition uses) and rotate under
|
|
4338
|
+
* `reason:"member_removed"`, which needs `gitvault.writer` only — any
|
|
4339
|
+
* surviving writer can run it, no owner step-up. Refuses
|
|
4340
|
+
* `GITVAULT_ROTATION_COUNTERS_UNAVAILABLE` on a gateway that does not yet
|
|
4341
|
+
* carry the counters on that read.
|
|
4342
|
+
*/
|
|
4343
|
+
async rotateEpochForMemberRemoval(options = {}) {
|
|
4344
|
+
const recipients = await this.transport.listEnvelopeRecipients({ repo_id: this.repoId });
|
|
4345
|
+
const state = recipients.recipient_state_version;
|
|
4346
|
+
const revocation = recipients.recipient_revocation_version;
|
|
4347
|
+
if (typeof state !== "string" || typeof revocation !== "string") {
|
|
4348
|
+
fail("GITVAULT_ROTATION_COUNTERS_UNAVAILABLE", "the gateway did not report the org's rotation counters on the envelope-recipients read — a member-removal rotation cannot be fenced without them; an owner can run `run402 repos access revoke-key <principal_id>` instead", "reading the rotation counters");
|
|
4349
|
+
}
|
|
4350
|
+
return this.rotateEpoch({ reason: "member_removed", recipient_state_version: state, recipient_revocation_version: revocation, client_idempotency_key: options.client_idempotency_key });
|
|
4351
|
+
}
|
|
4269
4352
|
/**
|
|
4270
4353
|
* The ONE fully self-contained rotation entry point: declares
|
|
4271
4354
|
* `reason:"recipient_key_revoked"` (owner + step-up) for `principal_id`,
|