@waaskey/sdk 0.2.1 → 0.3.1
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 +153 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -18
- package/dist/index.d.ts +28 -18
- package/dist/index.js +153 -22
- package/dist/index.js.map +1 -1
- package/dist/node-mpc-worker.d.ts +2 -0
- package/dist/node-mpc-worker.js +142 -0
- package/dist/node-mpc-worker.js.map +1 -0
- package/dist/node.d.ts +426 -0
- package/dist/node.js +466 -0
- package/dist/node.js.map +1 -0
- package/package.json +8 -2
package/dist/index.cjs
CHANGED
|
@@ -912,6 +912,32 @@ function bytesToHex(bytes) {
|
|
|
912
912
|
return hex;
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
+
// src/share-blob.ts
|
|
916
|
+
function serializeShare(keygen, relayUrl) {
|
|
917
|
+
return JSON.stringify({
|
|
918
|
+
keyShare: keygen.keyShare,
|
|
919
|
+
auxInfo: keygen.auxInfo,
|
|
920
|
+
sharedPublicKey: keygen.sharedPublicKey,
|
|
921
|
+
...relayUrl === void 0 ? {} : { relayUrl }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
function deserializeShare(blob) {
|
|
925
|
+
let parsed;
|
|
926
|
+
try {
|
|
927
|
+
parsed = JSON.parse(blob);
|
|
928
|
+
} catch (cause) {
|
|
929
|
+
throw new WaaskeyError("Stored device share is corrupt \u2014 it is not valid JSON.", "share_not_found", { cause });
|
|
930
|
+
}
|
|
931
|
+
if (typeof parsed.keyShare !== "string") {
|
|
932
|
+
throw new WaaskeyError("Stored device share is malformed \u2014 it is missing its KeyShare.", "share_not_found", { details: { keys: Object.keys(parsed) } });
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
keyShare: parsed.keyShare,
|
|
936
|
+
sharedPublicKey: typeof parsed.sharedPublicKey === "string" ? parsed.sharedPublicKey : void 0,
|
|
937
|
+
relayUrl: typeof parsed.relayUrl === "string" ? parsed.relayUrl : void 0
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
915
941
|
// src/passkey/assertion.ts
|
|
916
942
|
function isPasskeyAssertionSupported() {
|
|
917
943
|
return typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials !== "undefined";
|
|
@@ -1028,10 +1054,55 @@ var Wallet = class {
|
|
|
1028
1054
|
const message = normalizeDigest(digest);
|
|
1029
1055
|
const body = { message };
|
|
1030
1056
|
await this.attachStepUp(body, "sign", options);
|
|
1031
|
-
const
|
|
1057
|
+
const coSign = this.startDeviceCoSign(message);
|
|
1058
|
+
const post = this.http.request("POST", `/v1/wallets/${this.id}/sign`, body, options.signal);
|
|
1059
|
+
const [res] = coSign === void 0 ? [await post] : await Promise.all([post, coSign.catch(() => void 0)]);
|
|
1032
1060
|
this.analytics?.track("wallet.signed", { walletId: this.id, curve: this.data.curve });
|
|
1033
1061
|
return res.signature;
|
|
1034
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Start this device's half of a secp sign ceremony when the wallet's sign quorum requires it.
|
|
1065
|
+
* The quorum is the first `threshold` roles of the wallet's party list (#292) — for the default
|
|
1066
|
+
* `[device, server, recovery]`/2 that is `[device, server]`, so the device MUST be online and
|
|
1067
|
+
* co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
|
|
1068
|
+
* the POST then completes alone, unchanged. A device-present quorum without the device deps or
|
|
1069
|
+
* stored share fails fast with a typed error instead of a guaranteed server-side timeout.
|
|
1070
|
+
*/
|
|
1071
|
+
startDeviceCoSign(digest) {
|
|
1072
|
+
if (this.data.curve === "ed25519") return void 0;
|
|
1073
|
+
const roles = this.data.parties;
|
|
1074
|
+
const { threshold } = this.data;
|
|
1075
|
+
if (!Array.isArray(roles) || typeof threshold !== "number") return void 0;
|
|
1076
|
+
const quorum = roles.slice(0, threshold);
|
|
1077
|
+
const pos = quorum.indexOf("device");
|
|
1078
|
+
if (pos < 0) return void 0;
|
|
1079
|
+
const { mpc, shareStore } = this.device;
|
|
1080
|
+
if (!mpc || !shareStore) {
|
|
1081
|
+
throw new WaaskeyError(
|
|
1082
|
+
`This wallet's sign quorum [${quorum.join(", ")}] includes the device, so signing requires the device MPC core and share store \u2014 pass \`mpc\` and \`shareStore\` to \`new Waaskey(...)\`.`,
|
|
1083
|
+
"device_core_required"
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return (async () => {
|
|
1087
|
+
const blob = await shareStore.get(this.id);
|
|
1088
|
+
if (!blob) {
|
|
1089
|
+
throw new WaaskeyError(`No stored device share for wallet ${this.id} \u2014 this device cannot join the sign quorum.`, "share_not_found");
|
|
1090
|
+
}
|
|
1091
|
+
const { keyShare, relayUrl } = deserializeShare(blob);
|
|
1092
|
+
if (!relayUrl) {
|
|
1093
|
+
throw new WaaskeyError("The stored device share predates relay-URL persistence \u2014 re-create the wallet (or recover) to enable device-present signing.", "share_not_found");
|
|
1094
|
+
}
|
|
1095
|
+
const participants = quorum.map((_role, index) => index);
|
|
1096
|
+
const base = { curve: toMpcCurve(this.data.curve), relayUrl, sessionId: this.id, share: keyShare, participants, signerPosition: pos, digest };
|
|
1097
|
+
if (quorum.length === 2) {
|
|
1098
|
+
return mpc.runSign({ ...base, role: quorum[pos], peerRole: quorum[1 - pos], partyIndex: pos, peerPartyIndex: 1 - pos });
|
|
1099
|
+
}
|
|
1100
|
+
if (!mpc.runMemberSign) {
|
|
1101
|
+
throw new WaaskeyError(`This wallet's sign quorum has ${quorum.length} parties, which needs an MPC core with routed (member-ceremony) sign support.`, "unsupported");
|
|
1102
|
+
}
|
|
1103
|
+
return mpc.runMemberSign({ ...base, roles: quorum });
|
|
1104
|
+
})();
|
|
1105
|
+
}
|
|
1035
1106
|
/**
|
|
1036
1107
|
* Send a transaction from this wallet. The platform builds the chain-specific transaction
|
|
1037
1108
|
* and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
|
|
@@ -1493,7 +1564,7 @@ var Wallets = class {
|
|
|
1493
1564
|
const pregeneratedPrimes = primePool ? await primePool.take(curve) : void 0;
|
|
1494
1565
|
throwIfAborted(signal);
|
|
1495
1566
|
const [keygen] = await this.runKeygenParties(mpc, [{ ...ceremony, curve, pregeneratedPrimes }]);
|
|
1496
|
-
await shareStore.put(walletId, serializeShare(keygen));
|
|
1567
|
+
await shareStore.put(walletId, serializeShare(keygen, ceremony.relayUrl));
|
|
1497
1568
|
return;
|
|
1498
1569
|
}
|
|
1499
1570
|
if (!backup) {
|
|
@@ -1508,7 +1579,7 @@ var Wallets = class {
|
|
|
1508
1579
|
{ ...ceremony, curve, pregeneratedPrimes: devicePrimes },
|
|
1509
1580
|
{ ...userBackupParty, curve, pregeneratedPrimes: backupPrimes }
|
|
1510
1581
|
]);
|
|
1511
|
-
await shareStore.put(walletId, serializeShare(deviceKeygen));
|
|
1582
|
+
await shareStore.put(walletId, serializeShare(deviceKeygen, ceremony.relayUrl));
|
|
1512
1583
|
throwIfAborted(signal);
|
|
1513
1584
|
const { payload } = await buildRecoveryRegistration({ share: serializeShare(backupKeygen), ...backup });
|
|
1514
1585
|
await shareStore.put(userBackupPendingKey(walletId), JSON.stringify(payload));
|
|
@@ -1653,12 +1724,39 @@ var Wallets = class {
|
|
|
1653
1724
|
*/
|
|
1654
1725
|
async runKeygenParties(mpc, params) {
|
|
1655
1726
|
try {
|
|
1656
|
-
return await Promise.all(params.map((party) =>
|
|
1727
|
+
return await Promise.all(params.map((party) => this.runKeygenParty(mpc, party, params)));
|
|
1657
1728
|
} catch (cause) {
|
|
1658
1729
|
if (cause instanceof WaaskeyError) throw cause;
|
|
1659
1730
|
throw new WaaskeyError("The device keygen ceremony failed.", "keygen_failed", { cause });
|
|
1660
1731
|
}
|
|
1661
1732
|
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Run ONE client keygen party over the right transport. A 2-party ceremony uses the plain
|
|
1735
|
+
* single-peer path; anything larger MUST be roster-routed — the 2-party transport attributes
|
|
1736
|
+
* every inbound message to the one configured peer, so a third party's very first message
|
|
1737
|
+
* aborts the protocol with "route received message" (waas-core#131).
|
|
1738
|
+
*/
|
|
1739
|
+
runKeygenParty(mpc, party, batch) {
|
|
1740
|
+
if (party.parties <= 2) {
|
|
1741
|
+
return mpc.runKeygen(party);
|
|
1742
|
+
}
|
|
1743
|
+
if (!mpc.runMemberKeygen) {
|
|
1744
|
+
throw new WaaskeyError(
|
|
1745
|
+
`This keygen ceremony has ${party.parties} parties, which needs an MPC core with routed (member-ceremony) keygen support \u2014 use a client-wasm build exposing keygenMember.`,
|
|
1746
|
+
"unsupported"
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
return mpc.runMemberKeygen({
|
|
1750
|
+
curve: party.curve,
|
|
1751
|
+
relayUrl: party.relayUrl,
|
|
1752
|
+
sessionId: party.sessionId,
|
|
1753
|
+
roles: ceremonyRoster(party, batch),
|
|
1754
|
+
partyIndex: party.partyIndex,
|
|
1755
|
+
threshold: party.threshold,
|
|
1756
|
+
relayToken: party.relayToken,
|
|
1757
|
+
pregeneratedPrimes: party.pregeneratedPrimes
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1662
1760
|
/**
|
|
1663
1761
|
* Run the device half of an ed25519 (FROST) keygen (#110) and seal the resulting `{keyPackage,
|
|
1664
1762
|
* publicKeyPackage}` share — the EdDSA counterpart of the cggmp24 `runKeygen` branch in {@link create}.
|
|
@@ -1729,21 +1827,6 @@ var Wallets = class {
|
|
|
1729
1827
|
}
|
|
1730
1828
|
}
|
|
1731
1829
|
};
|
|
1732
|
-
function serializeShare(keygen) {
|
|
1733
|
-
return JSON.stringify({ keyShare: keygen.keyShare, auxInfo: keygen.auxInfo, sharedPublicKey: keygen.sharedPublicKey });
|
|
1734
|
-
}
|
|
1735
|
-
function deserializeShare(blob) {
|
|
1736
|
-
let parsed;
|
|
1737
|
-
try {
|
|
1738
|
-
parsed = JSON.parse(blob);
|
|
1739
|
-
} catch (cause) {
|
|
1740
|
-
throw new WaaskeyError("Stored device share is corrupt \u2014 it is not valid JSON.", "share_not_found", { cause });
|
|
1741
|
-
}
|
|
1742
|
-
if (typeof parsed.keyShare !== "string") {
|
|
1743
|
-
throw new WaaskeyError("Stored device share is malformed \u2014 it is missing its KeyShare.", "share_not_found", { details: { keys: Object.keys(parsed) } });
|
|
1744
|
-
}
|
|
1745
|
-
return { keyShare: parsed.keyShare, sharedPublicKey: typeof parsed.sharedPublicKey === "string" ? parsed.sharedPublicKey : void 0 };
|
|
1746
|
-
}
|
|
1747
1830
|
async function restoreUserBackupShare(ciphertext, recoveryCode) {
|
|
1748
1831
|
let blob;
|
|
1749
1832
|
try {
|
|
@@ -1801,6 +1884,38 @@ function membershipIdFromRole(role) {
|
|
|
1801
1884
|
}
|
|
1802
1885
|
return role.slice(prefix.length);
|
|
1803
1886
|
}
|
|
1887
|
+
function ceremonyRoster(party, batch) {
|
|
1888
|
+
if (party.roles) {
|
|
1889
|
+
if (party.roles.length !== party.parties) {
|
|
1890
|
+
throw new WaaskeyError(`The ceremony roster names ${party.roles.length} parties but the ceremony has ${party.parties}.`, "validation");
|
|
1891
|
+
}
|
|
1892
|
+
return party.roles;
|
|
1893
|
+
}
|
|
1894
|
+
const roles = new Array(party.parties).fill(void 0);
|
|
1895
|
+
const put = (index, role) => {
|
|
1896
|
+
if (role === void 0) return;
|
|
1897
|
+
if (index < 0 || index >= party.parties) {
|
|
1898
|
+
throw new WaaskeyError(`Ceremony party index ${index} is out of range for a ${party.parties}-party ceremony.`, "validation");
|
|
1899
|
+
}
|
|
1900
|
+
if (roles[index] !== void 0 && roles[index] !== role) {
|
|
1901
|
+
throw new WaaskeyError(`Conflicting ceremony roles for party index ${index}: "${roles[index]}" vs "${role}".`, "validation");
|
|
1902
|
+
}
|
|
1903
|
+
roles[index] = role;
|
|
1904
|
+
};
|
|
1905
|
+
for (const entry of batch) {
|
|
1906
|
+
put(entry.partyIndex, entry.role ?? "device");
|
|
1907
|
+
put(entry.peerPartyIndex, entry.peerRole ?? "server");
|
|
1908
|
+
}
|
|
1909
|
+
const gaps = roles.reduce((acc, role, index) => role === void 0 ? [...acc, index] : acc, []);
|
|
1910
|
+
if (gaps.length > 1) {
|
|
1911
|
+
throw new WaaskeyError(
|
|
1912
|
+
`The ${party.parties}-party keygen ceremony leaves ${gaps.length} party slots unnamed \u2014 this backend must supply the full ceremony roster (roles).`,
|
|
1913
|
+
"validation"
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
if (gaps.length === 1) roles[gaps[0]] = "recovery";
|
|
1917
|
+
return roles;
|
|
1918
|
+
}
|
|
1804
1919
|
function buildMemberRoster(shareholders, parties) {
|
|
1805
1920
|
const roles = new Array(parties).fill(void 0);
|
|
1806
1921
|
for (const shareholder of shareholders) {
|
|
@@ -2260,14 +2375,30 @@ function createVerifiedClientWasmLoader(options) {
|
|
|
2260
2375
|
return mod;
|
|
2261
2376
|
};
|
|
2262
2377
|
}
|
|
2378
|
+
var clientWasm;
|
|
2263
2379
|
var loadClientWasm = async () => {
|
|
2380
|
+
return clientWasm ??= importAndInit();
|
|
2381
|
+
};
|
|
2382
|
+
async function importAndInit() {
|
|
2383
|
+
let mod;
|
|
2264
2384
|
try {
|
|
2265
|
-
|
|
2266
|
-
return mod;
|
|
2385
|
+
mod = await import(CLIENT_WASM_PACKAGE);
|
|
2267
2386
|
} catch (cause) {
|
|
2268
2387
|
throw new Error("Waaskey: creating a wallet needs the wasm engine \u2014 install '@waaskey/client-wasm' alongside '@waaskey/sdk'.", { cause });
|
|
2269
2388
|
}
|
|
2270
|
-
|
|
2389
|
+
const init = mod["default"] ?? mod["init"];
|
|
2390
|
+
if (typeof init === "function") {
|
|
2391
|
+
try {
|
|
2392
|
+
await init();
|
|
2393
|
+
} catch (cause) {
|
|
2394
|
+
throw new Error(
|
|
2395
|
+
"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.",
|
|
2396
|
+
{ cause }
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return mod;
|
|
2401
|
+
}
|
|
2271
2402
|
function toBase64(bytes) {
|
|
2272
2403
|
let binary = "";
|
|
2273
2404
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|