@dfx.swiss/wallet 0.0.0-stage → 0.1.1-beta.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 +39 -2
- package/dist/config/environment.d.ts +40 -0
- package/dist/config/environment.d.ts.map +1 -0
- package/dist/config/environment.js +37 -0
- package/dist/config/environment.js.map +1 -0
- package/dist/derivation/bitcoin-keys.d.ts +20 -0
- package/dist/derivation/bitcoin-keys.d.ts.map +1 -0
- package/dist/derivation/bitcoin-keys.js +50 -0
- package/dist/derivation/bitcoin-keys.js.map +1 -0
- package/dist/derivation/evm-keys.d.ts +8 -0
- package/dist/derivation/evm-keys.d.ts.map +1 -0
- package/dist/derivation/evm-keys.js +32 -0
- package/dist/derivation/evm-keys.js.map +1 -0
- package/dist/derivation/keyring.d.ts +68 -0
- package/dist/derivation/keyring.d.ts.map +1 -0
- package/dist/derivation/keyring.js +150 -0
- package/dist/derivation/keyring.js.map +1 -0
- package/dist/derivation/public-data.d.ts +26 -0
- package/dist/derivation/public-data.d.ts.map +1 -0
- package/dist/derivation/public-data.js +19 -0
- package/dist/derivation/public-data.js.map +1 -0
- package/dist/derivation/seed-check.d.ts +6 -0
- package/dist/derivation/seed-check.d.ts.map +1 -0
- package/dist/derivation/seed-check.js +13 -0
- package/dist/derivation/seed-check.js.map +1 -0
- package/dist/derivation/spark-keys.d.ts +14 -0
- package/dist/derivation/spark-keys.d.ts.map +1 -0
- package/dist/derivation/spark-keys.js +64 -0
- package/dist/derivation/spark-keys.js.map +1 -0
- package/dist/derivation/v1.d.ts +32 -0
- package/dist/derivation/v1.d.ts.map +1 -0
- package/dist/derivation/v1.js +64 -0
- package/dist/derivation/v1.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/passkey/ceremonies.d.ts +91 -0
- package/dist/passkey/ceremonies.d.ts.map +1 -0
- package/dist/passkey/ceremonies.js +250 -0
- package/dist/passkey/ceremonies.js.map +1 -0
- package/dist/passkey/ceremony-gate.d.ts +25 -0
- package/dist/passkey/ceremony-gate.d.ts.map +1 -0
- package/dist/passkey/ceremony-gate.js +45 -0
- package/dist/passkey/ceremony-gate.js.map +1 -0
- package/dist/passkey/credential-cache.d.ts +38 -0
- package/dist/passkey/credential-cache.d.ts.map +1 -0
- package/dist/passkey/credential-cache.js +91 -0
- package/dist/passkey/credential-cache.js.map +1 -0
- package/dist/passkey/diagnostics.d.ts +22 -0
- package/dist/passkey/diagnostics.d.ts.map +1 -0
- package/dist/passkey/diagnostics.js +59 -0
- package/dist/passkey/diagnostics.js.map +1 -0
- package/dist/passkey/errors.d.ts +49 -0
- package/dist/passkey/errors.d.ts.map +1 -0
- package/dist/passkey/errors.js +87 -0
- package/dist/passkey/errors.js.map +1 -0
- package/dist/passkey/platform.d.ts +77 -0
- package/dist/passkey/platform.d.ts.map +1 -0
- package/dist/passkey/platform.js +163 -0
- package/dist/passkey/platform.js.map +1 -0
- package/dist/passkey/webauthn-json.d.ts +35 -0
- package/dist/passkey/webauthn-json.d.ts.map +1 -0
- package/dist/passkey/webauthn-json.js +79 -0
- package/dist/passkey/webauthn-json.js.map +1 -0
- package/dist/providers/blocklist.d.ts +22 -0
- package/dist/providers/blocklist.d.ts.map +1 -0
- package/dist/providers/blocklist.js +51 -0
- package/dist/providers/blocklist.js.map +1 -0
- package/dist/providers/chain-registry.d.ts +26 -0
- package/dist/providers/chain-registry.d.ts.map +1 -0
- package/dist/providers/chain-registry.js +59 -0
- package/dist/providers/chain-registry.js.map +1 -0
- package/dist/providers/dfx-data.d.ts +25 -0
- package/dist/providers/dfx-data.d.ts.map +1 -0
- package/dist/providers/dfx-data.js +44 -0
- package/dist/providers/dfx-data.js.map +1 -0
- package/dist/providers/price.d.ts +34 -0
- package/dist/providers/price.d.ts.map +1 -0
- package/dist/providers/price.js +46 -0
- package/dist/providers/price.js.map +1 -0
- package/dist/providers/snapshot.d.ts +28 -0
- package/dist/providers/snapshot.d.ts.map +1 -0
- package/dist/providers/snapshot.js +2 -0
- package/dist/providers/snapshot.js.map +1 -0
- package/dist/session/idle-lock.d.ts +31 -0
- package/dist/session/idle-lock.d.ts.map +1 -0
- package/dist/session/idle-lock.js +73 -0
- package/dist/session/idle-lock.js.map +1 -0
- package/dist/session/passkey-auth.d.ts +95 -0
- package/dist/session/passkey-auth.d.ts.map +1 -0
- package/dist/session/passkey-auth.js +356 -0
- package/dist/session/passkey-auth.js.map +1 -0
- package/dist/session/tab-lock.d.ts +30 -0
- package/dist/session/tab-lock.d.ts.map +1 -0
- package/dist/session/tab-lock.js +79 -0
- package/dist/session/tab-lock.js.map +1 -0
- package/dist/signing/auto-signer.d.ts +26 -0
- package/dist/signing/auto-signer.d.ts.map +1 -0
- package/dist/signing/auto-signer.js +76 -0
- package/dist/signing/auto-signer.js.map +1 -0
- package/dist/signing/bitcoin-message.d.ts +11 -0
- package/dist/signing/bitcoin-message.d.ts.map +1 -0
- package/dist/signing/bitcoin-message.js +62 -0
- package/dist/signing/bitcoin-message.js.map +1 -0
- package/dist/signing/login-messages.d.ts +15 -0
- package/dist/signing/login-messages.d.ts.map +1 -0
- package/dist/signing/login-messages.js +16 -0
- package/dist/signing/login-messages.js.map +1 -0
- package/dist/signing/login-text-guard.d.ts +3 -0
- package/dist/signing/login-text-guard.d.ts.map +1 -0
- package/dist/signing/login-text-guard.js +13 -0
- package/dist/signing/login-text-guard.js.map +1 -0
- package/dist/signing/spark-login.d.ts +17 -0
- package/dist/signing/spark-login.d.ts.map +1 -0
- package/dist/signing/spark-login.js +51 -0
- package/dist/signing/spark-login.js.map +1 -0
- package/dist/util/bytes.d.ts +11 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +26 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/emitter.d.ts +17 -0
- package/dist/util/emitter.d.ts.map +1 -0
- package/dist/util/emitter.js +53 -0
- package/dist/util/emitter.js.map +1 -0
- package/dist/util/errors.d.ts +12 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +15 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/jwt.d.ts +6 -0
- package/dist/util/jwt.d.ts.map +1 -0
- package/dist/util/jwt.js +23 -0
- package/dist/util/jwt.js.map +1 -0
- package/dist/util/kv-store.d.ts +51 -0
- package/dist/util/kv-store.d.ts.map +1 -0
- package/dist/util/kv-store.js +133 -0
- package/dist/util/kv-store.js.map +1 -0
- package/dist/util/random.d.ts +3 -0
- package/dist/util/random.d.ts.map +1 -0
- package/dist/util/random.js +18 -0
- package/dist/util/random.js.map +1 -0
- package/package.json +42 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ApiException } from '@dfx.swiss/core';
|
|
2
|
+
import { ProviderUnavailableError } from './dfx-data';
|
|
3
|
+
/** A price older than this is stale; a stale price must not enter a fiat amount (§9.6). */
|
|
4
|
+
export const PRICE_STALE_MS = 5 * 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Fiat prices of public assets, always by asset id — never by symbol (D32). A missing asset is simply
|
|
7
|
+
* unpriced ("–"); a stale price is returned as stale so callers can refuse fiat entry (§9.6). Prices never
|
|
8
|
+
* feed fee caps.
|
|
9
|
+
*/
|
|
10
|
+
export class PricePolicy {
|
|
11
|
+
constructor(walletData, options = {}) {
|
|
12
|
+
this.walletData = walletData;
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
async prices(assetIds, currency, now = this.options.now ?? Date.now) {
|
|
16
|
+
const clock = now();
|
|
17
|
+
const ids = [...new Set(assetIds)].slice(0, 100);
|
|
18
|
+
let remote = [];
|
|
19
|
+
try {
|
|
20
|
+
remote = await this.walletData.prices({ assetIds: ids, currency });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof ProviderUnavailableError || error instanceof ApiException)
|
|
24
|
+
return new Map();
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const result = new Map();
|
|
28
|
+
for (const entry of remote) {
|
|
29
|
+
if (entry.isValid !== true)
|
|
30
|
+
continue;
|
|
31
|
+
if (typeof entry.price !== 'number' || !Number.isFinite(entry.price))
|
|
32
|
+
continue;
|
|
33
|
+
const timestamp = Date.parse(entry.timestamp);
|
|
34
|
+
result.set(entry.assetId, {
|
|
35
|
+
price: entry.price,
|
|
36
|
+
stale: Number.isNaN(timestamp) || clock - timestamp > PRICE_STALE_MS,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/** Convenience: price with staleness, or `undefined` when the asset has none. */
|
|
42
|
+
priceOf(prices, assetId) {
|
|
43
|
+
return prices.get(assetId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=price.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price.js","sourceRoot":"","sources":["../../src/providers/price.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAEtD,2FAA2F;AAC3F,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAQ5C;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACtB,YACmB,UAAyB,EACzB,UAA8B,EAAE;QADhC,eAAU,GAAV,UAAU,CAAe;QACzB,YAAO,GAAP,OAAO,CAAyB;IAChD,CAAC;IAEJ,KAAK,CAAC,MAAM,CACV,QAAkB,EAClB,QAAgB,EAChB,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG;QAElC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;QACpB,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACjD,IAAI,MAAM,GAAkB,EAAE,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,wBAAwB,IAAI,KAAK,YAAY,YAAY;gBAAE,OAAO,IAAI,GAAG,EAAE,CAAC;YACjG,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6C,CAAC;QACpE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;gBAAE,SAAS;YACrC,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;gBAAE,SAAS;YAC/E,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE;gBACxB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,GAAG,SAAS,GAAG,cAAc;aACrE,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,iFAAiF;IACjF,OAAO,CACL,MAAsD,EACtD,OAAe;QAEf,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChainInfo } from '@dfx.swiss/core';
|
|
2
|
+
/**
|
|
3
|
+
* Release-pinned registry fallback per environment (§9.2): chains, asset list and blocklists as bundled at
|
|
4
|
+
* build time. Live API answers always replace it.
|
|
5
|
+
*/
|
|
6
|
+
export interface WalletSnapshot {
|
|
7
|
+
env: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
chains: ChainInfo[];
|
|
10
|
+
assets: SnapshotAsset[];
|
|
11
|
+
blocklists: SnapshotBlocklists;
|
|
12
|
+
}
|
|
13
|
+
export interface SnapshotAsset {
|
|
14
|
+
id: number;
|
|
15
|
+
blockchain: string;
|
|
16
|
+
name: string;
|
|
17
|
+
uniqueName?: string;
|
|
18
|
+
chainId?: number;
|
|
19
|
+
decimals?: number;
|
|
20
|
+
buyable: boolean;
|
|
21
|
+
sellable: boolean;
|
|
22
|
+
isPrivate: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SnapshotBlocklists {
|
|
25
|
+
phishingDomains: string[];
|
|
26
|
+
scamAddresses: string[];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/providers/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/providers/snapshot.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** When the wallet locks itself after inactivity (D5): 60 minutes by default, or when the tab hides. */
|
|
2
|
+
export type IdleDelay = number | 'tab';
|
|
3
|
+
export interface IdleLockOptions {
|
|
4
|
+
delay: IdleDelay;
|
|
5
|
+
onIdle(): void;
|
|
6
|
+
target?: EventTarget;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Idle auto-lock: activity touches the timer, a 30 s tick and `visibilitychange` check it (background
|
|
11
|
+
* timers are throttled). `'tab'` locks as soon as the page hides. The check never locks on a hidden page
|
|
12
|
+
* with a plain delay — only on the tick after it becomes visible again.
|
|
13
|
+
*/
|
|
14
|
+
export declare class IdleLock {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly events;
|
|
17
|
+
private readonly now;
|
|
18
|
+
private lastActivity;
|
|
19
|
+
private timer?;
|
|
20
|
+
private stopped;
|
|
21
|
+
constructor(options: IdleLockOptions);
|
|
22
|
+
touch(): void;
|
|
23
|
+
stop(): void;
|
|
24
|
+
private start;
|
|
25
|
+
private onActivity;
|
|
26
|
+
private onVisibility;
|
|
27
|
+
private check;
|
|
28
|
+
private visibilityState;
|
|
29
|
+
private lock;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=idle-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-lock.d.ts","sourceRoot":"","sources":["../../src/session/idle-lock.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,IAAI,IAAI,CAAC;IACf,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAKD;;;;GAIG;AACH,qBAAa,QAAQ;IAOP,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAS;gBAEK,OAAO,EAAE,eAAe;IAOrD,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAQZ,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,UAAU,CAEhB;IAEF,OAAO,CAAC,YAAY,CAKlB;IAEF,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,IAAI;CAKb"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const CHECK_INTERVAL_MS = 30000;
|
|
2
|
+
const ACTIVITY_EVENTS = ['pointerdown', 'keydown', 'wheel', 'touchstart'];
|
|
3
|
+
/**
|
|
4
|
+
* Idle auto-lock: activity touches the timer, a 30 s tick and `visibilitychange` check it (background
|
|
5
|
+
* timers are throttled). `'tab'` locks as soon as the page hides. The check never locks on a hidden page
|
|
6
|
+
* with a plain delay — only on the tick after it becomes visible again.
|
|
7
|
+
*/
|
|
8
|
+
export class IdleLock {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.stopped = false;
|
|
12
|
+
this.onActivity = () => {
|
|
13
|
+
this.touch();
|
|
14
|
+
};
|
|
15
|
+
this.onVisibility = () => {
|
|
16
|
+
if (this.stopped)
|
|
17
|
+
return;
|
|
18
|
+
const state = this.visibilityState();
|
|
19
|
+
if (this.options.delay === 'tab' && state === 'hidden')
|
|
20
|
+
this.lock();
|
|
21
|
+
if (state === 'visible')
|
|
22
|
+
this.check();
|
|
23
|
+
};
|
|
24
|
+
this.events = options.target ?? defaultIdleTarget();
|
|
25
|
+
this.now = options.now ?? Date.now;
|
|
26
|
+
this.lastActivity = this.now();
|
|
27
|
+
this.start();
|
|
28
|
+
}
|
|
29
|
+
touch() {
|
|
30
|
+
this.lastActivity = this.now();
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
this.stopped = true;
|
|
34
|
+
if (this.timer !== undefined)
|
|
35
|
+
clearInterval(this.timer);
|
|
36
|
+
this.timer = undefined;
|
|
37
|
+
for (const type of ACTIVITY_EVENTS)
|
|
38
|
+
this.events.removeEventListener(type, this.onActivity);
|
|
39
|
+
this.events.removeEventListener('visibilitychange', this.onVisibility);
|
|
40
|
+
}
|
|
41
|
+
start() {
|
|
42
|
+
for (const type of ACTIVITY_EVENTS)
|
|
43
|
+
this.events.addEventListener(type, this.onActivity, { passive: true });
|
|
44
|
+
this.events.addEventListener('visibilitychange', this.onVisibility);
|
|
45
|
+
if (typeof this.options.delay === 'number') {
|
|
46
|
+
this.timer = setInterval(() => this.check(), CHECK_INTERVAL_MS);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
check() {
|
|
50
|
+
if (this.stopped)
|
|
51
|
+
return;
|
|
52
|
+
if (this.visibilityState() === 'hidden')
|
|
53
|
+
return;
|
|
54
|
+
if (typeof this.options.delay === 'number' && this.now() - this.lastActivity >= this.options.delay)
|
|
55
|
+
this.lock();
|
|
56
|
+
}
|
|
57
|
+
visibilityState() {
|
|
58
|
+
return this.events.visibilityState;
|
|
59
|
+
}
|
|
60
|
+
lock() {
|
|
61
|
+
if (this.stopped)
|
|
62
|
+
return;
|
|
63
|
+
this.stop();
|
|
64
|
+
this.options.onIdle();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** `visibilitychange` fires on `document` and does not bubble to `window`. */
|
|
68
|
+
function defaultIdleTarget() {
|
|
69
|
+
if (typeof document !== 'undefined')
|
|
70
|
+
return document;
|
|
71
|
+
return globalThis;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=idle-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-lock.js","sourceRoot":"","sources":["../../src/session/idle-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { DfxApiClient, PasskeyAddress, PasskeyWallet } from '@dfx.swiss/core';
|
|
2
|
+
import type { WalletEnv } from '../config/environment';
|
|
3
|
+
import { CredentialCache } from '../passkey/credential-cache';
|
|
4
|
+
import type { PasskeyCeremonies } from '../passkey/ceremonies';
|
|
5
|
+
import { WalletError } from '../util/errors';
|
|
6
|
+
import { KeyRing } from '../derivation/keyring';
|
|
7
|
+
/** Wallet states the flows produce (§8.2). */
|
|
8
|
+
export type WalletState = 'locked' | 'unlocked' | 'seed-mismatch' | 'compromised' | 'open-elsewhere';
|
|
9
|
+
/**
|
|
10
|
+
* Result of a passkey flow: the in-memory account token and the server wallet view. `keys` is set
|
|
11
|
+
* whenever the ceremony returned PRF output, including a seed mismatch, so the caller can show the
|
|
12
|
+
* derived address. `state` is `unlocked` only after the seed check passes.
|
|
13
|
+
*/
|
|
14
|
+
export interface AuthResult {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
wallet: PasskeyWallet;
|
|
17
|
+
keys?: KeyRing;
|
|
18
|
+
state: WalletState;
|
|
19
|
+
}
|
|
20
|
+
/** A server refusal (403 body code), e.g. `CredentialCompromised` or `CredentialLost`. */
|
|
21
|
+
export interface PasskeyRefusal {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
code?: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class PasskeyRefusedError extends WalletError {
|
|
27
|
+
readonly refusal: PasskeyRefusal;
|
|
28
|
+
constructor(refusal: PasskeyRefusal);
|
|
29
|
+
}
|
|
30
|
+
export interface RegisterExtras {
|
|
31
|
+
mail: string;
|
|
32
|
+
language?: string;
|
|
33
|
+
usedRef?: string;
|
|
34
|
+
recommendationCode?: string;
|
|
35
|
+
specialCode?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PasskeyAuthDeps {
|
|
38
|
+
api: DfxApiClient;
|
|
39
|
+
ceremonies: PasskeyCeremonies;
|
|
40
|
+
env: WalletEnv;
|
|
41
|
+
cache?: CredentialCache;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The passkey account flows (§8): register, login, unlock, claim, link, revoke and the seed export. The
|
|
45
|
+
* account token never leaves memory; the seed is derived only from a PRF output of a ceremony of this
|
|
46
|
+
* device and only accepted after the seed check against the server value (§6.3).
|
|
47
|
+
*/
|
|
48
|
+
export declare class PasskeyAuth {
|
|
49
|
+
private readonly deps;
|
|
50
|
+
constructor(deps: PasskeyAuthDeps);
|
|
51
|
+
/** Registers a new passkey wallet (2-tap ceremony, seed proof for EVM and Spark, D22). */
|
|
52
|
+
register(extras: RegisterExtras): Promise<AuthResult>;
|
|
53
|
+
/** Logs in (§8.4): the assertion authenticates; the PRF output unlocks the seed. Without PRF the session
|
|
54
|
+
* stays locked (D17); a different seed is a hard mismatch, never signed around. */
|
|
55
|
+
login(): Promise<AuthResult>;
|
|
56
|
+
/** Unlocks an already authenticated session with a fresh PRF assertion (§6.2, §11.4). */
|
|
57
|
+
unlock(): Promise<{
|
|
58
|
+
keys?: KeyRing;
|
|
59
|
+
state: WalletState;
|
|
60
|
+
}>;
|
|
61
|
+
/** Claims a passkey for an existing account with a fresh (≤ 300 s) login token (§8.6). */
|
|
62
|
+
claim(freshLoginToken: string): Promise<AuthResult>;
|
|
63
|
+
/** Links another derived address of the unlocked wallet (§8.5); EVM index i or the Bitcoin login address. */
|
|
64
|
+
link(family: 'evm' | 'bitcoin', accessToken: string, keys: KeyRing, evmIndex?: number): Promise<{
|
|
65
|
+
accessToken: string;
|
|
66
|
+
address: PasskeyAddress;
|
|
67
|
+
}>;
|
|
68
|
+
/** Revokes the wallet by proving the credential one last time (D11). */
|
|
69
|
+
revoke(reason: 'Lost' | 'Compromised'): Promise<PasskeyWallet>;
|
|
70
|
+
/**
|
|
71
|
+
* The 12 words, only behind a fresh local challenge (G5); the server is never involved.
|
|
72
|
+
* One stored active passkey is pinned. Several stored passkeys need `credentialId`, so the
|
|
73
|
+
* authenticator cannot return another wallet. No stored passkey stays discoverable.
|
|
74
|
+
*/
|
|
75
|
+
exportMnemonic(credentialId?: Uint8Array): Promise<string[]>;
|
|
76
|
+
/** Server records must match family, path and address of the local derivations (bitcoin 0/0, Spark, EVM 0..99). */
|
|
77
|
+
verifyServerAddresses(wallet: PasskeyWallet, keys: KeyRing): {
|
|
78
|
+
address: string;
|
|
79
|
+
family: string;
|
|
80
|
+
matches: boolean;
|
|
81
|
+
}[];
|
|
82
|
+
/** The one active passkey on this origin. Several active passkeys are not a guess. */
|
|
83
|
+
private onlyActiveCredentialId;
|
|
84
|
+
private finishAuthentication;
|
|
85
|
+
/** The registration seed proof: EVM seed check address with EIP-191 and the Spark identity with D31. */
|
|
86
|
+
private seedProof;
|
|
87
|
+
/** Server accepts at most ten ids per status call; anything past that must be a later request. */
|
|
88
|
+
private activeExcludeIds;
|
|
89
|
+
private checkRpId;
|
|
90
|
+
private confirmOrAbort;
|
|
91
|
+
private isAddressExists;
|
|
92
|
+
private isDefinitiveRejection;
|
|
93
|
+
private cacheWallet;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=passkey-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-auth.d.ts","sourceRoot":"","sources":["../../src/session/passkey-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { ApiException } from '@dfx.swiss/core';
|
|
2
|
+
import { RP_IDS, SIGN_MESSAGE_PREFIX, WALLET_NETWORKS } from '../config/environment';
|
|
3
|
+
import { base64UrlDecode, base64UrlEncode, equalBytes } from '../util/bytes';
|
|
4
|
+
import { WalletError } from '../util/errors';
|
|
5
|
+
import { KeyRing, withPrivateKey } from '../derivation/keyring';
|
|
6
|
+
import { seedPublicData } from '../derivation/public-data';
|
|
7
|
+
import { mnemonicFromPrfV1 } from '../derivation/v1';
|
|
8
|
+
import { seedCheck } from '../derivation/seed-check';
|
|
9
|
+
import { LoginAutoSigner, signEip191 } from '../signing/auto-signer';
|
|
10
|
+
import { LOGIN_TEMPLATES, buildLoginMessage } from '../signing/login-messages';
|
|
11
|
+
export class PasskeyRefusedError extends WalletError {
|
|
12
|
+
constructor(refusal) {
|
|
13
|
+
super('passkey-refused', refusal.message);
|
|
14
|
+
this.refusal = refusal;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function keysFromPrf(prfOutput, env) {
|
|
18
|
+
return KeyRing.fromMnemonic(mnemonicFromPrfV1(prfOutput), { bitcoinNetwork: WALLET_NETWORKS[env].bitcoin });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The passkey account flows (§8): register, login, unlock, claim, link, revoke and the seed export. The
|
|
22
|
+
* account token never leaves memory; the seed is derived only from a PRF output of a ceremony of this
|
|
23
|
+
* device and only accepted after the seed check against the server value (§6.3).
|
|
24
|
+
*/
|
|
25
|
+
export class PasskeyAuth {
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
}
|
|
29
|
+
/** Registers a new passkey wallet (2-tap ceremony, seed proof for EVM and Spark, D22). */
|
|
30
|
+
async register(extras) {
|
|
31
|
+
const challenge = await this.deps.api.passkey.registerBegin();
|
|
32
|
+
this.checkRpId(challenge.rpId);
|
|
33
|
+
const excludeIds = (await this.activeExcludeIds()).map((id) => base64UrlDecode(id));
|
|
34
|
+
const pending = await this.deps.ceremonies.createCredential({
|
|
35
|
+
challenge: base64UrlDecode(challenge.challenge),
|
|
36
|
+
excludeCredentialIds: excludeIds,
|
|
37
|
+
});
|
|
38
|
+
const created = await this.confirmOrAbort(pending);
|
|
39
|
+
const keys = keysFromPrf(created.prfOutput, this.deps.env);
|
|
40
|
+
const proof = this.seedProof(keys);
|
|
41
|
+
const finish = (challengeId, assertion) => this.deps.api.passkey.registerFinish({
|
|
42
|
+
challengeId,
|
|
43
|
+
credential: created.attestation,
|
|
44
|
+
userHandle: base64UrlEncode(created.userHandle),
|
|
45
|
+
derivationVersion: 'V1',
|
|
46
|
+
assertion,
|
|
47
|
+
...proof,
|
|
48
|
+
...extras,
|
|
49
|
+
});
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await finish(challenge.challengeId, created.assertion);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (this.isDefinitiveRejection(error)) {
|
|
56
|
+
await pending.abort();
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
// The original attestation stays. Only the new assertion is bound to the retry challenge.
|
|
61
|
+
const retry = await this.deps.api.passkey.registerBegin();
|
|
62
|
+
this.checkRpId(retry.rpId);
|
|
63
|
+
const assertion = await this.deps.ceremonies.assert({
|
|
64
|
+
challenge: base64UrlDecode(retry.challenge),
|
|
65
|
+
credentialId: created.credentialId,
|
|
66
|
+
continueCeremony: true,
|
|
67
|
+
});
|
|
68
|
+
response = await finish(retry.challengeId, assertion.assertion);
|
|
69
|
+
}
|
|
70
|
+
catch (retryError) {
|
|
71
|
+
// AddressExists means the lost finish was stored. Any other 4xx was not.
|
|
72
|
+
if (this.isDefinitiveRejection(retryError) && !this.isAddressExists(retryError))
|
|
73
|
+
await pending.abort();
|
|
74
|
+
throw retryError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.cacheWallet(response.wallet, keys, created.credentialId);
|
|
78
|
+
return { accessToken: response.accessToken, wallet: response.wallet, keys, state: 'unlocked' };
|
|
79
|
+
}
|
|
80
|
+
/** Logs in (§8.4): the assertion authenticates; the PRF output unlocks the seed. Without PRF the session
|
|
81
|
+
* stays locked (D17); a different seed is a hard mismatch, never signed around. */
|
|
82
|
+
async login() {
|
|
83
|
+
const challenge = await this.deps.api.passkey.authenticateBegin();
|
|
84
|
+
this.checkRpId(challenge.rpId);
|
|
85
|
+
const assertion = await this.deps.ceremonies.assert({ challenge: base64UrlDecode(challenge.challenge) });
|
|
86
|
+
const response = await this.finishAuthentication(challenge.challengeId, assertion.assertion);
|
|
87
|
+
if (!assertion.prfOutput) {
|
|
88
|
+
return { ...response, state: 'locked' };
|
|
89
|
+
}
|
|
90
|
+
const keys = keysFromPrf(assertion.prfOutput, this.deps.env);
|
|
91
|
+
const state = seedCheck(keys.evm(0).address, response.wallet.seedCheckAddress)
|
|
92
|
+
? 'unlocked'
|
|
93
|
+
: 'seed-mismatch';
|
|
94
|
+
if (state === 'unlocked')
|
|
95
|
+
this.cacheWallet(response.wallet, keys, assertion.credentialId);
|
|
96
|
+
return { ...response, keys, state };
|
|
97
|
+
}
|
|
98
|
+
/** Unlocks an already authenticated session with a fresh PRF assertion (§6.2, §11.4). */
|
|
99
|
+
async unlock() {
|
|
100
|
+
const assertion = await this.deps.ceremonies.assert({});
|
|
101
|
+
if (!assertion.prfOutput)
|
|
102
|
+
return { state: 'locked' };
|
|
103
|
+
const credentialId = base64UrlEncode(assertion.credentialId);
|
|
104
|
+
const entry = this.deps.cache?.list().find((candidate) => candidate.credentialId === credentialId);
|
|
105
|
+
if (!entry || entry.state !== 'active' || !entry.publicData)
|
|
106
|
+
return { state: 'locked' };
|
|
107
|
+
const keys = keysFromPrf(assertion.prfOutput, this.deps.env);
|
|
108
|
+
if (!seedCheck(keys.evm(0).address, entry.publicData.seedCheckAddress))
|
|
109
|
+
return { keys, state: 'seed-mismatch' };
|
|
110
|
+
return { keys, state: 'unlocked' };
|
|
111
|
+
}
|
|
112
|
+
/** Claims a passkey for an existing account with a fresh (≤ 300 s) login token (§8.6). */
|
|
113
|
+
async claim(freshLoginToken) {
|
|
114
|
+
const challenge = await this.deps.api.passkey.claimBegin(freshLoginToken);
|
|
115
|
+
this.checkRpId(challenge.rpId);
|
|
116
|
+
const pending = await this.deps.ceremonies.createCredential({
|
|
117
|
+
challenge: base64UrlDecode(challenge.challenge),
|
|
118
|
+
});
|
|
119
|
+
const created = await this.confirmOrAbort(pending);
|
|
120
|
+
const keys = keysFromPrf(created.prfOutput, this.deps.env);
|
|
121
|
+
const proof = this.seedProof(keys);
|
|
122
|
+
const finish = (challengeId, assertion, signatures) => this.deps.api.passkey.claimFinish({
|
|
123
|
+
challengeId,
|
|
124
|
+
credential: created.attestation,
|
|
125
|
+
userHandle: base64UrlEncode(created.userHandle),
|
|
126
|
+
derivationVersion: 'V1',
|
|
127
|
+
assertion,
|
|
128
|
+
address: proof.address,
|
|
129
|
+
sparkAddress: proof.sparkAddress,
|
|
130
|
+
...signatures,
|
|
131
|
+
}, freshLoginToken);
|
|
132
|
+
const originalSignatures = { signature: proof.signature, sparkSignature: proof.sparkSignature };
|
|
133
|
+
// The server already has this address, so the credential stays. A later 4xx is not a delete.
|
|
134
|
+
const claimWithNewSignature = async () => {
|
|
135
|
+
const retry = await this.deps.api.passkey.claimBegin(freshLoginToken);
|
|
136
|
+
this.checkRpId(retry.rpId);
|
|
137
|
+
const assertion = await this.deps.ceremonies.assert({
|
|
138
|
+
challenge: base64UrlDecode(retry.challenge),
|
|
139
|
+
credentialId: created.credentialId,
|
|
140
|
+
continueCeremony: true,
|
|
141
|
+
});
|
|
142
|
+
const claimText = SIGN_MESSAGE_PREFIX[this.deps.env] +
|
|
143
|
+
LOGIN_TEMPLATES.general +
|
|
144
|
+
'passkey-claim\n' +
|
|
145
|
+
retry.challenge +
|
|
146
|
+
'\n' +
|
|
147
|
+
proof.address;
|
|
148
|
+
const signature = withPrivateKey(keys, { family: 'evm', index: 0 }, (privateKey) => signEip191(claimText, privateKey));
|
|
149
|
+
return finish(retry.challengeId, assertion.assertion, {
|
|
150
|
+
signature,
|
|
151
|
+
sparkSignature: proof.sparkSignature,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
let response;
|
|
155
|
+
try {
|
|
156
|
+
response = await finish(challenge.challengeId, created.assertion, originalSignatures);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (this.isAddressExists(error)) {
|
|
160
|
+
response = await claimWithNewSignature();
|
|
161
|
+
}
|
|
162
|
+
else if (this.isDefinitiveRejection(error)) {
|
|
163
|
+
await pending.abort();
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
try {
|
|
168
|
+
const retry = await this.deps.api.passkey.claimBegin(freshLoginToken);
|
|
169
|
+
this.checkRpId(retry.rpId);
|
|
170
|
+
const assertion = await this.deps.ceremonies.assert({
|
|
171
|
+
challenge: base64UrlDecode(retry.challenge),
|
|
172
|
+
credentialId: created.credentialId,
|
|
173
|
+
continueCeremony: true,
|
|
174
|
+
});
|
|
175
|
+
response = await finish(retry.challengeId, assertion.assertion, originalSignatures);
|
|
176
|
+
}
|
|
177
|
+
catch (retryError) {
|
|
178
|
+
if (this.isAddressExists(retryError)) {
|
|
179
|
+
response = await claimWithNewSignature();
|
|
180
|
+
}
|
|
181
|
+
else if (this.isDefinitiveRejection(retryError)) {
|
|
182
|
+
await pending.abort();
|
|
183
|
+
throw retryError;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw retryError;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.cacheWallet(response.wallet, keys, created.credentialId);
|
|
192
|
+
return { accessToken: response.accessToken, wallet: response.wallet, keys, state: 'unlocked' };
|
|
193
|
+
}
|
|
194
|
+
/** Links another derived address of the unlocked wallet (§8.5); EVM index i or the Bitcoin login address. */
|
|
195
|
+
async link(family, accessToken, keys, evmIndex = 0) {
|
|
196
|
+
const address = family === 'evm' ? keys.evm(evmIndex).address : keys.bitcoinAddress(0, 0).address;
|
|
197
|
+
const signer = new LoginAutoSigner(keys, this.deps.env);
|
|
198
|
+
const signature = signer.sign(family, address, buildLoginMessage(this.deps.env, address));
|
|
199
|
+
const response = await this.deps.api.passkey.link({
|
|
200
|
+
address,
|
|
201
|
+
signature,
|
|
202
|
+
family: family === 'evm' ? 'Evm' : 'Bitcoin',
|
|
203
|
+
path: family === 'evm' ? keys.evm(evmIndex).path : keys.bitcoinAddress(0, 0).path,
|
|
204
|
+
}, accessToken);
|
|
205
|
+
return { accessToken: response.accessToken, address: response.address };
|
|
206
|
+
}
|
|
207
|
+
/** Revokes the wallet by proving the credential one last time (D11). */
|
|
208
|
+
async revoke(reason) {
|
|
209
|
+
const challenge = await this.deps.api.passkey.revokeBegin();
|
|
210
|
+
this.checkRpId(challenge.rpId);
|
|
211
|
+
const assertion = await this.deps.ceremonies.assert({ challenge: base64UrlDecode(challenge.challenge) });
|
|
212
|
+
const wallet = await this.deps.api.passkey.revokeFinish({
|
|
213
|
+
challengeId: challenge.challengeId,
|
|
214
|
+
credential: assertion.assertion,
|
|
215
|
+
reason,
|
|
216
|
+
});
|
|
217
|
+
this.deps.cache?.update(base64UrlEncode(assertion.credentialId), { state: 'revoked' });
|
|
218
|
+
return wallet;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The 12 words, only behind a fresh local challenge (G5); the server is never involved.
|
|
222
|
+
* One stored active passkey is pinned. Several stored passkeys need `credentialId`, so the
|
|
223
|
+
* authenticator cannot return another wallet. No stored passkey stays discoverable.
|
|
224
|
+
*/
|
|
225
|
+
async exportMnemonic(credentialId) {
|
|
226
|
+
const pinned = credentialId ?? this.onlyActiveCredentialId();
|
|
227
|
+
const assertion = await this.deps.ceremonies.assert(pinned ? { credentialId: pinned } : {});
|
|
228
|
+
if (!assertion.prfOutput)
|
|
229
|
+
throw new WalletError('prf-unavailable', 'The passkey cannot derive the seed');
|
|
230
|
+
if (pinned && !equalBytes(assertion.credentialId, pinned)) {
|
|
231
|
+
throw new WalletError('credential-mismatch', 'The authenticator returned a different passkey');
|
|
232
|
+
}
|
|
233
|
+
const keys = keysFromPrf(assertion.prfOutput, this.deps.env);
|
|
234
|
+
try {
|
|
235
|
+
const id = base64UrlEncode(assertion.credentialId);
|
|
236
|
+
const expected = this.deps.cache?.list().find((entry) => entry.credentialId === id)?.publicData?.seedCheckAddress;
|
|
237
|
+
if (expected && !seedCheck(keys.evm(0).address, expected)) {
|
|
238
|
+
throw new WalletError('seed-mismatch', 'The passkey does not match the stored wallet');
|
|
239
|
+
}
|
|
240
|
+
return keys.mnemonic().split(' ');
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
keys.wipe();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Server records must match family, path and address of the local derivations (bitcoin 0/0, Spark, EVM 0..99). */
|
|
247
|
+
verifyServerAddresses(wallet, keys) {
|
|
248
|
+
const local = new Map();
|
|
249
|
+
const bitcoin = keys.bitcoinAddress(0, 0);
|
|
250
|
+
local.set(bitcoin.address.toLowerCase(), { family: 'Bitcoin', path: bitcoin.path });
|
|
251
|
+
const spark = keys.sparkIdentity();
|
|
252
|
+
local.set(spark.address.toLowerCase(), { family: 'Spark', path: spark.path });
|
|
253
|
+
for (let index = 0; index < 100; index++) {
|
|
254
|
+
const account = keys.evm(index);
|
|
255
|
+
local.set(account.address.toLowerCase(), { family: 'Evm', path: account.path });
|
|
256
|
+
}
|
|
257
|
+
return wallet.addresses.map((record) => {
|
|
258
|
+
const expected = local.get(record.address.toLowerCase());
|
|
259
|
+
return {
|
|
260
|
+
address: record.address,
|
|
261
|
+
family: record.family,
|
|
262
|
+
matches: expected !== undefined && expected.family === record.family && expected.path === record.path,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** The one active passkey on this origin. Several active passkeys are not a guess. */
|
|
267
|
+
onlyActiveCredentialId() {
|
|
268
|
+
const active = this.deps.cache?.list().filter((entry) => entry.state === 'active') ?? [];
|
|
269
|
+
if (active.length > 1) {
|
|
270
|
+
throw new WalletError('credential-ambiguous', 'Pass the passkey to export when several are stored');
|
|
271
|
+
}
|
|
272
|
+
if (active.length === 0)
|
|
273
|
+
return undefined;
|
|
274
|
+
return base64UrlDecode(active[0].credentialId);
|
|
275
|
+
}
|
|
276
|
+
async finishAuthentication(challengeId, assertion) {
|
|
277
|
+
try {
|
|
278
|
+
return await this.deps.api.passkey.authenticateFinish({ challengeId, credential: assertion });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error instanceof ApiException && error.statusCode === 403) {
|
|
282
|
+
throw new PasskeyRefusedError({ statusCode: 403, code: error.code ?? error.message, message: error.message });
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** The registration seed proof: EVM seed check address with EIP-191 and the Spark identity with D31. */
|
|
288
|
+
seedProof(keys) {
|
|
289
|
+
const signer = new LoginAutoSigner(keys, this.deps.env);
|
|
290
|
+
const address = keys.evm(0).address;
|
|
291
|
+
const sparkAddress = keys.sparkIdentity().address;
|
|
292
|
+
return {
|
|
293
|
+
address,
|
|
294
|
+
signature: signer.sign('evm', address, buildLoginMessage(this.deps.env, address)),
|
|
295
|
+
sparkAddress,
|
|
296
|
+
sparkSignature: signer.sign('spark', sparkAddress, buildLoginMessage(this.deps.env, sparkAddress)),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** Server accepts at most ten ids per status call; anything past that must be a later request. */
|
|
300
|
+
async activeExcludeIds() {
|
|
301
|
+
const cache = this.deps.cache;
|
|
302
|
+
if (!cache)
|
|
303
|
+
return [];
|
|
304
|
+
const ids = cache.activeIds();
|
|
305
|
+
if (ids.length === 0)
|
|
306
|
+
return [];
|
|
307
|
+
try {
|
|
308
|
+
const known = new Set();
|
|
309
|
+
for (let offset = 0; offset < ids.length; offset += 10) {
|
|
310
|
+
const statuses = await this.deps.api.passkey.credentialStatus(ids.slice(offset, offset + 10));
|
|
311
|
+
for (const status of statuses) {
|
|
312
|
+
if (status.status !== 'Unknown')
|
|
313
|
+
known.add(status.credentialId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return ids.filter((id) => known.has(id));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return ids;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
checkRpId(rpId) {
|
|
323
|
+
if (rpId !== RP_IDS[this.deps.env]) {
|
|
324
|
+
throw new WalletError('wrong-origin', `The server expects RP ID ${rpId}, this wallet uses ${RP_IDS[this.deps.env]}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async confirmOrAbort(pending) {
|
|
328
|
+
try {
|
|
329
|
+
return await pending.confirm();
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
await pending.abort();
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
isAddressExists(error) {
|
|
337
|
+
return (error instanceof ApiException &&
|
|
338
|
+
error.statusCode === 409 &&
|
|
339
|
+
(error.code === 'AddressExists' || error.message === 'AddressExists'));
|
|
340
|
+
}
|
|
341
|
+
isDefinitiveRejection(error) {
|
|
342
|
+
return error instanceof ApiException && error.statusCode >= 400 && error.statusCode < 500;
|
|
343
|
+
}
|
|
344
|
+
cacheWallet(wallet, keys, credentialId) {
|
|
345
|
+
this.deps.cache?.put({
|
|
346
|
+
credentialId: base64UrlEncode(credentialId),
|
|
347
|
+
rpId: RP_IDS[this.deps.env],
|
|
348
|
+
state: wallet.status === 'Active' ? 'active' : 'revoked',
|
|
349
|
+
seedCheckAddress: wallet.seedCheckAddress,
|
|
350
|
+
publicData: seedPublicData(keys),
|
|
351
|
+
backupEligible: wallet.backupEligible,
|
|
352
|
+
createdAt: Date.now(),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=passkey-auth.js.map
|