@run402/sdk 4.93.2 → 4.95.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 +40 -38
- package/core-dist/config.d.ts +16 -2
- package/core-dist/config.js +52 -18
- package/core-dist/control-plane-session.d.ts +29 -14
- package/core-dist/control-plane-session.js +31 -6
- package/core-dist/keystore.js +7 -7
- package/core-dist/profiles.d.ts +3 -3
- package/core-dist/profiles.js +9 -7
- package/core-dist/{allowance-auth.d.ts → wallet-auth.d.ts} +6 -6
- package/core-dist/{allowance-auth.js → wallet-auth.js} +15 -15
- package/core-dist/wallet.d.ts +22 -0
- package/core-dist/{allowance.js → wallet.js} +20 -20
- package/core-dist/{write-auth-session.d.ts → write-approvals.d.ts} +28 -26
- package/core-dist/{write-auth-session.js → write-approvals.js} +19 -19
- package/dist/actions.d.ts +4 -4
- package/dist/actions.d.ts.map +1 -1
- package/dist/app-up.d.ts +2 -2
- package/dist/app-up.d.ts.map +1 -1
- package/dist/config.d.ts +1 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/control-plane-credentials.d.ts +8 -8
- package/dist/control-plane-credentials.js +7 -7
- package/dist/credentials.d.ts +20 -20
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +1 -1
- package/dist/errors.d.ts +26 -28
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +35 -38
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +29 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -17
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +10 -9
- package/dist/kernel.js.map +1 -1
- package/dist/namespaces/admin.d.ts +52 -121
- package/dist/namespaces/admin.d.ts.map +1 -1
- package/dist/namespaces/admin.js +41 -45
- package/dist/namespaces/admin.js.map +1 -1
- package/dist/namespaces/agent.d.ts +2 -2
- package/dist/namespaces/agent.d.ts.map +1 -1
- package/dist/namespaces/agent.js +2 -2
- package/dist/namespaces/agent.js.map +1 -1
- package/dist/namespaces/ai.d.ts +2 -2
- package/dist/namespaces/ai.js +3 -3
- package/dist/namespaces/ai.js.map +1 -1
- package/dist/namespaces/assets.js +1 -1
- package/dist/namespaces/assets.js.map +1 -1
- package/dist/namespaces/billing.d.ts +6 -3
- package/dist/namespaces/billing.d.ts.map +1 -1
- package/dist/namespaces/billing.js.map +1 -1
- package/dist/namespaces/buzz-notifications.types.d.ts +1 -1
- package/dist/namespaces/buzz.d.ts +2 -2
- package/dist/namespaces/buzz.d.ts.map +1 -1
- package/dist/namespaces/buzz.js +2 -2
- package/dist/namespaces/buzz.js.map +1 -1
- package/dist/namespaces/buzz.types.d.ts +2 -2
- package/dist/namespaces/buzz.types.d.ts.map +1 -1
- package/dist/namespaces/ci.js +3 -3
- package/dist/namespaces/ci.js.map +1 -1
- package/dist/namespaces/credentials.types.d.ts +1 -1
- package/dist/namespaces/deploy.d.ts +1 -1
- package/dist/namespaces/deploy.d.ts.map +1 -1
- package/dist/namespaces/deploy.js +31 -32
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/deploy.types.d.ts +6 -6
- package/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/dist/namespaces/domains.d.ts +10 -2
- package/dist/namespaces/domains.d.ts.map +1 -1
- package/dist/namespaces/domains.js +10 -5
- package/dist/namespaces/domains.js.map +1 -1
- package/dist/namespaces/errors.js +1 -1
- package/dist/namespaces/escalations.d.ts +1 -1
- package/dist/namespaces/escalations.js +1 -1
- package/dist/namespaces/escalations.types.d.ts +2 -2
- package/dist/namespaces/escalations.types.d.ts.map +1 -1
- package/dist/namespaces/events.types.d.ts +1 -1
- package/dist/namespaces/functions.d.ts +1 -1
- package/dist/namespaces/functions.js +2 -2
- package/dist/namespaces/functions.js.map +1 -1
- package/dist/namespaces/gitvault.crypto.d.ts +1 -1
- package/dist/namespaces/gitvault.d.ts +26 -26
- package/dist/namespaces/gitvault.d.ts.map +1 -1
- package/dist/namespaces/gitvault.js +113 -113
- package/dist/namespaces/gitvault.js.map +1 -1
- package/dist/namespaces/me.d.ts +120 -0
- package/dist/namespaces/me.d.ts.map +1 -0
- package/dist/namespaces/me.js +34 -0
- package/dist/namespaces/me.js.map +1 -0
- package/dist/namespaces/org.d.ts +108 -6
- package/dist/namespaces/org.d.ts.map +1 -1
- package/dist/namespaces/org.js +63 -7
- package/dist/namespaces/org.js.map +1 -1
- package/dist/namespaces/org.types.d.ts +29 -9
- package/dist/namespaces/org.types.d.ts.map +1 -1
- package/dist/namespaces/pay.js +3 -3
- package/dist/namespaces/pay.js.map +1 -1
- package/dist/namespaces/projects.d.ts +9 -9
- package/dist/namespaces/projects.js +15 -15
- package/dist/namespaces/projects.js.map +1 -1
- package/dist/namespaces/projects.types.d.ts +28 -12
- package/dist/namespaces/projects.types.d.ts.map +1 -1
- package/dist/namespaces/rooms.d.ts +7 -7
- package/dist/namespaces/rooms.js +17 -17
- package/dist/namespaces/rooms.js.map +1 -1
- package/dist/namespaces/rooms.types.d.ts +9 -9
- package/dist/namespaces/rooms.types.d.ts.map +1 -1
- package/dist/namespaces/service.d.ts +1 -1
- package/dist/namespaces/service.js +1 -1
- package/dist/namespaces/{operator-session.d.ts → session.d.ts} +135 -51
- package/dist/namespaces/session.d.ts.map +1 -0
- package/dist/namespaces/{operator-session.js → session.js} +118 -30
- package/dist/namespaces/session.js.map +1 -0
- package/dist/namespaces/subdomains.d.ts +5 -5
- package/dist/namespaces/subdomains.d.ts.map +1 -1
- package/dist/namespaces/subdomains.js +6 -6
- package/dist/namespaces/subdomains.js.map +1 -1
- package/dist/namespaces/tier.d.ts +44 -21
- package/dist/namespaces/tier.d.ts.map +1 -1
- package/dist/namespaces/tier.js +7 -7
- package/dist/namespaces/tier.js.map +1 -1
- package/dist/namespaces/transfers.d.ts +119 -85
- package/dist/namespaces/transfers.d.ts.map +1 -1
- package/dist/namespaces/transfers.js +31 -38
- package/dist/namespaces/transfers.js.map +1 -1
- package/dist/namespaces/vouchers.d.ts +9 -10
- package/dist/namespaces/vouchers.d.ts.map +1 -1
- package/dist/namespaces/vouchers.js +4 -5
- package/dist/namespaces/vouchers.js.map +1 -1
- package/dist/namespaces/wallets.d.ts +72 -3
- package/dist/namespaces/wallets.d.ts.map +1 -1
- package/dist/namespaces/wallets.js +141 -3
- package/dist/namespaces/wallets.js.map +1 -1
- package/dist/namespaces/write-approval.d.ts +101 -0
- package/dist/namespaces/write-approval.d.ts.map +1 -0
- package/dist/namespaces/write-approval.js +62 -0
- package/dist/namespaces/write-approval.js.map +1 -0
- package/dist/node/actions-node.d.ts +3 -3
- package/dist/node/actions-node.d.ts.map +1 -1
- package/dist/node/actions-node.js +44 -44
- package/dist/node/actions-node.js.map +1 -1
- package/dist/node/assets-node.js +6 -6
- package/dist/node/assets-node.js.map +1 -1
- package/dist/node/{bearer-claim-key.d.ts → bearer-redeem-key.d.ts} +12 -12
- package/dist/node/bearer-redeem-key.d.ts.map +1 -0
- package/dist/node/{bearer-claim-key.js → bearer-redeem-key.js} +21 -21
- package/dist/node/bearer-redeem-key.js.map +1 -0
- package/dist/node/ci.d.ts +1 -1
- package/dist/node/ci.d.ts.map +1 -1
- package/dist/node/ci.js +6 -6
- package/dist/node/ci.js.map +1 -1
- package/dist/node/credentials.d.ts +18 -17
- package/dist/node/credentials.d.ts.map +1 -1
- package/dist/node/credentials.js +29 -28
- package/dist/node/credentials.js.map +1 -1
- package/dist/node/deploy-manifest.d.ts +3 -5
- package/dist/node/deploy-manifest.d.ts.map +1 -1
- package/dist/node/deploy-manifest.js +3 -12
- package/dist/node/deploy-manifest.js.map +1 -1
- package/dist/node/funding-recovery.d.ts +1 -1
- package/dist/node/funding-recovery.d.ts.map +1 -1
- package/dist/node/funding-recovery.js +1 -1
- package/dist/node/funding-recovery.js.map +1 -1
- package/dist/node/gitvault-apply.d.ts +1 -1
- package/dist/node/gitvault-apply.d.ts.map +1 -1
- package/dist/node/gitvault-apply.js +1 -1
- package/dist/node/gitvault-apply.js.map +1 -1
- package/dist/node/gitvault-handoff.d.ts +22 -22
- package/dist/node/gitvault-handoff.d.ts.map +1 -1
- package/dist/node/gitvault-handoff.js +45 -45
- package/dist/node/gitvault-handoff.js.map +1 -1
- package/dist/node/gitvault-prewarm.js +7 -7
- package/dist/node/gitvault-prewarm.js.map +1 -1
- package/dist/node/gitvault-restore.d.ts +1 -1
- package/dist/node/gitvault-restore.js +1 -1
- package/dist/node/index.d.ts +16 -16
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +15 -15
- package/dist/node/index.js.map +1 -1
- package/dist/node/manifest-export.js +2 -2
- package/dist/node/manifest-export.js.map +1 -1
- package/dist/node/org-adopt.d.ts +49 -0
- package/dist/node/org-adopt.d.ts.map +1 -0
- package/dist/node/org-adopt.js +77 -0
- package/dist/node/org-adopt.js.map +1 -0
- package/dist/node/paid-fetch.d.ts +7 -7
- package/dist/node/paid-fetch.d.ts.map +1 -1
- package/dist/node/paid-fetch.js +54 -54
- package/dist/node/paid-fetch.js.map +1 -1
- package/dist/node/preflight.d.ts +1 -1
- package/dist/node/preflight.d.ts.map +1 -1
- package/dist/node/preflight.js +1 -1
- package/dist/node/preflight.js.map +1 -1
- package/dist/node/sites-node.js +1 -1
- package/dist/node/sites-node.js.map +1 -1
- package/dist/node/source-scan.d.ts +1 -1
- package/dist/node/source-scan.js +2 -2
- package/dist/node/source-scan.js.map +1 -1
- package/dist/scoped.d.ts +11 -11
- package/dist/scoped.d.ts.map +1 -1
- package/dist/scoped.js +5 -5
- package/dist/scoped.js.map +1 -1
- package/package.json +1 -1
- package/core-dist/allowance.d.ts +0 -22
- package/core-dist/operator-session.d.ts +0 -80
- package/core-dist/operator-session.js +0 -154
- package/dist/namespaces/allowance.d.ts +0 -79
- package/dist/namespaces/allowance.d.ts.map +0 -1
- package/dist/namespaces/allowance.js +0 -149
- package/dist/namespaces/allowance.js.map +0 -1
- package/dist/namespaces/operator-session.d.ts.map +0 -1
- package/dist/namespaces/operator-session.js.map +0 -1
- package/dist/namespaces/operator.d.ts +0 -359
- package/dist/namespaces/operator.d.ts.map +0 -1
- package/dist/namespaces/operator.js +0 -272
- package/dist/namespaces/operator.js.map +0 -1
- package/dist/node/bearer-claim-key.d.ts.map +0 -1
- package/dist/node/bearer-claim-key.js.map +0 -1
- package/dist/node/operator-claim.d.ts +0 -49
- package/dist/node/operator-claim.d.ts.map +0 -1
- package/dist/node/operator-claim.js +0 -77
- package/dist/node/operator-claim.js.map +0 -1
|
@@ -97,7 +97,7 @@ function randomHandoffUuid() {
|
|
|
97
97
|
* The gateway names the vault by its three ids on the wire — `repo_id`,
|
|
98
98
|
* `org_id`, `project_id` (docs/style.md's API-boundary vocabulary) — on
|
|
99
99
|
* BOTH the handoff mint (`POST /gitvault/v1/vaults/:vault_id/handoffs`) and
|
|
100
|
-
* claim (`POST /gitvault/v1/handoffs/:handoff_id/
|
|
100
|
+
* claim (`POST /gitvault/v1/handoffs/:handoff_id/redeem`) responses. The SDK
|
|
101
101
|
* groups them under `vault` with the `organization_id` spelling every other
|
|
102
102
|
* SDK result uses. Neither response carries a slug-form address, so
|
|
103
103
|
* `address` is `null` unless the caller already knows one (a slug-form
|
|
@@ -106,7 +106,7 @@ function randomHandoffUuid() {
|
|
|
106
106
|
export function handoffVaultFromWire(wire, address = null) {
|
|
107
107
|
return { vault_id: wire.repo_id, address, organization_id: wire.org_id, project_id: wire.project_id };
|
|
108
108
|
}
|
|
109
|
-
/** The
|
|
109
|
+
/** The redeem response's `membership` block (`org_id` on the wire) in the SDK's `organization_id` spelling. Pure; exported for tests. */
|
|
110
110
|
export function handoffMembershipFromWire(wire) {
|
|
111
111
|
return { organization_id: wire.org_id, role: wire.role, status: wire.status };
|
|
112
112
|
}
|
|
@@ -1462,11 +1462,11 @@ export class Gitvault {
|
|
|
1462
1462
|
// nothing; pointing at it here sent users to a command that silently did
|
|
1463
1463
|
// not do what this line promised (dogfood #1, finding A).
|
|
1464
1464
|
if (!record)
|
|
1465
|
-
nextActions.push({ action: "allocate the project's vault", command: "run402 repos create --project <
|
|
1465
|
+
nextActions.push({ action: "allocate the project's vault", command: "run402 repos create --project <project_id>" });
|
|
1466
1466
|
else if (pending.length > 0)
|
|
1467
1467
|
nextActions.push({ action: `complete ${pending.length} unvaulted-override journal(s)`, command: "run402 repos snapshot" });
|
|
1468
1468
|
else if (record && !holdsRepoKey)
|
|
1469
|
-
nextActions.push({ action: "this machine holds no key for the vault — allocate resolves to the existing vault and is idempotent", command: "run402 repos create --project <
|
|
1469
|
+
nextActions.push({ action: "this machine holds no key for the vault — allocate resolves to the existing vault and is idempotent", command: "run402 repos create --project <project_id>" });
|
|
1470
1470
|
return {
|
|
1471
1471
|
repo_id: repoId,
|
|
1472
1472
|
project_id: options.project_id ?? record?.project_id ?? null,
|
|
@@ -1632,7 +1632,7 @@ export class Gitvault {
|
|
|
1632
1632
|
const identity = handle.keystore.readIdentity();
|
|
1633
1633
|
const localWriterKeyId = identity?.signing_fingerprint ?? null;
|
|
1634
1634
|
if (!localWriterKeyId || !repoFile.writer_set_pin?.writers.some((w) => w.writer_key_id === localWriterKeyId)) {
|
|
1635
|
-
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting a handoff requires an ACTIVE writer (design D4); an existing writer must add this key first (`run402
|
|
1635
|
+
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting a handoff requires an ACTIVE writer (design D4); an existing writer must add this key first (`run402 orgs members add` / repos access sync), or push once to reconcile if a pending admission already exists", "minting a handoff", { code: "GITVAULT_WRITER_NOT_ADMITTED" });
|
|
1636
1636
|
}
|
|
1637
1637
|
// The grant's `minted_role` must EXACTLY predict what the gateway's own
|
|
1638
1638
|
// role-attenuation (`services/gitvault/claims.ts mintHandoff`) will
|
|
@@ -1687,7 +1687,7 @@ export class Gitvault {
|
|
|
1687
1687
|
const { key, handoff_id_bytes, master_secret } = assembleHandoffKey(handoffId, randomBytes(32));
|
|
1688
1688
|
const secrets = deriveHandoffSecrets(handoff_id_bytes, master_secret);
|
|
1689
1689
|
// gitvault-multi-writer rev 47 (task 5.5, design D4) — the MINTER's own
|
|
1690
|
-
// writer key signs `writer_admission_grant`, authorizing whoever
|
|
1690
|
+
// writer key signs `writer_admission_grant`, authorizing whoever redeems
|
|
1691
1691
|
// this handoff to become a writer (D224/D225). This is a HARD
|
|
1692
1692
|
// requirement distinct from the earlier `writer_set_pin` presence
|
|
1693
1693
|
// check: that check only proves the fingerprint is admitted, not that
|
|
@@ -1697,8 +1697,8 @@ export class Gitvault {
|
|
|
1697
1697
|
//
|
|
1698
1698
|
// Built BEFORE the envelope is sealed — design D4's "no hash cycle:
|
|
1699
1699
|
// grant first, then seal" — so the v2 envelope below can embed this
|
|
1700
|
-
// grant's own stored-bytes SHA-256, letting the
|
|
1701
|
-
// (task 5.6) cross-check the
|
|
1700
|
+
// grant's own stored-bytes SHA-256, letting the redeemer's `resume()`
|
|
1701
|
+
// (task 5.6) cross-check the redeem response's grant against what this
|
|
1702
1702
|
// call actually sealed, independent of anything the gateway could alter.
|
|
1703
1703
|
const signingKeypair = handle.keystore.signingKeypair(identity); // non-null: the writer precheck above already required identity.signing_fingerprint
|
|
1704
1704
|
if (!signingKeypair) {
|
|
@@ -1731,7 +1731,7 @@ export class Gitvault {
|
|
|
1731
1731
|
// handoff minted AFTER an epoch rotation must let the recipient open
|
|
1732
1732
|
// the pre-rotation generations too ("membership grants FULL history"),
|
|
1733
1733
|
// and no rotation ever re-wraps old epochs to a principal that did not
|
|
1734
|
-
// exist then. Found live: a recipient
|
|
1734
|
+
// exist then. Found live: a recipient redeemed a post-removal handoff
|
|
1735
1735
|
// and its clone died GITVAULT_EPOCH_NOT_OPENABLE at generation 1.
|
|
1736
1736
|
epoch_keys: { ...(repoFile.epoch_keys ?? {}), [repoFile.epoch]: repoFile.k_repo_hex },
|
|
1737
1737
|
checkpoint: { generation: pushResult.generation, commit_oid: snapshot.oid },
|
|
@@ -1788,12 +1788,12 @@ export class Gitvault {
|
|
|
1788
1788
|
next_actions: response.next_actions ?? [],
|
|
1789
1789
|
};
|
|
1790
1790
|
}
|
|
1791
|
-
/** List a vault's handoffs (ids, kind, state, role, expiry,
|
|
1791
|
+
/** List a vault's handoffs (ids, kind, state, role, expiry, redeemed_by — never the hash or envelope). */
|
|
1792
1792
|
async listHandoffs(options) {
|
|
1793
1793
|
const repoId = await this.#resolveRepoId(options);
|
|
1794
1794
|
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/handoffs`, { context: "listing handoffs" });
|
|
1795
1795
|
}
|
|
1796
|
-
/** Revoke a handoff (idempotent — a second revoke of an already-revoked/
|
|
1796
|
+
/** Revoke a handoff (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
|
|
1797
1797
|
async revokeHandoff(handoffId, options) {
|
|
1798
1798
|
const repoId = await this.#resolveRepoId(options);
|
|
1799
1799
|
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/handoffs/${encodeURIComponent(handoffId)}`, { method: "DELETE", context: "revoking a handoff" });
|
|
@@ -1807,9 +1807,9 @@ export class Gitvault {
|
|
|
1807
1807
|
* `inviter_presence_id`; a registration failure is reported and the mint
|
|
1808
1808
|
* proceeds without one), seal the vault's current epoch key under a fresh
|
|
1809
1809
|
* `wrap_key`, mint through the gateway at `developer` (or `role`,
|
|
1810
|
-
* attenuated to never exceed the minter's own), and post ONE
|
|
1810
|
+
* attenuated to never exceed the minter's own), and post ONE message from
|
|
1811
1811
|
* the inviter's presence naming the checkpoint and the invite id (never
|
|
1812
|
-
* the key) — a
|
|
1812
|
+
* the key) — a message-post failure is reported in `room_message` and never
|
|
1813
1813
|
* voids the mint (design D4). The assembled `kgi1_…` key is returned
|
|
1814
1814
|
* exactly ONCE — nothing here or downstream persists it. Creating an
|
|
1815
1815
|
* invite never touches the inviter's worktree, index, branch, refs, or
|
|
@@ -1914,7 +1914,7 @@ export class Gitvault {
|
|
|
1914
1914
|
const secrets = deriveInviteSecrets(invite_id_bytes, master_secret);
|
|
1915
1915
|
// gitvault-multi-writer rev 47 (kygit-invite design D4) — the MINTER's
|
|
1916
1916
|
// own writer key signs `writer_admission_grant`, authorizing whoever
|
|
1917
|
-
//
|
|
1917
|
+
// redeems this invite to become a writer under its OWN key. Built and
|
|
1918
1918
|
// SIGNED BEFORE the envelope is sealed ("no hash cycle: grant first,
|
|
1919
1919
|
// then seal") so the v2 envelope below can embed the grant's own
|
|
1920
1920
|
// stored-bytes SHA-256 and `join()` can cross-check the claim
|
|
@@ -1993,10 +1993,10 @@ export class Gitvault {
|
|
|
1993
1993
|
if (response.writer_admission_grant_sha256 !== grantSha256Local) {
|
|
1994
1994
|
throw new LocalError(`the gateway echoed a writer_admission_grant_sha256 (${response.writer_admission_grant_sha256}) that does not match what this call sent (${grantSha256Local}) — the stored grant may not be the one this call signed; do not distribute this invite key`, "minting an invite key", { code: "INVITE_MINT_GRANT_MISMATCH", details: { expected: grantSha256Local, received: response.writer_admission_grant_sha256 } });
|
|
1995
1995
|
}
|
|
1996
|
-
// design D4: post the
|
|
1997
|
-
// leave no orphan message, and a
|
|
1996
|
+
// design D4: post the message AFTER the mint succeeds — a mint refusal must
|
|
1997
|
+
// leave no orphan message, and a message-post failure never voids a mint
|
|
1998
1998
|
// the caller may already have copied the key from.
|
|
1999
|
-
let
|
|
1999
|
+
let roomMessage = { posted: false, reason: "inviter presence was not registered" };
|
|
2000
2000
|
if (inviterPresence) {
|
|
2001
2001
|
const receiptShort = snapshot.oid.slice(0, 12);
|
|
2002
2002
|
const inviteShort = response.invite_id.slice(0, 8);
|
|
@@ -2007,10 +2007,10 @@ export class Gitvault {
|
|
|
2007
2007
|
...(options.sessionKey !== undefined ? { sessionKey: options.sessionKey } : {}),
|
|
2008
2008
|
idempotencyKey: `invite:${response.invite_id}:minted`,
|
|
2009
2009
|
});
|
|
2010
|
-
|
|
2010
|
+
roomMessage = { posted: true, message_id: sent.message_id, cursor: sent.cursor };
|
|
2011
2011
|
}
|
|
2012
2012
|
catch (e) {
|
|
2013
|
-
|
|
2013
|
+
roomMessage = { posted: false, reason: e instanceof Error ? e.message : String(e) };
|
|
2014
2014
|
}
|
|
2015
2015
|
}
|
|
2016
2016
|
const nextActions = [...(response.next_actions ?? [])];
|
|
@@ -2023,7 +2023,7 @@ export class Gitvault {
|
|
|
2023
2023
|
nextActions.push({
|
|
2024
2024
|
type: "join_invite",
|
|
2025
2025
|
command: door === "kygit" ? `kygit join ${key}` : `run402 repos join ${key}`,
|
|
2026
|
-
why: "Run this on the other agent's machine to
|
|
2026
|
+
why: "Run this on the other agent's machine to redeem the invite.",
|
|
2027
2027
|
safe_to_auto_execute: false,
|
|
2028
2028
|
});
|
|
2029
2029
|
}
|
|
@@ -2047,42 +2047,42 @@ export class Gitvault {
|
|
|
2047
2047
|
},
|
|
2048
2048
|
snapshot,
|
|
2049
2049
|
inviter_presence: inviterPresenceReport,
|
|
2050
|
-
|
|
2050
|
+
room_message: roomMessage,
|
|
2051
2051
|
warnings: response.warnings ?? [],
|
|
2052
2052
|
next_actions: nextActions,
|
|
2053
2053
|
};
|
|
2054
2054
|
}
|
|
2055
|
-
/** List a vault's invites (ids, kind, state, role, room, expiry,
|
|
2055
|
+
/** List a vault's invites (ids, kind, state, role, room, expiry, redeemed_by — never the hash or envelope). */
|
|
2056
2056
|
async listInvites(options) {
|
|
2057
2057
|
const repoId = await this.#resolveRepoId(options);
|
|
2058
2058
|
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/invites`, { context: "listing invites" });
|
|
2059
2059
|
}
|
|
2060
|
-
/** Revoke an invite (idempotent — a second revoke of an already-revoked/
|
|
2060
|
+
/** Revoke an invite (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
|
|
2061
2061
|
async revokeInvite(inviteId, options) {
|
|
2062
2062
|
const repoId = await this.#resolveRepoId(options);
|
|
2063
2063
|
return this.#client.request(`/gitvault/v1/vaults/${encodeURIComponent(repoId)}/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE", context: "revoking an invite" });
|
|
2064
2064
|
}
|
|
2065
2065
|
/**
|
|
2066
|
-
* Create this machine's wallet (the
|
|
2066
|
+
* Create this machine's wallet (the wallet file) when the credentials
|
|
2067
2067
|
* provider supports one and none exists yet. A keypair on disk — no
|
|
2068
2068
|
* faucet, no tier, no payment. Pure no-op for a provider without the
|
|
2069
|
-
* optional
|
|
2069
|
+
* optional wallet methods, or when a wallet already exists.
|
|
2070
2070
|
*/
|
|
2071
2071
|
async #ensureLocalWallet(onLine) {
|
|
2072
2072
|
const creds = this.#client.credentials;
|
|
2073
|
-
if (!creds.
|
|
2073
|
+
if (!creds.readWallet || !creds.createWallet || !creds.saveWallet)
|
|
2074
2074
|
return;
|
|
2075
|
-
if (await creds.
|
|
2075
|
+
if (await creds.readWallet())
|
|
2076
2076
|
return;
|
|
2077
|
-
const created = await creds.
|
|
2078
|
-
await creds.
|
|
2077
|
+
const created = await creds.createWallet();
|
|
2078
|
+
await creds.saveWallet(created);
|
|
2079
2079
|
onLine?.(`wallet created for this machine: ${created.address} (a keypair on disk — no payment)`);
|
|
2080
2080
|
}
|
|
2081
2081
|
/**
|
|
2082
2082
|
* Resume a Handoff Key: parse → ensure this machine has a wallet (the
|
|
2083
|
-
* claim is bare SIWX, so on a fresh machine the
|
|
2083
|
+
* claim is bare SIWX, so on a fresh machine the wallet file is created
|
|
2084
2084
|
* here — a keypair on disk, no faucet, no tier, no payment, ever; design
|
|
2085
|
-
* D5) →
|
|
2085
|
+
* D5) → redeem → open the
|
|
2086
2086
|
* sealed envelope → write the repo file to the keystore BEFORE touching
|
|
2087
2087
|
* disk → clone at the base HEAD → `git stash apply --index` → local
|
|
2088
2088
|
* git-config pins only → the session-start reconcile so a principal
|
|
@@ -2091,7 +2091,7 @@ export class Gitvault {
|
|
|
2091
2091
|
/**
|
|
2092
2092
|
* gitvault-multi-writer rev 47 (task 5.6, design D4/D5) — order: parse →
|
|
2093
2093
|
* ensure wallet → ensure identity → derive auth/wrap/admission → build
|
|
2094
|
-
* acceptance →
|
|
2094
|
+
* acceptance → redeem → verify grant → open envelope → check the grant
|
|
2095
2095
|
* hash → persist repo file with `pending_writer_admission` → clone →
|
|
2096
2096
|
* verify chain → submit the ref-neutral activation head → reconcile
|
|
2097
2097
|
* principal envelope → apply the checkpoint. Reported as
|
|
@@ -2102,7 +2102,7 @@ export class Gitvault {
|
|
|
2102
2102
|
* the acceptance is derived fresh every call from the (deterministic)
|
|
2103
2103
|
* admission seed + this checkout's own identity, so re-running this
|
|
2104
2104
|
* ENTIRE method after a crash anywhere before activation lands is safe —
|
|
2105
|
-
* the
|
|
2105
|
+
* the redeem route is itself idempotent (same-redeemer replay returns the
|
|
2106
2106
|
* SAME grant). The one step that is NOT safely repeatable is submitting
|
|
2107
2107
|
* the activation head a second time (the chain burns `handoff_id`
|
|
2108
2108
|
* single-use) — `submitWriterActivationHead` handles that by checking
|
|
@@ -2117,42 +2117,42 @@ export class Gitvault {
|
|
|
2117
2117
|
const { cloneGitvaultRemote, applyHandoffCheckpoint, resolveResumeTargetDir, readGitCommitMessage, excludeMessagingCacheFromGit } = restore;
|
|
2118
2118
|
// parse
|
|
2119
2119
|
const parsed = parseHandoffKey(options.key);
|
|
2120
|
-
// ensure wallet — A fresh machine has no wallet, and the
|
|
2120
|
+
// ensure wallet — A fresh machine has no wallet, and the redeem route
|
|
2121
2121
|
// accepts ONLY a SIWX wallet signature (a control-plane session,
|
|
2122
|
-
// delegate, or service key is refused
|
|
2123
|
-
// the keystore key the
|
|
2124
|
-
// real key-holder). Nothing upstream creates the
|
|
2122
|
+
// delegate, or service key is refused HANDOFF_REDEEM_REQUIRES_WALLET —
|
|
2123
|
+
// the keystore key the redemption publishes is what makes the recipient a
|
|
2124
|
+
// real key-holder). Nothing upstream creates the wallet for an
|
|
2125
2125
|
// unpaid request, so `resume` does it here, exactly as `repos resume
|
|
2126
|
-
// --help` promises: a keypair written to the
|
|
2126
|
+
// --help` promises: a keypair written to the wallet file, no
|
|
2127
2127
|
// faucet, no tier, no payment. Without it a bare machine answers
|
|
2128
2128
|
// AUTH_REQUIRED. A provider without
|
|
2129
|
-
//
|
|
2129
|
+
// wallet support (an isomorphic one) is left alone — it
|
|
2130
2130
|
// authenticates however it authenticates.
|
|
2131
2131
|
await this.#ensureLocalWallet(options.onLine);
|
|
2132
|
-
// ensure identity — BEFORE
|
|
2133
|
-
// checkout's own signing key, which the
|
|
2132
|
+
// ensure identity — BEFORE redeem (D5): the acceptance below needs this
|
|
2133
|
+
// checkout's own signing key, which the redeem REQUEST body carries.
|
|
2134
2134
|
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
2135
2135
|
const identity = keystore.ensureIdentity();
|
|
2136
2136
|
const signingKeypair = keystore.signingKeypair(identity);
|
|
2137
2137
|
if (!signingKeypair) {
|
|
2138
2138
|
throw new LocalError("this keystore has no local signing seed — resuming a gitvault-multi-writer handoff requires signing writer_acceptance with a full Ed25519 seed, which a read-only recovery identity does not hold", "resuming a handoff", { code: "VAULT_UNRECOVERABLE" });
|
|
2139
2139
|
}
|
|
2140
|
-
const
|
|
2140
|
+
const redeemerEncryptionPubkeyRaw = fromBase64url(identity.encryption_pubkey, "identity.encryption_pubkey");
|
|
2141
2141
|
// derive auth/wrap/admission
|
|
2142
2142
|
const secrets = deriveHandoffSecrets(parsed.handoff_id_bytes, parsed.master_secret);
|
|
2143
2143
|
const admissionSeed = deriveWriterAdmissionSeed(parsed.handoff_id_bytes, parsed.master_secret);
|
|
2144
|
-
// build acceptance — before
|
|
2145
|
-
//
|
|
2144
|
+
// build acceptance — before redeem: design D4's own point is that the
|
|
2145
|
+
// redeemer can construct BOTH signatures before ever seeing the stored
|
|
2146
2146
|
// grant, since `handoff_id`/`auth_hash` are already independently known.
|
|
2147
2147
|
const acceptance = buildWriterAcceptance({
|
|
2148
2148
|
handoff_id: parsed.handoff_id,
|
|
2149
2149
|
auth_hash: secrets.auth_hash_hex,
|
|
2150
2150
|
admission_seed: admissionSeed,
|
|
2151
|
-
|
|
2152
|
-
|
|
2151
|
+
redeemer_signing_seed: signingKeypair.seed,
|
|
2152
|
+
redeemer_encryption_pubkey_raw: redeemerEncryptionPubkeyRaw,
|
|
2153
2153
|
});
|
|
2154
2154
|
// claim
|
|
2155
|
-
const
|
|
2155
|
+
const redeem = await this.#client.request(`/gitvault/v1/handoffs/${encodeURIComponent(parsed.handoff_id)}/redeem`, {
|
|
2156
2156
|
method: "POST",
|
|
2157
2157
|
// Base64url, per the documented wire contract (openapi: "Base64url — the
|
|
2158
2158
|
// HKDF-derived auth_secret half of the parsed kgh1_ key"). The gateway
|
|
@@ -2161,12 +2161,12 @@ export class Gitvault {
|
|
|
2161
2161
|
// claim answers HANDOFF_KEY_INVALID. Each side's own tests can pass
|
|
2162
2162
|
// independently; only the cross-side vector below catches this class.
|
|
2163
2163
|
body: { auth_secret: toBase64url(secrets.auth_secret), writer_acceptance: toBase64url(jcs(acceptance)) },
|
|
2164
|
-
context: "
|
|
2164
|
+
context: "redeeming a handoff key",
|
|
2165
2165
|
});
|
|
2166
2166
|
// The claim names the vault by id only (no slug-form address rides the
|
|
2167
2167
|
// wire), so the default target directory falls back to the vault id —
|
|
2168
2168
|
// `--to <dir>` names it explicitly.
|
|
2169
|
-
const vault = handoffVaultFromWire(
|
|
2169
|
+
const vault = handoffVaultFromWire(redeem);
|
|
2170
2170
|
// verify grant — a light structural/binding check, NOT the full
|
|
2171
2171
|
// cryptographic verification (which needs the vault's writer set to
|
|
2172
2172
|
// resolve the grantor's pubkey, and therefore waits until the chain is
|
|
@@ -2174,37 +2174,37 @@ export class Gitvault {
|
|
|
2174
2174
|
// just not reachable from here yet).
|
|
2175
2175
|
let grantBytes;
|
|
2176
2176
|
try {
|
|
2177
|
-
grantBytes = fromBase64url(
|
|
2177
|
+
grantBytes = fromBase64url(redeem.writer_admission_grant, "writer_admission_grant");
|
|
2178
2178
|
}
|
|
2179
2179
|
catch {
|
|
2180
|
-
throw new LocalError("the
|
|
2180
|
+
throw new LocalError("the redeem response's writer_admission_grant is not valid base64url", "resuming a handoff", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2181
2181
|
}
|
|
2182
2182
|
let grant;
|
|
2183
2183
|
try {
|
|
2184
2184
|
grant = JSON.parse(new TextDecoder().decode(grantBytes));
|
|
2185
2185
|
}
|
|
2186
2186
|
catch {
|
|
2187
|
-
throw new LocalError("the
|
|
2187
|
+
throw new LocalError("the redeem response's writer_admission_grant does not decode to valid JSON", "resuming a handoff", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2188
2188
|
}
|
|
2189
2189
|
if (grant.handoff_id !== parsed.handoff_id || grant.auth_hash !== secrets.auth_hash_hex || grant.repo_id !== vault.vault_id) {
|
|
2190
|
-
throw new LocalError("the
|
|
2190
|
+
throw new LocalError("the redeem response's writer_admission_grant does not bind this handoff — refusing", "resuming a handoff", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2191
2191
|
}
|
|
2192
2192
|
const grantSha256 = sha256Hex(grantBytes);
|
|
2193
2193
|
// open envelope — v2 (task 5.4): the v2 payload's own
|
|
2194
2194
|
// writer_admission_grant_sha256 is what the next step cross-checks;
|
|
2195
2195
|
// opening under the SAME v1/v2-requiring rule that refuses a
|
|
2196
2196
|
// pre-gitvault-multi-writer mint's envelope outright.
|
|
2197
|
-
const payload = openHandoffEnvelopeV2(parsed.handoff_id_bytes, secrets.wrap_key,
|
|
2197
|
+
const payload = openHandoffEnvelopeV2(parsed.handoff_id_bytes, secrets.wrap_key, redeem.sealed_envelope, redeem.envelope_kind);
|
|
2198
2198
|
if (payload.repo_id !== vault.vault_id) {
|
|
2199
|
-
throw new LocalError("the opened envelope's repo_id does not match the
|
|
2199
|
+
throw new LocalError("the opened envelope's repo_id does not match the redeem response's vault — refusing", "resuming a handoff", { code: "HANDOFF_ENVELOPE_INVALID" });
|
|
2200
2200
|
}
|
|
2201
2201
|
// check the grant hash — design D4's own integrity binding: the SEALED
|
|
2202
2202
|
// envelope (wrap_key-authenticated, the gateway never holds wrap_key)
|
|
2203
|
-
// names the grant it was minted alongside; if the
|
|
2204
|
-
// grant doesn't match, something between mint and
|
|
2203
|
+
// names the grant it was minted alongside; if the redeem response's
|
|
2204
|
+
// grant doesn't match, something between mint and redeem substituted a
|
|
2205
2205
|
// different one — refuse before spending anything on this handoff.
|
|
2206
2206
|
if (payload.writer_admission_grant_sha256 !== grantSha256) {
|
|
2207
|
-
throw new LocalError(`the
|
|
2207
|
+
throw new LocalError(`the redeem response's writer_admission_grant (sha256 ${grantSha256}) does not match the hash sealed into the envelope at mint time (${payload.writer_admission_grant_sha256}) — refusing to activate under a substituted grant`, "resuming a handoff", { code: "HANDOFF_REDEEM_WRITER_KEY_MISMATCH", details: { expected: payload.writer_admission_grant_sha256, received: grantSha256 } });
|
|
2208
2208
|
}
|
|
2209
2209
|
// Genesis must be pinned before ANY materialize call can succeed
|
|
2210
2210
|
// (`GitvaultVault.genesis()` requires a keystore repo file — this is
|
|
@@ -2224,7 +2224,7 @@ export class Gitvault {
|
|
|
2224
2224
|
// persist repo file with pending_writer_admission — BEFORE clone/chain
|
|
2225
2225
|
// work below (design D10's "write to keystore before touching disk",
|
|
2226
2226
|
// extended): a crash from here on leaves a durable record of the
|
|
2227
|
-
// ALREADY-VERIFIED grant, so a retry never needs to re-
|
|
2227
|
+
// ALREADY-VERIFIED grant, so a retry never needs to re-redeem (the
|
|
2228
2228
|
// acceptance is trivially re-derivable from data already in hand, per
|
|
2229
2229
|
// this method's own doc comment above; only the grant is not).
|
|
2230
2230
|
const myWriterKeyId = identity.signing_fingerprint;
|
|
@@ -2275,8 +2275,8 @@ export class Gitvault {
|
|
|
2275
2275
|
// explicit step AFTER activation (D5's ordering), not run implicitly
|
|
2276
2276
|
// and possibly twice.
|
|
2277
2277
|
const handle = await this.open({ repo_id: vault.vault_id, repo_dir: targetDir, keystore_root: options.keystore_root, reconcile: "forbidden" });
|
|
2278
|
-
// `added_writer.principal_id` names the
|
|
2279
|
-
// principal — the
|
|
2278
|
+
// `added_writer.principal_id` names the redeemer's OWN control-plane
|
|
2279
|
+
// principal — the redeem response never carries it (its `membership`
|
|
2280
2280
|
// block names the ORG, not the principal), so resolve it fresh here,
|
|
2281
2281
|
// the same one-call pattern `handoff()` already uses for its own role
|
|
2282
2282
|
// resolution (task 5.5).
|
|
@@ -2305,9 +2305,9 @@ export class Gitvault {
|
|
|
2305
2305
|
// leaves the working tree untouched (freshly cloned, nothing stashed),
|
|
2306
2306
|
// the cleanest possible state to retry `resume()` from.
|
|
2307
2307
|
const restored = await applyHandoffCheckpoint({ dir: targetDir, stash_oid: payload.checkpoint.commit_oid });
|
|
2308
|
-
const senderIsOwner =
|
|
2309
|
-
const nextActions = [...(
|
|
2310
|
-
if (
|
|
2308
|
+
const senderIsOwner = redeem.membership.role === "owner";
|
|
2309
|
+
const nextActions = [...(redeem.next_actions ?? [])];
|
|
2310
|
+
if (redeem.kind === "handoff" && senderIsOwner && !nextActions.some((a) => a.type === "remove_member")) {
|
|
2311
2311
|
nextActions.push({
|
|
2312
2312
|
type: "remove_member",
|
|
2313
2313
|
why: "The previous agent is still an owner; if its environment is gone for good, remove it.",
|
|
@@ -2329,15 +2329,15 @@ export class Gitvault {
|
|
|
2329
2329
|
note = null;
|
|
2330
2330
|
}
|
|
2331
2331
|
return {
|
|
2332
|
-
handoff_id:
|
|
2333
|
-
kind:
|
|
2334
|
-
deduplicated:
|
|
2332
|
+
handoff_id: redeem.handoff_id,
|
|
2333
|
+
kind: redeem.kind,
|
|
2334
|
+
deduplicated: redeem.deduplicated,
|
|
2335
2335
|
note,
|
|
2336
2336
|
note_raw: noteRaw,
|
|
2337
2337
|
restored: { dir: targetDir, branch: restored.branch, base_head_oid: restored.base_head_oid, stash_oid: restored.stash_oid },
|
|
2338
|
-
membership: handoffMembershipFromWire(
|
|
2339
|
-
members:
|
|
2340
|
-
expires_at:
|
|
2338
|
+
membership: handoffMembershipFromWire(redeem.membership),
|
|
2339
|
+
members: redeem.members ?? [],
|
|
2340
|
+
expires_at: redeem.expires_at,
|
|
2341
2341
|
writer_activation: { outcome: "active", writer_key_id: myWriterKeyId, generation: activationGeneration },
|
|
2342
2342
|
reconcile_recipients: reconcile,
|
|
2343
2343
|
next_actions: nextActions,
|
|
@@ -2347,9 +2347,9 @@ export class Gitvault {
|
|
|
2347
2347
|
* Join an Invite Key: parse (refusing a `kgh1_` handoff key by name,
|
|
2348
2348
|
* pointing at `resume` — design D9) → ensure this machine has a wallet
|
|
2349
2349
|
* (design D5, mirroring {@link Gitvault.resume}'s own bare-wallet
|
|
2350
|
-
* backstop; the CALLER folds the fuller cold-start chain —
|
|
2350
|
+
* backstop; the CALLER folds the fuller cold-start chain — wallet,
|
|
2351
2351
|
* faucet, one x402 prototype payment — before invoking this, never
|
|
2352
|
-
* blocking the
|
|
2352
|
+
* blocking the redemption itself) → redeem → open the sealed envelope → write
|
|
2353
2353
|
* the repo file to the keystore BEFORE touching disk → clone at the base
|
|
2354
2354
|
* HEAD → `git stash apply --index` → local git-config pins (including
|
|
2355
2355
|
* `r402.room` set to the invite's OWN room, never just the project id) →
|
|
@@ -2373,10 +2373,10 @@ export class Gitvault {
|
|
|
2373
2373
|
// Bare-wallet backstop, exactly like `resume` (design D5) — the claim
|
|
2374
2374
|
// route accepts ONLY a SIWX wallet signature; the fuller cold-start
|
|
2375
2375
|
// chain (faucet + one x402 prototype payment) is the CALLER's to fold
|
|
2376
|
-
// before this call, and never blocks the
|
|
2376
|
+
// before this call, and never blocks the redemption either way.
|
|
2377
2377
|
await this.#ensureLocalWallet(options.onLine);
|
|
2378
|
-
// ensure identity BEFORE the
|
|
2379
|
-
// below needs THIS checkout's own signing key, and the
|
|
2378
|
+
// ensure identity BEFORE the redemption (design D5): the `writer_acceptance`
|
|
2379
|
+
// below needs THIS checkout's own signing key, and the redeem REQUEST
|
|
2380
2380
|
// body carries it. Nothing about the joiner's key is copied from the
|
|
2381
2381
|
// inviter — the joiner generates it and pushes under it.
|
|
2382
2382
|
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
@@ -2385,7 +2385,7 @@ export class Gitvault {
|
|
|
2385
2385
|
if (!signingKeypair) {
|
|
2386
2386
|
throw new LocalError("this keystore has no local signing seed — joining an invite requires signing writer_acceptance with a full Ed25519 seed, which a read-only recovery identity does not hold", "joining an invite", { code: "VAULT_UNRECOVERABLE" });
|
|
2387
2387
|
}
|
|
2388
|
-
const
|
|
2388
|
+
const redeemerEncryptionPubkeyRaw = fromBase64url(identity.encryption_pubkey, "identity.encryption_pubkey");
|
|
2389
2389
|
// Both signatures are constructible BEFORE the stored grant is ever
|
|
2390
2390
|
// seen — the invite id and `auth_hash` are already independently known
|
|
2391
2391
|
// (design D4). The protocol field is spelled `handoff_id` and the
|
|
@@ -2396,10 +2396,10 @@ export class Gitvault {
|
|
|
2396
2396
|
handoff_id: parsed.invite_id,
|
|
2397
2397
|
auth_hash: secrets.auth_hash_hex,
|
|
2398
2398
|
admission_seed: admissionSeed,
|
|
2399
|
-
|
|
2400
|
-
|
|
2399
|
+
redeemer_signing_seed: signingKeypair.seed,
|
|
2400
|
+
redeemer_encryption_pubkey_raw: redeemerEncryptionPubkeyRaw,
|
|
2401
2401
|
});
|
|
2402
|
-
const
|
|
2402
|
+
const redeem = await this.#client.request(`/gitvault/v1/invites/${encodeURIComponent(parsed.invite_id)}/redeem`, {
|
|
2403
2403
|
method: "POST",
|
|
2404
2404
|
// Base64url, per the documented wire contract — the gateway decodes
|
|
2405
2405
|
// base64/base64url and substitutes 32 zero bytes for anything else,
|
|
@@ -2407,39 +2407,39 @@ export class Gitvault {
|
|
|
2407
2407
|
// claim answers INVITE_KEY_INVALID. Each side's own tests can pass
|
|
2408
2408
|
// independently; only a cross-side vector catches this class.
|
|
2409
2409
|
body: { auth_secret: toBase64url(secrets.auth_secret), writer_acceptance: toBase64url(jcs(acceptance)) },
|
|
2410
|
-
context: "
|
|
2410
|
+
context: "redeeming an invite key",
|
|
2411
2411
|
});
|
|
2412
2412
|
// The claim names the vault by id only (no slug-form address rides the
|
|
2413
2413
|
// wire), so the default target directory falls back to the vault id —
|
|
2414
2414
|
// `--to <dir>` names it explicitly.
|
|
2415
|
-
const vault = handoffVaultFromWire(
|
|
2415
|
+
const vault = handoffVaultFromWire(redeem);
|
|
2416
2416
|
// verify grant — a light structural/binding check, NOT the full
|
|
2417
2417
|
// cryptographic verification (which needs the vault's writer set to
|
|
2418
2418
|
// resolve the grantor's pubkey, and therefore waits until the chain is
|
|
2419
2419
|
// walked below — protocol §4.17's own admission ordering).
|
|
2420
2420
|
let grantBytes;
|
|
2421
2421
|
try {
|
|
2422
|
-
grantBytes = fromBase64url(
|
|
2422
|
+
grantBytes = fromBase64url(redeem.writer_admission_grant, "writer_admission_grant");
|
|
2423
2423
|
}
|
|
2424
2424
|
catch {
|
|
2425
|
-
throw new LocalError("the
|
|
2425
|
+
throw new LocalError("the redeem response's writer_admission_grant is not valid base64url", "joining an invite", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2426
2426
|
}
|
|
2427
2427
|
let grant;
|
|
2428
2428
|
try {
|
|
2429
2429
|
grant = JSON.parse(new TextDecoder().decode(grantBytes));
|
|
2430
2430
|
}
|
|
2431
2431
|
catch {
|
|
2432
|
-
throw new LocalError("the
|
|
2432
|
+
throw new LocalError("the redeem response's writer_admission_grant does not decode to valid JSON", "joining an invite", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2433
2433
|
}
|
|
2434
|
-
// `handoff_id` is the frozen protocol spelling of the
|
|
2434
|
+
// `handoff_id` is the frozen protocol spelling of the key id (D11) —
|
|
2435
2435
|
// for an invite it carries the INVITE id.
|
|
2436
2436
|
if (grant.handoff_id !== parsed.invite_id || grant.auth_hash !== secrets.auth_hash_hex || grant.repo_id !== vault.vault_id) {
|
|
2437
|
-
throw new LocalError("the
|
|
2437
|
+
throw new LocalError("the redeem response's writer_admission_grant does not bind this invite — refusing", "joining an invite", { code: "VALIDATION_FAILED", details: { field: "writer_admission_grant" } });
|
|
2438
2438
|
}
|
|
2439
2439
|
const grantSha256 = sha256Hex(grantBytes);
|
|
2440
|
-
const payload = openInviteEnvelope(parsed.invite_id_bytes, secrets.wrap_key,
|
|
2440
|
+
const payload = openInviteEnvelope(parsed.invite_id_bytes, secrets.wrap_key, redeem.sealed_envelope, redeem.envelope_kind);
|
|
2441
2441
|
if (payload.repo_id !== vault.vault_id) {
|
|
2442
|
-
throw new LocalError("the opened envelope's repo_id does not match the
|
|
2442
|
+
throw new LocalError("the opened envelope's repo_id does not match the redeem response's vault — refusing", "joining an invite", { code: "INVITE_ENVELOPE_INVALID" });
|
|
2443
2443
|
}
|
|
2444
2444
|
// design D5's own integrity binding: the SEALED envelope
|
|
2445
2445
|
// (wrap_key-authenticated, and the gateway never holds wrap_key) names
|
|
@@ -2447,7 +2447,7 @@ export class Gitvault {
|
|
|
2447
2447
|
// this comparison LOCALLY, before anything is written to disk or the
|
|
2448
2448
|
// keystore.
|
|
2449
2449
|
if (payload.writer_admission_grant_sha256 !== grantSha256) {
|
|
2450
|
-
throw new LocalError(`the
|
|
2450
|
+
throw new LocalError(`the redeem response's writer_admission_grant (sha256 ${grantSha256}) does not match the hash sealed into the envelope at mint time (${payload.writer_admission_grant_sha256}) — refusing to activate under a substituted grant`, "joining an invite", { code: "INVITE_REDEEM_WRITER_KEY_MISMATCH", details: { expected: payload.writer_admission_grant_sha256, received: grantSha256 } });
|
|
2451
2451
|
}
|
|
2452
2452
|
// Genesis must be pinned before ANY materialize call can succeed — the
|
|
2453
2453
|
// one read that happens BEFORE a keystore repo file exists, via the
|
|
@@ -2466,7 +2466,7 @@ export class Gitvault {
|
|
|
2466
2466
|
// carrying EVERY epoch key the envelope holds so a joiner arriving after
|
|
2467
2467
|
// a rotation can still open the pre-rotation generations, and the
|
|
2468
2468
|
// already-verified grant as `pending_writer_admission` so a crash from
|
|
2469
|
-
// here on never needs to re-
|
|
2469
|
+
// here on never needs to re-redeem (the acceptance is trivially
|
|
2470
2470
|
// re-derivable from data already in hand; only the grant is not).
|
|
2471
2471
|
const myWriterKeyId = identity.signing_fingerprint;
|
|
2472
2472
|
keystore.saveRepo({
|
|
@@ -2489,7 +2489,7 @@ export class Gitvault {
|
|
|
2489
2489
|
// The ROW's room key — a named room, or the project id when the mint
|
|
2490
2490
|
// omitted one — is what makes `messages wait` in the joined checkout
|
|
2491
2491
|
// address the right room with zero flags (design D5).
|
|
2492
|
-
const roomKey =
|
|
2492
|
+
const roomKey = redeem.room?.room_key ?? vault.project_id ?? "";
|
|
2493
2493
|
// Local-only pins (design D5) — never a worktree file, never the global
|
|
2494
2494
|
// active project. Reuses the SAME pin-writer every other gitvault
|
|
2495
2495
|
// resolution path uses, this time with the invite's OWN room key.
|
|
@@ -2518,7 +2518,7 @@ export class Gitvault {
|
|
|
2518
2518
|
// SAME `add_writer_key` door `resume` drives — so the joiner's first
|
|
2519
2519
|
// `git push` is an ordinary push under its OWN key, not a
|
|
2520
2520
|
// `GITVAULT_WRITER_NOT_ADMITTED` refusal. `added_writer.principal_id`
|
|
2521
|
-
// names the
|
|
2521
|
+
// names the redeemer's own control-plane principal, which the claim
|
|
2522
2522
|
// response never carries (its `membership` block names the ORG), so
|
|
2523
2523
|
// resolve it fresh here.
|
|
2524
2524
|
//
|
|
@@ -2526,7 +2526,7 @@ export class Gitvault {
|
|
|
2526
2526
|
// concurrent rotation, a lost network) leaves this key a PENDING writer
|
|
2527
2527
|
// of the vault, which any live writer's next push or `repos access
|
|
2528
2528
|
// sync` admits. Report it and carry `request_writer_sync` rather than
|
|
2529
|
-
// throwing away a
|
|
2529
|
+
// throwing away a redemption already spent and a tree about to be restored.
|
|
2530
2530
|
let writerActivation;
|
|
2531
2531
|
try {
|
|
2532
2532
|
const who = await this.#client.request("/agent/v1/whoami", { context: "resolving this principal's id for the writer activation head" });
|
|
@@ -2563,11 +2563,11 @@ export class Gitvault {
|
|
|
2563
2563
|
// design D5: register THIS session's presence, then post the ONE
|
|
2564
2564
|
// arrival fact — both best-effort, neither ever throws `join()`.
|
|
2565
2565
|
const rooms = new Rooms(this.#client);
|
|
2566
|
-
const inviteShort =
|
|
2566
|
+
const inviteShort = redeem.invite_id.slice(0, 8);
|
|
2567
2567
|
let myPresence = null;
|
|
2568
2568
|
let presenceFailure = null;
|
|
2569
2569
|
try {
|
|
2570
|
-
const registration = await rooms.registerPresence(
|
|
2570
|
+
const registration = await rooms.registerPresence(redeem.org_id, roomKey, {
|
|
2571
2571
|
task: options.task ?? `joined via invite ${inviteShort}`,
|
|
2572
2572
|
...(options.program !== undefined ? { program: options.program } : {}),
|
|
2573
2573
|
...(options.model !== undefined ? { model: options.model } : {}),
|
|
@@ -2583,11 +2583,11 @@ export class Gitvault {
|
|
|
2583
2583
|
if (myPresence) {
|
|
2584
2584
|
const receiptShort = payload.checkpoint.commit_oid.slice(0, 12);
|
|
2585
2585
|
try {
|
|
2586
|
-
const posted = await rooms.sendMessage(
|
|
2586
|
+
const posted = await rooms.sendMessage(redeem.org_id, roomKey, {
|
|
2587
2587
|
body: `Joined as ${myPresence.name} from checkpoint ${receiptShort}.`,
|
|
2588
2588
|
presenceId: myPresence.presence_id,
|
|
2589
2589
|
...(options.sessionKey !== undefined ? { sessionKey: options.sessionKey } : {}),
|
|
2590
|
-
idempotencyKey: `invite:${
|
|
2590
|
+
idempotencyKey: `invite:${redeem.invite_id}:joined`,
|
|
2591
2591
|
});
|
|
2592
2592
|
if (typeof posted.cursor === "string")
|
|
2593
2593
|
arrivalCursor = posted.cursor;
|
|
@@ -2596,11 +2596,11 @@ export class Gitvault {
|
|
|
2596
2596
|
// best-effort — never blocks arrival
|
|
2597
2597
|
}
|
|
2598
2598
|
}
|
|
2599
|
-
let livePresences =
|
|
2599
|
+
let livePresences = redeem.live_presences ?? [];
|
|
2600
2600
|
let recentMessages = [];
|
|
2601
|
-
let cursor =
|
|
2601
|
+
let cursor = redeem.cursor ?? null;
|
|
2602
2602
|
try {
|
|
2603
|
-
const page = await rooms.listMessages(
|
|
2603
|
+
const page = await rooms.listMessages(redeem.org_id, roomKey, {
|
|
2604
2604
|
order: "desc",
|
|
2605
2605
|
limit: options.recentMessagesLimit ?? 10,
|
|
2606
2606
|
...(myPresence ? { presenceId: myPresence.presence_id } : {}),
|
|
@@ -2618,19 +2618,19 @@ export class Gitvault {
|
|
|
2618
2618
|
cursor = newestRead;
|
|
2619
2619
|
}
|
|
2620
2620
|
catch {
|
|
2621
|
-
// best-effort — arrival still reports the arrival fact's cursor, else the
|
|
2621
|
+
// best-effort — arrival still reports the arrival fact's cursor, else the redemption's own
|
|
2622
2622
|
if (arrivalCursor)
|
|
2623
2623
|
cursor = arrivalCursor;
|
|
2624
2624
|
}
|
|
2625
|
-
const nextActions = [...(
|
|
2625
|
+
const nextActions = [...(redeem.next_actions ?? [])];
|
|
2626
2626
|
if (!nextActions.some((a) => a.type === "push_repo")) {
|
|
2627
2627
|
nextActions.push({ type: "push_repo", command: "git push origin main", why: "Publish continued work back to the vault." });
|
|
2628
2628
|
}
|
|
2629
2629
|
if (!nextActions.some((a) => a.type === "wait_room")) {
|
|
2630
2630
|
nextActions.push({ type: "wait_room", command: "run402 messages wait", why: "Block until the inviter (or anyone else) speaks; silence returns who is still here." });
|
|
2631
2631
|
}
|
|
2632
|
-
if (
|
|
2633
|
-
nextActions.push({ type: "send_room_message", command: `run402 messages send "…" --to ${
|
|
2632
|
+
if (redeem.inviter && !nextActions.some((a) => a.type === "send_room_message")) {
|
|
2633
|
+
nextActions.push({ type: "send_room_message", command: `run402 messages send "…" --to ${redeem.inviter.name}`, why: `${redeem.inviter.name} invited you and may still be live.` });
|
|
2634
2634
|
}
|
|
2635
2635
|
// design D9: a joiner whose activation did not land is not stranded —
|
|
2636
2636
|
// its key is a PENDING writer, and any live writer's reconcile admits
|
|
@@ -2654,22 +2654,22 @@ export class Gitvault {
|
|
|
2654
2654
|
note = null;
|
|
2655
2655
|
}
|
|
2656
2656
|
return {
|
|
2657
|
-
invite_id:
|
|
2658
|
-
kind:
|
|
2659
|
-
deduplicated:
|
|
2657
|
+
invite_id: redeem.invite_id,
|
|
2658
|
+
kind: redeem.kind,
|
|
2659
|
+
deduplicated: redeem.deduplicated,
|
|
2660
2660
|
note,
|
|
2661
2661
|
note_raw: noteRaw,
|
|
2662
2662
|
restored: { dir: targetDir, branch: restored.branch, base_head_oid: restored.base_head_oid, stash_oid: restored.stash_oid },
|
|
2663
|
-
membership: handoffMembershipFromWire(
|
|
2664
|
-
members:
|
|
2665
|
-
room: { organization_id:
|
|
2666
|
-
inviter:
|
|
2663
|
+
membership: handoffMembershipFromWire(redeem.membership),
|
|
2664
|
+
members: redeem.members ?? [],
|
|
2665
|
+
room: { organization_id: redeem.room?.org_id ?? redeem.org_id, room_key: roomKey },
|
|
2666
|
+
inviter: redeem.inviter ?? null,
|
|
2667
2667
|
presence: myPresence,
|
|
2668
2668
|
presence_failure: presenceFailure,
|
|
2669
2669
|
live_presences: livePresences,
|
|
2670
2670
|
cursor,
|
|
2671
2671
|
recent_messages: recentMessages,
|
|
2672
|
-
expires_at:
|
|
2672
|
+
expires_at: redeem.expires_at,
|
|
2673
2673
|
writer_activation: writerActivation,
|
|
2674
2674
|
reconcile_recipients: reconcile,
|
|
2675
2675
|
next_actions: nextActions,
|
|
@@ -3658,7 +3658,7 @@ export class Gitvault {
|
|
|
3658
3658
|
* the envelope-recipients read, no declaration, no owner step-up — any
|
|
3659
3659
|
* surviving writer can run it. `push()` runs it automatically when the
|
|
3660
3660
|
* gate names an outstanding removal; this is the explicit entry point
|
|
3661
|
-
* (`run402
|
|
3661
|
+
* (`run402 orgs members rm` drives it on every vault the caller can).
|
|
3662
3662
|
*/
|
|
3663
3663
|
async rotateEpochForMemberRemoval(options = {}) {
|
|
3664
3664
|
const handle = await this.open(options);
|