@waaskey/sdk 0.3.0 → 0.3.2
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/dist/index.cjs +230 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -49
- package/dist/index.d.ts +90 -49
- package/dist/index.js +230 -69
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +5 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -626,7 +626,7 @@ async function buildRecoveryRegistration(params) {
|
|
|
626
626
|
const recoveryCode = params.recoveryCode ?? generateRecoveryCode();
|
|
627
627
|
const ciphertext = await sealWithPassword(recoveryCode, params.share);
|
|
628
628
|
const factors = [
|
|
629
|
-
{ type: "recovery_code",
|
|
629
|
+
{ type: "recovery_code", credential: await sha256Hex(recoveryCode) },
|
|
630
630
|
{ type: "totp", credential: params.totpSecret },
|
|
631
631
|
{ type: "email_otp", credential: params.email },
|
|
632
632
|
...params.extraFactors ?? []
|
|
@@ -654,7 +654,7 @@ async function registerRecoveryShare(http, walletId, params, signal) {
|
|
|
654
654
|
return { recoveryCode, share };
|
|
655
655
|
}
|
|
656
656
|
async function hashRecoveryFactors(verifications) {
|
|
657
|
-
return Promise.all(verifications.map(async (v) => v.type === "recovery_code" && v.token
|
|
657
|
+
return Promise.all(verifications.map(async (v) => v.type === "recovery_code" && typeof v.token === "string" ? { type: v.type, token: await sha256Hex(v.token) } : v));
|
|
658
658
|
}
|
|
659
659
|
function generateRecoveryCode() {
|
|
660
660
|
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
@@ -910,6 +910,51 @@ function bytesToHex(bytes) {
|
|
|
910
910
|
return hex;
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
+
// src/session-sign.ts
|
|
914
|
+
function toDeviceSignParams(ceremony, share, digest) {
|
|
915
|
+
const { roles, participants, signerPosition } = ceremony;
|
|
916
|
+
if (participants.length !== 2 || roles.length !== 2 || signerPosition < 0 || signerPosition > 1) {
|
|
917
|
+
throw new WaaskeyError(
|
|
918
|
+
`Unexpected sign descriptor: the quorum must be exactly 2 parties with signerPosition in {0, 1} (got ${participants.length} participants, ${roles.length} roles, signerPosition ${signerPosition}).`,
|
|
919
|
+
"sign_failed",
|
|
920
|
+
{ details: { roles, participants, signerPosition } }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const peerPosition = 1 - signerPosition;
|
|
924
|
+
return {
|
|
925
|
+
curve: toMpcCurve(ceremony.curve),
|
|
926
|
+
relayUrl: ceremony.relayUrl,
|
|
927
|
+
sessionId: ceremony.sessionId,
|
|
928
|
+
role: roles[signerPosition],
|
|
929
|
+
peerRole: roles[peerPosition],
|
|
930
|
+
partyIndex: signerPosition,
|
|
931
|
+
peerPartyIndex: peerPosition,
|
|
932
|
+
relayToken: ceremony.relayToken,
|
|
933
|
+
share,
|
|
934
|
+
participants,
|
|
935
|
+
signerPosition,
|
|
936
|
+
digest
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function toSessionSignParams(session, share, digest) {
|
|
940
|
+
return toDeviceSignParams(
|
|
941
|
+
{
|
|
942
|
+
curve: session.curve,
|
|
943
|
+
relayUrl: session.relayUrl,
|
|
944
|
+
sessionId: session.sessionId,
|
|
945
|
+
roles: sessionRoles(session),
|
|
946
|
+
participants: session.participants,
|
|
947
|
+
signerPosition: session.signerPosition,
|
|
948
|
+
relayToken: session.relayToken
|
|
949
|
+
},
|
|
950
|
+
share,
|
|
951
|
+
digest
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
function sessionRoles(session) {
|
|
955
|
+
return session.signerPosition === 0 ? [session.role, session.peerRole] : [session.peerRole, session.role];
|
|
956
|
+
}
|
|
957
|
+
|
|
913
958
|
// src/share-blob.ts
|
|
914
959
|
function serializeShare(keygen, relayUrl) {
|
|
915
960
|
return JSON.stringify({
|
|
@@ -976,6 +1021,7 @@ async function getSigningAssertion(challenge, options = {}) {
|
|
|
976
1021
|
}
|
|
977
1022
|
|
|
978
1023
|
// src/wallet.ts
|
|
1024
|
+
var DEVICE_ROLE = "device";
|
|
979
1025
|
var Wallet = class {
|
|
980
1026
|
constructor(http, data, analytics, device = {}) {
|
|
981
1027
|
this.http = http;
|
|
@@ -1050,56 +1096,106 @@ var Wallet = class {
|
|
|
1050
1096
|
*/
|
|
1051
1097
|
async sign(digest, options = {}) {
|
|
1052
1098
|
const message = normalizeDigest(digest);
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1099
|
+
const quorum = this.signQuorum();
|
|
1100
|
+
const devicePosition = quorum?.indexOf(DEVICE_ROLE) ?? -1;
|
|
1101
|
+
if (devicePosition < 0) {
|
|
1102
|
+
const body = { message };
|
|
1103
|
+
await this.attachStepUp(body, "sign", options);
|
|
1104
|
+
const res = await this.http.request("POST", `/v1/wallets/${this.id}/sign`, body, options.signal);
|
|
1105
|
+
this.analytics?.track("wallet.signed", { walletId: this.id, curve: this.data.curve });
|
|
1106
|
+
return res.signature;
|
|
1107
|
+
}
|
|
1108
|
+
const { mpc, keyShare } = await this.loadDeviceParty(quorum, "signing");
|
|
1109
|
+
const startBody = { digest: message };
|
|
1110
|
+
await this.attachStepUp(startBody, "sign", options);
|
|
1111
|
+
const session = await this.http.request("POST", `/v1/wallets/${this.id}/sign-session`, startBody, options.signal);
|
|
1112
|
+
const { signature } = await this.runDeviceSign(mpc, this.toSignCeremony(session, quorum), keyShare, message);
|
|
1058
1113
|
this.analytics?.track("wallet.signed", { walletId: this.id, curve: this.data.curve });
|
|
1059
|
-
return
|
|
1114
|
+
return signature;
|
|
1060
1115
|
}
|
|
1061
1116
|
/**
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
* co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
|
|
1066
|
-
* the POST then completes alone, unchanged. A device-present quorum without the device deps or
|
|
1067
|
-
* stored share fails fast with a typed error instead of a guaranteed server-side timeout.
|
|
1117
|
+
* The roles that will actually sign — the first `threshold` of the wallet's party list (#292),
|
|
1118
|
+
* mirroring the signer's own derivation. `undefined` when the topology is unknown (a legacy
|
|
1119
|
+
* wallet record), which callers treat as "let the server decide".
|
|
1068
1120
|
*/
|
|
1069
|
-
|
|
1121
|
+
signQuorum() {
|
|
1070
1122
|
if (this.data.curve === "ed25519") return void 0;
|
|
1071
1123
|
const roles = this.data.parties;
|
|
1072
1124
|
const { threshold } = this.data;
|
|
1073
1125
|
if (!Array.isArray(roles) || typeof threshold !== "number") return void 0;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1126
|
+
return roles.slice(0, threshold);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Everything this device needs to join a ceremony, resolved BEFORE one is started (#89): the MPC
|
|
1130
|
+
* core, the routed-sign capability a >2-party quorum needs, and the stored key share.
|
|
1131
|
+
*
|
|
1132
|
+
* Each failure is a typed error thrown straight to the caller, and the ordering is the point: a
|
|
1133
|
+
* device that cannot co-sign must never leave the platform party waiting on the relay for a
|
|
1134
|
+
* counterpart that will never arrive. That wait ends at the party-runner timeout (~210s) and
|
|
1135
|
+
* reaches the caller as an opaque 5xx — minutes after a knowable, local cause.
|
|
1136
|
+
*/
|
|
1137
|
+
async loadDeviceParty(quorum, action) {
|
|
1077
1138
|
const { mpc, shareStore } = this.device;
|
|
1078
1139
|
if (!mpc || !shareStore) {
|
|
1079
1140
|
throw new WaaskeyError(
|
|
1080
|
-
`This wallet's
|
|
1141
|
+
`This wallet's signing quorum [${quorum.join(", ")}] includes the device, so ${action} requires the device MPC core and share store \u2014 pass \`mpc\` and \`shareStore\` to \`new Waaskey(...)\`.`,
|
|
1081
1142
|
"device_core_required"
|
|
1082
1143
|
);
|
|
1083
1144
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1145
|
+
if (quorum.length > 2 && !mpc.runMemberSign) {
|
|
1146
|
+
throw new WaaskeyError(`This wallet's sign quorum has ${quorum.length} parties, which needs an MPC core with routed (member-ceremony) sign support.`, "unsupported");
|
|
1147
|
+
}
|
|
1148
|
+
const blob = await shareStore.get(this.id);
|
|
1149
|
+
if (!blob) {
|
|
1150
|
+
throw new WaaskeyError(`No stored device share for wallet ${this.id} \u2014 this device cannot join the signing quorum.`, "share_not_found");
|
|
1151
|
+
}
|
|
1152
|
+
const { keyShare } = deserializeShare(blob);
|
|
1153
|
+
return { mpc, keyShare };
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Normalize a sign-session descriptor into the shared {@link DeviceCeremony}. The 2-party roster
|
|
1157
|
+
* comes from the descriptor's own `role`/`peerRole` (authoritative — the relay token is bound to
|
|
1158
|
+
* that role); a larger quorum carries no server-sent roster, so the wallet's own party slice — the
|
|
1159
|
+
* same slice the signer derives — supplies it.
|
|
1160
|
+
*/
|
|
1161
|
+
toSignCeremony(session, quorum) {
|
|
1162
|
+
return {
|
|
1163
|
+
curve: session.curve,
|
|
1164
|
+
relayUrl: session.relayUrl,
|
|
1165
|
+
sessionId: session.sessionId,
|
|
1166
|
+
roles: session.participants.length === 2 ? sessionRoles(session) : quorum,
|
|
1167
|
+
participants: session.participants,
|
|
1168
|
+
signerPosition: session.signerPosition,
|
|
1169
|
+
relayToken: session.relayToken
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Run this device's half of a started ceremony. A 2-party quorum uses the plain single-peer
|
|
1174
|
+
* transport; anything larger MUST be roster-routed, or the transport attributes every inbound
|
|
1175
|
+
* message to the one configured peer and the protocol aborts on the third party's first message
|
|
1176
|
+
* (waas-core#131).
|
|
1177
|
+
*/
|
|
1178
|
+
runDeviceSign(mpc, ceremony, keyShare, digest) {
|
|
1179
|
+
if (ceremony.participants.length === 2) {
|
|
1180
|
+
return mpc.runSign(toDeviceSignParams(ceremony, keyShare, digest));
|
|
1181
|
+
}
|
|
1182
|
+
if (!mpc.runMemberSign) {
|
|
1183
|
+
throw new WaaskeyError(
|
|
1184
|
+
`This wallet's sign quorum has ${ceremony.participants.length} parties, which needs an MPC core with routed (member-ceremony) sign support.`,
|
|
1185
|
+
"unsupported"
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
return mpc.runMemberSign({
|
|
1189
|
+
curve: toMpcCurve(ceremony.curve),
|
|
1190
|
+
relayUrl: ceremony.relayUrl,
|
|
1191
|
+
sessionId: ceremony.sessionId,
|
|
1192
|
+
relayToken: ceremony.relayToken,
|
|
1193
|
+
roles: ceremony.roles,
|
|
1194
|
+
share: keyShare,
|
|
1195
|
+
participants: ceremony.participants,
|
|
1196
|
+
signerPosition: ceremony.signerPosition,
|
|
1197
|
+
digest
|
|
1198
|
+
});
|
|
1103
1199
|
}
|
|
1104
1200
|
/**
|
|
1105
1201
|
* Send a transaction from this wallet. The platform builds the chain-specific transaction
|
|
@@ -1119,19 +1215,60 @@ var Wallet = class {
|
|
|
1119
1215
|
if (this.data.curve === "ed25519") {
|
|
1120
1216
|
return this.sendEd25519(params, options);
|
|
1121
1217
|
}
|
|
1218
|
+
const quorum = this.signQuorum();
|
|
1219
|
+
if ((quorum?.indexOf(DEVICE_ROLE) ?? -1) >= 0) {
|
|
1220
|
+
return this.sendWithDevice(params, options, quorum);
|
|
1221
|
+
}
|
|
1122
1222
|
const body = { ...params };
|
|
1123
1223
|
await this.attachStepUp(body, "send", options);
|
|
1124
1224
|
const res = await this.http.request("POST", `/v1/wallets/${this.id}/send`, body, options.signal);
|
|
1125
1225
|
this.analytics?.track("wallet.sent", { walletId: this.id, chain: params.chainId });
|
|
1126
1226
|
return res;
|
|
1127
1227
|
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Device-co-signed send for a secp wallet — the cggmp24 counterpart of {@link sendEd25519}:
|
|
1230
|
+
*
|
|
1231
|
+
* 1. **START** (`POST …/send-session`): the platform runs the transfer gates, builds the unsigned
|
|
1232
|
+
* tx, puts its own party on the relay in the background, and returns the 32-byte digest plus
|
|
1233
|
+
* the relay coordination. It does NOT wait for the ceremony.
|
|
1234
|
+
* 2. **CO-SIGN**: this device runs its half over the relay with its stored share; cggmp24 hands
|
|
1235
|
+
* the completed signature to both parties.
|
|
1236
|
+
* 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the platform verifies the signature
|
|
1237
|
+
* (recovers to the wallet key AND equals its own party's) and embeds it into the wire tx.
|
|
1238
|
+
*
|
|
1239
|
+
* Every device-side precondition is resolved BEFORE the START (see {@link loadDeviceParty}), so a
|
|
1240
|
+
* device that cannot co-sign costs nothing: no tx is built and no platform party is left waiting.
|
|
1241
|
+
*
|
|
1242
|
+
* **A co-sign that fails after a successful START rejects with its typed error.** START is not the
|
|
1243
|
+
* commit point — it yields an *unsigned* tx and a pending session, and nothing broadcastable exists
|
|
1244
|
+
* until ASSEMBLE returns a `signedTx` — so there is no result to salvage by swallowing the failure,
|
|
1245
|
+
* and no fallback to `POST /send` (the platform cannot reach the threshold on this wallet alone, so
|
|
1246
|
+
* a retry there would only hang). The backend expires the abandoned session and fails the tx row.
|
|
1247
|
+
*/
|
|
1248
|
+
async sendWithDevice(params, options, quorum) {
|
|
1249
|
+
const { signal } = options;
|
|
1250
|
+
const { mpc, keyShare } = await this.loadDeviceParty(quorum, "sending");
|
|
1251
|
+
throwIfAborted(signal);
|
|
1252
|
+
const startBody = { ...params };
|
|
1253
|
+
await this.attachStepUp(startBody, "send", options);
|
|
1254
|
+
throwIfAborted(signal);
|
|
1255
|
+
const session = await this.http.request("POST", `/v1/wallets/${this.id}/send-session`, startBody, signal);
|
|
1256
|
+
const digest = sendPayload(session, "digest");
|
|
1257
|
+
throwIfAborted(signal);
|
|
1258
|
+
const { signature } = await this.runDeviceSign(mpc, toSendCeremony(session), keyShare, digest);
|
|
1259
|
+
throwIfAborted(signal);
|
|
1260
|
+
const assemble = { signature };
|
|
1261
|
+
const res = await this.http.request("POST", `/v1/wallets/${this.id}/send-session/${session.txId}/assemble`, assemble, signal);
|
|
1262
|
+
this.analytics?.track("wallet.sent", { walletId: this.id, chain: params.chainId });
|
|
1263
|
+
return res;
|
|
1264
|
+
}
|
|
1128
1265
|
/**
|
|
1129
1266
|
* Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
|
|
1130
1267
|
* share and co-signs 2-party with the backend `server` party over the relay:
|
|
1131
1268
|
*
|
|
1132
1269
|
* 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
|
|
1133
1270
|
* its server FROST party on the relay in the background, and returns the raw `message` bytes to
|
|
1134
|
-
* sign + the relay coordination ({@link
|
|
1271
|
+
* sign + the relay coordination ({@link SendSessionResponse}).
|
|
1135
1272
|
* 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
|
|
1136
1273
|
* publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
|
|
1137
1274
|
* 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the backend embeds the aggregated
|
|
@@ -1166,6 +1303,7 @@ var Wallet = class {
|
|
|
1166
1303
|
await this.attachStepUp(startBody, "send", options);
|
|
1167
1304
|
throwIfAborted(signal);
|
|
1168
1305
|
const session = await this.http.request("POST", `/v1/wallets/${this.id}/send-session`, startBody, signal);
|
|
1306
|
+
const message = sendPayload(session, "message");
|
|
1169
1307
|
throwIfAborted(signal);
|
|
1170
1308
|
let signature;
|
|
1171
1309
|
try {
|
|
@@ -1177,7 +1315,7 @@ var Wallet = class {
|
|
|
1177
1315
|
keyPackage,
|
|
1178
1316
|
publicKeyPackage,
|
|
1179
1317
|
participants: session.participants,
|
|
1180
|
-
message
|
|
1318
|
+
message,
|
|
1181
1319
|
relayToken: session.relayToken
|
|
1182
1320
|
}));
|
|
1183
1321
|
} catch (cause) {
|
|
@@ -1226,6 +1364,26 @@ async function attachPasskeyStepUp(http, walletId, body, operation, options) {
|
|
|
1226
1364
|
body["passkeyAssertion"] = await getSigningAssertion(challenge, { credentialId: options.passkeyCredentialId });
|
|
1227
1365
|
body["passkeyChallengeId"] = challengeId;
|
|
1228
1366
|
}
|
|
1367
|
+
function toSendCeremony(session) {
|
|
1368
|
+
return {
|
|
1369
|
+
curve: session.curve,
|
|
1370
|
+
relayUrl: session.relayUrl,
|
|
1371
|
+
sessionId: session.sessionId,
|
|
1372
|
+
roles: session.roles,
|
|
1373
|
+
participants: session.participants,
|
|
1374
|
+
signerPosition: session.signerPosition,
|
|
1375
|
+
relayToken: session.relayToken
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function sendPayload(session, field) {
|
|
1379
|
+
const payload = session[field];
|
|
1380
|
+
if (!payload) {
|
|
1381
|
+
throw new WaaskeyError(`The send session did not return the \`${field}\` this wallet's curve (${session.curve}) signs.`, "sign_failed", {
|
|
1382
|
+
details: { txId: session.txId, curve: session.curve }
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
return payload;
|
|
1386
|
+
}
|
|
1229
1387
|
function normalizeDigest(digest) {
|
|
1230
1388
|
const hex = digest.startsWith("0x") || digest.startsWith("0X") ? digest.slice(2) : digest;
|
|
1231
1389
|
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
@@ -1688,7 +1846,7 @@ var Wallets = class {
|
|
|
1688
1846
|
throwIfAborted(signal);
|
|
1689
1847
|
let signature;
|
|
1690
1848
|
try {
|
|
1691
|
-
({ signature } = await mpc.runSign(
|
|
1849
|
+
({ signature } = await mpc.runSign(toSessionSignParams(session, share, digest)));
|
|
1692
1850
|
} catch (cause) {
|
|
1693
1851
|
if (cause instanceof WaaskeyError) throw cause;
|
|
1694
1852
|
throw new WaaskeyError("The user_backup device recover-sign ceremony failed.", "sign_failed", { cause });
|
|
@@ -1834,29 +1992,6 @@ async function restoreUserBackupShare(ciphertext, recoveryCode) {
|
|
|
1834
1992
|
}
|
|
1835
1993
|
return deserializeShare(blob).keyShare;
|
|
1836
1994
|
}
|
|
1837
|
-
function toUserBackupSignParams(session, share, digest) {
|
|
1838
|
-
if (session.participants.length !== 2 || session.signerPosition < 0 || session.signerPosition > 1) {
|
|
1839
|
-
throw new WaaskeyError(
|
|
1840
|
-
`Unexpected recover-sign descriptor: the {server, user_backup} quorum must be exactly 2 parties with signerPosition in {0, 1} (got ${session.participants.length} participants, signerPosition ${session.signerPosition}).`,
|
|
1841
|
-
"sign_failed",
|
|
1842
|
-
{ details: { participants: session.participants, signerPosition: session.signerPosition } }
|
|
1843
|
-
);
|
|
1844
|
-
}
|
|
1845
|
-
return {
|
|
1846
|
-
curve: toMpcCurve(session.curve),
|
|
1847
|
-
relayUrl: session.relayUrl,
|
|
1848
|
-
sessionId: session.sessionId,
|
|
1849
|
-
role: session.role,
|
|
1850
|
-
peerRole: session.peerRole,
|
|
1851
|
-
partyIndex: session.signerPosition,
|
|
1852
|
-
peerPartyIndex: 1 - session.signerPosition,
|
|
1853
|
-
relayToken: session.relayToken,
|
|
1854
|
-
share,
|
|
1855
|
-
participants: session.participants,
|
|
1856
|
-
signerPosition: session.signerPosition,
|
|
1857
|
-
digest
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
1995
|
function memberShareKey(walletId, membershipId) {
|
|
1861
1996
|
return `${walletId}@member-${membershipId}`;
|
|
1862
1997
|
}
|
|
@@ -1873,7 +2008,17 @@ function parseBackupPayload(stored, walletId) {
|
|
|
1873
2008
|
if (typeof parsed.ciphertext !== "string" || !Array.isArray(parsed.factors)) {
|
|
1874
2009
|
throw new WaaskeyError(`The pending user_backup backup for wallet "${walletId}" is malformed.`, "backup_failed", { details: { walletId, keys: Object.keys(parsed) } });
|
|
1875
2010
|
}
|
|
1876
|
-
return { ciphertext: parsed.ciphertext, factors: parsed.factors };
|
|
2011
|
+
return { ciphertext: parsed.ciphertext, factors: parsed.factors.map((factor) => normalizeStoredFactor(factor, walletId)) };
|
|
2012
|
+
}
|
|
2013
|
+
function normalizeStoredFactor(factor, walletId) {
|
|
2014
|
+
const { type, credential, credentialHash } = factor ?? {};
|
|
2015
|
+
const value = typeof credential === "string" && credential !== "" ? credential : credentialHash;
|
|
2016
|
+
if (typeof type !== "string" || type === "" || typeof value !== "string" || value === "") {
|
|
2017
|
+
throw new WaaskeyError(`The pending user_backup backup for wallet "${walletId}" is malformed \u2014 a factor entry is missing its type or credential.`, "backup_failed", {
|
|
2018
|
+
details: { walletId, factorKeys: Object.keys(factor ?? {}) }
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return { type, credential: value };
|
|
1877
2022
|
}
|
|
1878
2023
|
function membershipIdFromRole(role) {
|
|
1879
2024
|
const prefix = "member:";
|
|
@@ -2330,7 +2475,7 @@ function decodePublicKey(sharedPublicKeyJson) {
|
|
|
2330
2475
|
|
|
2331
2476
|
// src/mpc/load-wasm.ts
|
|
2332
2477
|
var CLIENT_WASM_PACKAGE = "@waaskey/client-wasm";
|
|
2333
|
-
var CLIENT_WASM_VERSION = "0.2.
|
|
2478
|
+
var CLIENT_WASM_VERSION = "0.2.2";
|
|
2334
2479
|
async function verifyWasmIntegrity(bytes, expectedSha384) {
|
|
2335
2480
|
if (!expectedSha384 || !expectedSha384.startsWith("sha384-")) {
|
|
2336
2481
|
throw new Error("Waaskey: an expected SHA-384 integrity hash (sha384-<base64>) is required to load the wasm MPC core.");
|
|
@@ -2373,14 +2518,30 @@ function createVerifiedClientWasmLoader(options) {
|
|
|
2373
2518
|
return mod;
|
|
2374
2519
|
};
|
|
2375
2520
|
}
|
|
2521
|
+
var clientWasm;
|
|
2376
2522
|
var loadClientWasm = async () => {
|
|
2523
|
+
return clientWasm ??= importAndInit();
|
|
2524
|
+
};
|
|
2525
|
+
async function importAndInit() {
|
|
2526
|
+
let mod;
|
|
2377
2527
|
try {
|
|
2378
|
-
|
|
2379
|
-
return mod;
|
|
2528
|
+
mod = await import(CLIENT_WASM_PACKAGE);
|
|
2380
2529
|
} catch (cause) {
|
|
2381
2530
|
throw new Error("Waaskey: creating a wallet needs the wasm engine \u2014 install '@waaskey/client-wasm' alongside '@waaskey/sdk'.", { cause });
|
|
2382
2531
|
}
|
|
2383
|
-
|
|
2532
|
+
const init = mod["default"] ?? mod["init"];
|
|
2533
|
+
if (typeof init === "function") {
|
|
2534
|
+
try {
|
|
2535
|
+
await init();
|
|
2536
|
+
} catch (cause) {
|
|
2537
|
+
throw new Error(
|
|
2538
|
+
"Waaskey: the wasm engine failed to initialize. In the browser this loader fetches the `.wasm` next to the module \u2014 serve it, or pass a loader built with `createVerifiedClientWasmLoader({ wasmUrl, expectedSha384 })`. On Node use `NodeWorkerMpcCore` from `@waaskey/sdk/node`, which loads the binary from disk in a worker thread.",
|
|
2539
|
+
{ cause }
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return mod;
|
|
2544
|
+
}
|
|
2384
2545
|
function toBase64(bytes) {
|
|
2385
2546
|
let binary = "";
|
|
2386
2547
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|