Skip to content

Commit 83dcf33

Browse files
chore: add Pr review comments
WCN-1192 TICKET: WCN-1192
1 parent 14e320a commit 83dcf33

9 files changed

Lines changed: 327 additions & 146 deletions

File tree

modules/sdk-core/src/bitgo/enterprise/enterprise.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,9 @@ export class Enterprise implements IEnterprise {
253253

254254
/**
255255
* Get the vaults collection accessor scoped to this Enterprise
256+
* @experimental
256257
*/
257258
vaults(): Vaults {
258-
return new Vaults(this.bitgo, this.baseCoin, this.id);
259+
return new Vaults(this.bitgo, this.id);
259260
}
260261
}

modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ export interface IEnterprise {
4040
bitgoNitroChallenge: SerializedNtildeWithVerifiers
4141
): Promise<void>;
4242
hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean;
43+
/** @experimental */
4344
vaults(): IVaults;
4445
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @prettier
3+
*
4+
* io-ts codecs for the vault REST surface. These are the single source of truth for the
5+
* vault data shapes: the TypeScript interfaces in `iVault.ts` are derived from them via
6+
* `t.TypeOf`, request bodies are encoded with `postWithCodec`, and responses are decoded
7+
* (and validated) with `decodeWithCodec` — so there are no `as VaultData` casts.
8+
*
9+
* Timestamps use `DateFromISOString`: the wire representation stays an ISO-8601 string while
10+
* the decoded object exposes a real `Date`.
11+
*/
12+
import * as t from 'io-ts';
13+
import { DateFromISOString } from 'io-ts-types';
14+
15+
/**
16+
* The four static root-key slots of a vault, keyed by (curve, scheme):
17+
* - secp256k1Multisig ① — ex. UTXO/XRP/XTZ/TRX/EOS
18+
* - ecdsaMpc ② — ex. EVM/Cosmos (DKLS)
19+
* - eddsaMpc ③ — ex. SOL/SUI/NEAR/TON/APT/DOT
20+
* - ed25519Multisig ④ — ex. ALGO/XLM/HBAR
21+
*/
22+
export const RootKeyType = t.keyof(
23+
{
24+
secp256k1Multisig: null,
25+
ecdsaMpc: null,
26+
eddsaMpc: null,
27+
ed25519Multisig: null,
28+
},
29+
'RootKeyType'
30+
);
31+
32+
export const VaultPermission = t.keyof(
33+
{
34+
view: null,
35+
spend: null,
36+
admin: null,
37+
dapp: null,
38+
},
39+
'VaultPermission'
40+
);
41+
42+
/** An ordered [userKeyId, backupKeyId, bitgoKeyId] triplet — same shape/order as wallet.keys[]. */
43+
export const RootKeyTriplet = t.tuple([t.string, t.string, t.string], 'RootKeyTriplet');
44+
45+
/** The 12 static root key ids, by (curve, scheme). */
46+
export const VaultRootKeys = t.type(
47+
{
48+
secp256k1Multisig: RootKeyTriplet,
49+
ecdsaMpc: RootKeyTriplet,
50+
eddsaMpc: RootKeyTriplet,
51+
ed25519Multisig: RootKeyTriplet,
52+
},
53+
'VaultRootKeys'
54+
);
55+
56+
export const VaultMembershipData = t.intersection(
57+
[
58+
t.type({
59+
userId: t.string,
60+
permissions: t.array(VaultPermission),
61+
}),
62+
t.partial({
63+
needsRecovery: t.boolean,
64+
}),
65+
],
66+
'VaultMembershipData'
67+
);
68+
69+
/** A pending UMS spend grant awaiting a key share — mirror of the wallet's walletShareRequests[]. */
70+
export const VaultShareRequest = t.type(
71+
{
72+
userId: t.string,
73+
permissions: t.array(VaultPermission),
74+
createdAt: DateFromISOString,
75+
},
76+
'VaultShareRequest'
77+
);
78+
79+
export const VaultFreeze = t.partial(
80+
{
81+
time: DateFromISOString,
82+
expires: DateFromISOString,
83+
reason: t.string,
84+
},
85+
'VaultFreeze'
86+
);
87+
88+
export const VaultStatus = t.keyof(
89+
{
90+
initializing: null,
91+
active: null,
92+
archived: null,
93+
},
94+
'VaultStatus'
95+
);
96+
97+
export const VaultData = t.intersection(
98+
[
99+
t.type({
100+
id: t.string,
101+
enterpriseId: t.string,
102+
label: t.string,
103+
// freeze is NOT a status — a frozen vault stays 'active' with the freeze field set (wallet precedent)
104+
status: VaultStatus,
105+
creator: t.string,
106+
users: t.array(VaultMembershipData),
107+
createdAt: DateFromISOString,
108+
}),
109+
t.partial({
110+
vaultShareRequests: t.array(VaultShareRequest),
111+
freeze: VaultFreeze,
112+
rootKeys: VaultRootKeys,
113+
archivedAt: DateFromISOString,
114+
}),
115+
],
116+
'VaultData'
117+
);
118+
119+
/** Vault key-share states — identical to WalletShare states, no new states. */
120+
export const VaultShareState = t.keyof(
121+
{
122+
pendingapproval: null,
123+
active: null,
124+
accepted: null,
125+
canceled: null,
126+
rejected: null,
127+
},
128+
'VaultShareState'
129+
);
130+
131+
/** One of the 4 root USER keyshares carried on a VaultShare, ECDH-re-encrypted to the recipient. */
132+
export const VaultShareKeychain = t.type(
133+
{
134+
rootKeyType: RootKeyType,
135+
rootKeyId: t.string,
136+
encryptedPrv: t.string,
137+
publicIdentifier: t.string,
138+
fromPubKey: t.string,
139+
toPubKey: t.string,
140+
path: t.string,
141+
},
142+
'VaultShareKeychain'
143+
);
144+
145+
export const VaultShareData = t.intersection(
146+
[
147+
t.type({
148+
id: t.string,
149+
enterpriseId: t.string,
150+
vaultId: t.string,
151+
fromUser: t.string,
152+
toUser: t.string,
153+
permissions: t.array(VaultPermission),
154+
state: VaultShareState,
155+
createdAt: DateFromISOString,
156+
}),
157+
t.partial({
158+
vaultLabel: t.string,
159+
message: t.string,
160+
pendingApprovalId: t.string,
161+
isUMSInitiated: t.boolean,
162+
keychains: t.array(VaultShareKeychain),
163+
updatedAt: DateFromISOString,
164+
}),
165+
],
166+
'VaultShareData'
167+
);
168+
169+
// ---- request bodies ----
170+
171+
/** POST /enterprise/:eId/vaults — Phase 1 carries no key material. */
172+
export const InitializeVaultBody = t.type({ label: t.string }, 'InitializeVaultBody');
173+
174+
/** POST /enterprise/:eId/vaults/:vId/finalize — the 12 key ids as 4 ordered triplets. */
175+
export const FinalizeVaultBody = t.type({ rootKeys: VaultRootKeys }, 'FinalizeVaultBody');
176+
177+
/** POST/DELETE /enterprise/:eId/vaults/:vId/freeze */
178+
export const FreezeVaultBody = t.partial({ duration: t.number }, 'FreezeVaultBody');

modules/sdk-core/src/bitgo/vault/iVault.ts

Lines changed: 37 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,34 @@
11
/**
22
* @prettier
3+
*
4+
* @experimental The vault client surface is experimental and may change (including breaking
5+
* changes) before the public release.
36
*/
7+
import * as t from 'io-ts';
48
import type { FreezeOptions, Wallet, WalletShare } from '../wallet';
9+
import * as VaultCodecs from './codecs';
510

6-
/**
7-
* The four static root-key slots of a vault, keyed by (curve, scheme):
8-
* - secp256k1Multisig ① — UTXO/XRP/XTZ/TRX/EOS
9-
* - ecdsaMpc ② — EVM/Cosmos (DKLS)
10-
* - eddsaMpc ③ — SOL/SUI/NEAR/TON/APT/DOT
11-
* - ed25519Multisig ④ — ALGO/XLM/HBAR
12-
*/
13-
export type RootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig';
14-
15-
export type VaultPermission = 'view' | 'spend' | 'admin' | 'dapp';
11+
// ---- data shapes (derived from the io-ts codecs in ./codecs — single source of truth) ----
1612

17-
/**
18-
* The 12 static root key ids, by (curve, scheme). Each entry is an ordered
19-
* [userKeyId, backupKeyId, bitgoKeyId] triplet — same shape and order as wallet.keys[].
20-
*/
21-
export type VaultRootKeys = Record<RootKeyType, [userKeyId: string, backupKeyId: string, bitgoKeyId: string]>;
22-
23-
export interface VaultMembershipData {
24-
userId: string; // new-model naming convention
25-
permissions: VaultPermission[];
26-
needsRecovery?: boolean;
27-
}
28-
29-
/**
30-
* A pending UMS spend grant awaiting a key share — mirror of the wallet's walletShareRequests[].
31-
* Returned to spenders/admins and serviced via addMember.
32-
*/
33-
export interface VaultShareRequest {
34-
userId: string;
35-
permissions: VaultPermission[];
36-
createdAt: string;
37-
}
38-
39-
export interface VaultData {
40-
id: string;
41-
enterpriseId: string;
42-
label: string;
43-
// freeze is NOT a status — a frozen vault stays 'active' with the freeze field set (wallet precedent)
44-
status: 'initializing' | 'active' | 'archived';
45-
creator: string;
46-
users: VaultMembershipData[];
47-
vaultShareRequests?: VaultShareRequest[];
48-
freeze?: { time?: string; expires?: string; reason?: string };
49-
rootKeys?: VaultRootKeys; // ordered, like wallet.keys
50-
archivedAt?: string;
51-
createdAt: string;
52-
}
13+
export type RootKeyType = t.TypeOf<typeof VaultCodecs.RootKeyType>;
14+
export type VaultPermission = t.TypeOf<typeof VaultCodecs.VaultPermission>;
15+
export type VaultRootKeys = t.TypeOf<typeof VaultCodecs.VaultRootKeys>;
16+
export type VaultMembershipData = t.TypeOf<typeof VaultCodecs.VaultMembershipData>;
17+
export type VaultShareRequest = t.TypeOf<typeof VaultCodecs.VaultShareRequest>;
18+
export type VaultData = t.TypeOf<typeof VaultCodecs.VaultData>;
19+
export type VaultShareState = t.TypeOf<typeof VaultCodecs.VaultShareState>;
20+
export type VaultShareKeychain = t.TypeOf<typeof VaultCodecs.VaultShareKeychain>;
21+
export type VaultShareData = t.TypeOf<typeof VaultCodecs.VaultShareData>;
5322

5423
export interface InitializeVaultOptions {
55-
label: string; // Phase 1 carries no key material — key generation happens in Phase 2 via the existing keychain APIs
24+
label: string;
5625
}
5726

5827
// Phase 3 — the client hands back the 12 key ids it created in Phase 2:
5928
export interface FinalizeVaultOptions {
6029
rootKeys: VaultRootKeys;
6130
}
6231

63-
/** Vault key-share states — identical to WalletShare states, no new states. */
64-
export type VaultShareState = 'pendingapproval' | 'active' | 'accepted' | 'canceled' | 'rejected';
65-
66-
/**
67-
* One of the 4 root USER keyshares carried on a VaultShare, ECDH-re-encrypted to the recipient.
68-
* Body semantics land in the Part VI SDK ticket (WCN-1204).
69-
*/
70-
export interface VaultShareKeychain {
71-
rootKeyType: RootKeyType;
72-
rootKeyId: string;
73-
encryptedPrv: string;
74-
publicIdentifier: string;
75-
fromPubKey: string;
76-
toPubKey: string;
77-
path: string;
78-
}
79-
80-
export interface VaultShareData {
81-
id: string;
82-
enterpriseId: string;
83-
vaultId: string;
84-
vaultLabel?: string;
85-
fromUser: string;
86-
toUser: string;
87-
permissions: VaultPermission[];
88-
state: VaultShareState;
89-
message?: string;
90-
pendingApprovalId?: string;
91-
isUMSInitiated?: boolean;
92-
keychains?: VaultShareKeychain[];
93-
createdAt: string;
94-
updatedAt?: string;
95-
}
96-
9732
/**
9833
* Sharing ONE vault wallet with a non-member rides the existing wallet-share handshake (FR-13),
9934
* so the result is the existing WalletShare shape.
@@ -106,34 +41,45 @@ export interface CreateVaultWalletOptions {
10641
coin: string;
10742
label: string;
10843
type?: string;
109-
multisigType?: string;
11044
multisigTypeVersion?: string;
11145
}
11246

113-
export interface AddVaultMemberOptions {
114-
userId?: string;
115-
email?: string;
47+
interface AddVaultMemberBase {
11648
permissions: VaultPermission[];
117-
// required when 'spend' is included — the 4 root user keys ECDH-re-encrypted to the invitee
49+
/** required when 'spend' is included — the 4 root user keys ECDH-re-encrypted to the invitee */
11850
keychains?: VaultShareKeychain[];
11951
message?: string;
52+
/** when true, suppress the invitation email that would otherwise be sent to `email` */
12053
disableEmail?: boolean;
12154
}
12255

56+
/** Add a member by either `userId` or `email` — exactly one is required. */
57+
export type AddVaultMemberOptions =
58+
| (AddVaultMemberBase & { userId: string; email?: never })
59+
| (AddVaultMemberBase & { email: string; userId?: never });
60+
12361
export interface AddVaultWalletMemberOptions {
12462
walletId: string;
63+
/** required — sharing re-encrypts the user key, which needs hardened derivation from the passphrase */
64+
walletPassphrase: string;
12565
email?: string;
12666
permissions?: string[];
12767
message?: string;
12868
}
12969

130-
export interface AcceptVaultShareOptions {
70+
export type AcceptVaultShareAsSpenderOptions = {
13171
vaultShareId: string;
132-
userPassword?: string;
72+
userPassword: string;
13373
newWalletPassphrase?: string;
134-
overrideEncryptedPrv?: string;
135-
}
74+
};
75+
export type AcceptVaultShareAsNonSpenderOptions = {
76+
vaultShareId: string;
77+
};
78+
export type AcceptVaultShareOptions = AcceptVaultShareAsSpenderOptions | AcceptVaultShareAsNonSpenderOptions;
13679

80+
/**
81+
* @experimental
82+
*/
13783
export interface IVault {
13884
id(): string;
13985
enterpriseId(): string;
@@ -144,7 +90,7 @@ export interface IVault {
14490
// whole-vault: view/admin/spend; spend opens a key share (also how a spender services a
14591
// vaultShareRequests entry in UMS orgs)
14692
addMember(params: AddVaultMemberOptions): Promise<VaultData>;
147-
// share ONE vault wallet (FR-13), not the whole vault
93+
// share ONE vault wallet, not the whole vault
14894
addMemberToWallet(params: AddVaultWalletMemberOptions): Promise<WalletShareData>;
14995
listShares(params?: { state?: VaultShareState }): Promise<VaultShareData[]>;
15096
acceptShare(params: AcceptVaultShareOptions): Promise<VaultShareData>;

0 commit comments

Comments
 (0)