decane-connect-kit 2.1.0 → 2.2.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 +76 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +76 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -81,6 +81,17 @@ interface SocialConfig {
|
|
|
81
81
|
apiBaseUrl?: string;
|
|
82
82
|
chains?: string[];
|
|
83
83
|
rpcUrls?: Record<string, string>;
|
|
84
|
+
/**
|
|
85
|
+
* Require a fresh WebAuthn assertion for every signature rather than letting
|
|
86
|
+
* an unlocked session sign freely until it expires. Enforced inside the TEE.
|
|
87
|
+
*
|
|
88
|
+
* Does not change the session lifetime — the session stays warm (no
|
|
89
|
+
* re-attestation, no re-auth) and authority is re-proven per signature
|
|
90
|
+
* instead. Default false. Needs a passkey; PIN-only wallets have no
|
|
91
|
+
* authenticator to assert with. See SocialConfig's counterpart in
|
|
92
|
+
* social/types.ts for the full caveats.
|
|
93
|
+
*/
|
|
94
|
+
requireAssertionPerSignature?: boolean;
|
|
84
95
|
/**
|
|
85
96
|
* Sign in with a token from your own auth system (Auth0, Clerk, Firebase,
|
|
86
97
|
* Cognito, or your own backend) — surfaced as a button in the built-in
|
|
@@ -283,6 +294,25 @@ interface DecaneConnectConfig {
|
|
|
283
294
|
apiBaseUrl?: string;
|
|
284
295
|
sessionDurationMinutes?: number;
|
|
285
296
|
promptPin?: () => Promise<string>;
|
|
297
|
+
/**
|
|
298
|
+
* Require a fresh WebAuthn assertion for every signature, instead of letting
|
|
299
|
+
* an unlocked session sign freely until it expires. The TEE session stays
|
|
300
|
+
* warm — this costs one authenticator prompt per signature, not a re-auth or
|
|
301
|
+
* re-attestation — and the check is enforced inside the enclave, not here.
|
|
302
|
+
*
|
|
303
|
+
* Default false, which preserves the session-signing model: unlock once, sign
|
|
304
|
+
* for the session's lifetime with no further prompts.
|
|
305
|
+
*
|
|
306
|
+
* Two things worth knowing before turning this on:
|
|
307
|
+
* - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
|
|
308
|
+
* prompts, not one.
|
|
309
|
+
* - The policy is declared by the client at session open, so it hardens a
|
|
310
|
+
* leaked session handle (useless without the authenticator) but a fully
|
|
311
|
+
* compromised page could open a session without it. Making it
|
|
312
|
+
* unbypassable requires the policy to come from the server per user or
|
|
313
|
+
* per project; that is not implemented yet.
|
|
314
|
+
*/
|
|
315
|
+
requireAssertionPerSignature?: boolean;
|
|
286
316
|
rpId?: string;
|
|
287
317
|
rpName?: string;
|
|
288
318
|
rpcUrls?: Record<string, string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,17 @@ interface SocialConfig {
|
|
|
81
81
|
apiBaseUrl?: string;
|
|
82
82
|
chains?: string[];
|
|
83
83
|
rpcUrls?: Record<string, string>;
|
|
84
|
+
/**
|
|
85
|
+
* Require a fresh WebAuthn assertion for every signature rather than letting
|
|
86
|
+
* an unlocked session sign freely until it expires. Enforced inside the TEE.
|
|
87
|
+
*
|
|
88
|
+
* Does not change the session lifetime — the session stays warm (no
|
|
89
|
+
* re-attestation, no re-auth) and authority is re-proven per signature
|
|
90
|
+
* instead. Default false. Needs a passkey; PIN-only wallets have no
|
|
91
|
+
* authenticator to assert with. See SocialConfig's counterpart in
|
|
92
|
+
* social/types.ts for the full caveats.
|
|
93
|
+
*/
|
|
94
|
+
requireAssertionPerSignature?: boolean;
|
|
84
95
|
/**
|
|
85
96
|
* Sign in with a token from your own auth system (Auth0, Clerk, Firebase,
|
|
86
97
|
* Cognito, or your own backend) — surfaced as a button in the built-in
|
|
@@ -283,6 +294,25 @@ interface DecaneConnectConfig {
|
|
|
283
294
|
apiBaseUrl?: string;
|
|
284
295
|
sessionDurationMinutes?: number;
|
|
285
296
|
promptPin?: () => Promise<string>;
|
|
297
|
+
/**
|
|
298
|
+
* Require a fresh WebAuthn assertion for every signature, instead of letting
|
|
299
|
+
* an unlocked session sign freely until it expires. The TEE session stays
|
|
300
|
+
* warm — this costs one authenticator prompt per signature, not a re-auth or
|
|
301
|
+
* re-attestation — and the check is enforced inside the enclave, not here.
|
|
302
|
+
*
|
|
303
|
+
* Default false, which preserves the session-signing model: unlock once, sign
|
|
304
|
+
* for the session's lifetime with no further prompts.
|
|
305
|
+
*
|
|
306
|
+
* Two things worth knowing before turning this on:
|
|
307
|
+
* - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
|
|
308
|
+
* prompts, not one.
|
|
309
|
+
* - The policy is declared by the client at session open, so it hardens a
|
|
310
|
+
* leaked session handle (useless without the authenticator) but a fully
|
|
311
|
+
* compromised page could open a session without it. Making it
|
|
312
|
+
* unbypassable requires the policy to come from the server per user or
|
|
313
|
+
* per project; that is not implemented yet.
|
|
314
|
+
*/
|
|
315
|
+
requireAssertionPerSignature?: boolean;
|
|
286
316
|
rpId?: string;
|
|
287
317
|
rpName?: string;
|
|
288
318
|
rpcUrls?: Record<string, string>;
|
package/dist/index.js
CHANGED
|
@@ -2200,6 +2200,12 @@ var TeeClient = class {
|
|
|
2200
2200
|
this.teeUrl = teeUrl;
|
|
2201
2201
|
this.handle = null;
|
|
2202
2202
|
this.attested = false;
|
|
2203
|
+
/** Mirrors what the signer confirmed for this session, not what we asked for. */
|
|
2204
|
+
this.assertionPerSignature = false;
|
|
2205
|
+
}
|
|
2206
|
+
/** Whether this session actually got per-signature assertions. */
|
|
2207
|
+
requiresAssertionPerSignature() {
|
|
2208
|
+
return this.assertionPerSignature;
|
|
2203
2209
|
}
|
|
2204
2210
|
async ensureAttested() {
|
|
2205
2211
|
if (this.attested) return;
|
|
@@ -2215,16 +2221,20 @@ var TeeClient = class {
|
|
|
2215
2221
|
* couldn't derive them (older signer build, or a transient share-fetch
|
|
2216
2222
|
* failure); callers fall back to the backend's addresses in that case.
|
|
2217
2223
|
*/
|
|
2218
|
-
async openSession(deviceShare, jwt) {
|
|
2224
|
+
async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
|
|
2219
2225
|
await this.ensureAttested();
|
|
2220
2226
|
const res = await fetch(`${this.teeUrl}/signer/session`, {
|
|
2221
2227
|
method: "POST",
|
|
2222
2228
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2223
|
-
body: JSON.stringify({
|
|
2229
|
+
body: JSON.stringify({
|
|
2230
|
+
deviceShare: bytesToBase64(deviceShare),
|
|
2231
|
+
requireAssertionPerSignature
|
|
2232
|
+
})
|
|
2224
2233
|
});
|
|
2225
2234
|
if (!res.ok) throw new Error("session open failed");
|
|
2226
2235
|
const body = await res.json();
|
|
2227
2236
|
this.handle = body.handle;
|
|
2237
|
+
this.assertionPerSignature = body.requireAssertionPerSignature === true;
|
|
2228
2238
|
return body.addresses ?? null;
|
|
2229
2239
|
}
|
|
2230
2240
|
/**
|
|
@@ -2315,14 +2325,33 @@ var TeeClient = class {
|
|
|
2315
2325
|
hasSession() {
|
|
2316
2326
|
return this.handle !== null;
|
|
2317
2327
|
}
|
|
2318
|
-
|
|
2328
|
+
/**
|
|
2329
|
+
* `jwt` is only needed when this session requires per-signature assertions —
|
|
2330
|
+
* it authenticates the challenge request. Passed per call rather than cached
|
|
2331
|
+
* on the instance deliberately: the backend's JWTs are shorter-lived than the
|
|
2332
|
+
* TEE session TTL, so a stored copy would go stale mid-session.
|
|
2333
|
+
*/
|
|
2334
|
+
async sign(req, jwt) {
|
|
2319
2335
|
if (!this.handle) throw new Error("no session");
|
|
2336
|
+
let gate = null;
|
|
2337
|
+
if (this.assertionPerSignature) {
|
|
2338
|
+
if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
|
|
2339
|
+
gate = await this.completeAssertion(jwt);
|
|
2340
|
+
}
|
|
2320
2341
|
const res = await fetch(`${this.teeUrl}/signer/sign`, {
|
|
2321
2342
|
method: "POST",
|
|
2322
2343
|
headers: { "Content-Type": "application/json" },
|
|
2323
|
-
body: JSON.stringify({
|
|
2344
|
+
body: JSON.stringify({
|
|
2345
|
+
handle: this.handle,
|
|
2346
|
+
...req,
|
|
2347
|
+
...gate ? { ...gate, origin: window.location.origin } : {}
|
|
2348
|
+
})
|
|
2324
2349
|
});
|
|
2325
2350
|
if (res.status === 401) {
|
|
2351
|
+
const body = await res.clone().json().catch(() => ({}));
|
|
2352
|
+
if (body?.error === "assertion_required") {
|
|
2353
|
+
throw new ExportNotAuthorizedError();
|
|
2354
|
+
}
|
|
2326
2355
|
this.handle = null;
|
|
2327
2356
|
throw new SessionExpiredError();
|
|
2328
2357
|
}
|
|
@@ -2345,10 +2374,11 @@ var TeeClient = class {
|
|
|
2345
2374
|
// src/social/session.ts
|
|
2346
2375
|
init_errors();
|
|
2347
2376
|
var SessionManager = class {
|
|
2348
|
-
constructor(events, durationMs, teeUrl) {
|
|
2377
|
+
constructor(events, durationMs, teeUrl, requireAssertionPerSignature = false) {
|
|
2349
2378
|
this.events = events;
|
|
2350
2379
|
this.durationMs = durationMs;
|
|
2351
2380
|
this.teeUrl = teeUrl;
|
|
2381
|
+
this.requireAssertionPerSignature = requireAssertionPerSignature;
|
|
2352
2382
|
this.tee = null;
|
|
2353
2383
|
this.evmAddress = null;
|
|
2354
2384
|
this.solanaAddress = null;
|
|
@@ -2368,7 +2398,18 @@ var SessionManager = class {
|
|
|
2368
2398
|
*/
|
|
2369
2399
|
async start(deviceShare, jwt, fallback) {
|
|
2370
2400
|
this.tee = new TeeClient(this.teeUrl);
|
|
2371
|
-
const verified = await this.tee.openSession(
|
|
2401
|
+
const verified = await this.tee.openSession(
|
|
2402
|
+
deviceShare,
|
|
2403
|
+
jwt,
|
|
2404
|
+
this.requireAssertionPerSignature
|
|
2405
|
+
);
|
|
2406
|
+
if (this.requireAssertionPerSignature && !this.tee.requiresAssertionPerSignature()) {
|
|
2407
|
+
void this.tee.closeSession();
|
|
2408
|
+
this.tee = null;
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
"requireAssertionPerSignature was requested but this signer does not support it \u2014 refusing to open a session with weaker authority than configured."
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2372
2413
|
const addresses = verified ?? fallback;
|
|
2373
2414
|
if (!addresses) {
|
|
2374
2415
|
this.tee = null;
|
|
@@ -2597,15 +2638,15 @@ async function verifyEmailCode(api, email, code) {
|
|
|
2597
2638
|
|
|
2598
2639
|
// src/social/signers/evm.ts
|
|
2599
2640
|
import { keccak256, Transaction, resolveProperties } from "ethers";
|
|
2600
|
-
async function signEvmTransaction(tee, chain, tx) {
|
|
2641
|
+
async function signEvmTransaction(tee, chain, tx, jwt) {
|
|
2601
2642
|
const resolved = await resolveProperties(tx);
|
|
2602
2643
|
const unsigned = Transaction.from(resolved).unsignedSerialized;
|
|
2603
2644
|
const payload = unsigned.startsWith("0x") ? unsigned.slice(2) : unsigned;
|
|
2604
2645
|
const txHash = keccak256("0x" + payload);
|
|
2605
|
-
return tee.sign({ chain, kind: "transaction", payload, txHash });
|
|
2646
|
+
return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
|
|
2606
2647
|
}
|
|
2607
|
-
async function signEvmMessage(tee, chain, message) {
|
|
2608
|
-
return tee.sign({ chain, kind: "message", payload: message });
|
|
2648
|
+
async function signEvmMessage(tee, chain, message, jwt) {
|
|
2649
|
+
return tee.sign({ chain, kind: "message", payload: message }, jwt);
|
|
2609
2650
|
}
|
|
2610
2651
|
|
|
2611
2652
|
// src/social/signers/solana.ts
|
|
@@ -2618,13 +2659,13 @@ function b64(bytes) {
|
|
|
2618
2659
|
function unb64(s) {
|
|
2619
2660
|
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
2620
2661
|
}
|
|
2621
|
-
async function signSolanaTransaction(tee, chain, tx) {
|
|
2662
|
+
async function signSolanaTransaction(tee, chain, tx, jwt) {
|
|
2622
2663
|
const bytes = tx instanceof VTx ? tx.serialize() : tx.serialize({ requireAllSignatures: false });
|
|
2623
|
-
const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) });
|
|
2664
|
+
const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) }, jwt);
|
|
2624
2665
|
return unb64(signed);
|
|
2625
2666
|
}
|
|
2626
|
-
async function signSolanaMessage(tee, chain, message) {
|
|
2627
|
-
const sig = await tee.sign({ chain, kind: "message", payload: b64(message) });
|
|
2667
|
+
async function signSolanaMessage(tee, chain, message, jwt) {
|
|
2668
|
+
const sig = await tee.sign({ chain, kind: "message", payload: b64(message) }, jwt);
|
|
2628
2669
|
return unb64(sig);
|
|
2629
2670
|
}
|
|
2630
2671
|
|
|
@@ -2897,7 +2938,12 @@ async function createDecaneConnect(config) {
|
|
|
2897
2938
|
const events = new EventEmitter();
|
|
2898
2939
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
2899
2940
|
const storage = new Storage();
|
|
2900
|
-
const session = new SessionManager(
|
|
2941
|
+
const session = new SessionManager(
|
|
2942
|
+
events,
|
|
2943
|
+
durationMs,
|
|
2944
|
+
teeUrl,
|
|
2945
|
+
config.requireAssertionPerSignature === true
|
|
2946
|
+
);
|
|
2901
2947
|
let auth = null;
|
|
2902
2948
|
let persisted = loadPersisted(PERSIST_KEY);
|
|
2903
2949
|
async function pinSetup() {
|
|
@@ -3296,11 +3342,11 @@ async function createDecaneConnect(config) {
|
|
|
3296
3342
|
async signMessage(req) {
|
|
3297
3343
|
const tee = session.requireActive();
|
|
3298
3344
|
if (req.chain.startsWith("evm:")) {
|
|
3299
|
-
return signEvmMessage(tee, req.chain, req.message);
|
|
3345
|
+
return signEvmMessage(tee, req.chain, req.message, auth?.jwt);
|
|
3300
3346
|
}
|
|
3301
3347
|
if (req.chain.startsWith("solana:")) {
|
|
3302
3348
|
const msg = typeof req.message === "string" ? new TextEncoder().encode(req.message) : req.message;
|
|
3303
|
-
const sig = await signSolanaMessage(tee, req.chain, msg);
|
|
3349
|
+
const sig = await signSolanaMessage(tee, req.chain, msg, auth?.jwt);
|
|
3304
3350
|
return bufferToB64u(sig);
|
|
3305
3351
|
}
|
|
3306
3352
|
throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
|
|
@@ -3348,7 +3394,7 @@ async function createDecaneConnect(config) {
|
|
|
3348
3394
|
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n
|
|
3349
3395
|
} : { gasPrice: req.gasPrice ?? feeData.gasPrice ?? void 0 }
|
|
3350
3396
|
};
|
|
3351
|
-
const signedTx = await signEvmTransaction(tee, req.chain, tx);
|
|
3397
|
+
const signedTx = await signEvmTransaction(tee, req.chain, tx, auth?.jwt);
|
|
3352
3398
|
const txResponse = await provider.broadcastTransaction(signedTx);
|
|
3353
3399
|
return txResponse.hash;
|
|
3354
3400
|
},
|
|
@@ -3361,7 +3407,7 @@ async function createDecaneConnect(config) {
|
|
|
3361
3407
|
tx = Transaction2.from(serializedTx);
|
|
3362
3408
|
}
|
|
3363
3409
|
const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
3364
|
-
return signSolanaTransaction(tee, solChain, tx);
|
|
3410
|
+
return signSolanaTransaction(tee, solChain, tx, auth?.jwt);
|
|
3365
3411
|
},
|
|
3366
3412
|
async disconnect() {
|
|
3367
3413
|
session.end();
|
|
@@ -5205,7 +5251,15 @@ function DecaneKit({ config, children }) {
|
|
|
5205
5251
|
useEffect3(() => {
|
|
5206
5252
|
if (!hasSocial(mode) || !config?.social || socialInitialized.current) return;
|
|
5207
5253
|
socialInitialized.current = true;
|
|
5208
|
-
const {
|
|
5254
|
+
const {
|
|
5255
|
+
apiKey,
|
|
5256
|
+
authMethods,
|
|
5257
|
+
promptPin,
|
|
5258
|
+
apiBaseUrl,
|
|
5259
|
+
chains,
|
|
5260
|
+
rpcUrls,
|
|
5261
|
+
requireAssertionPerSignature
|
|
5262
|
+
} = config.social;
|
|
5209
5263
|
createDecaneConnect({
|
|
5210
5264
|
appId: config.appId,
|
|
5211
5265
|
apiKey,
|
|
@@ -5213,7 +5267,8 @@ function DecaneKit({ config, children }) {
|
|
|
5213
5267
|
authMethods: authMethods ?? ["google", "email"],
|
|
5214
5268
|
chains: chains ?? ["evm:1", "solana:mainnet"],
|
|
5215
5269
|
rpcUrls,
|
|
5216
|
-
promptPin
|
|
5270
|
+
promptPin,
|
|
5271
|
+
requireAssertionPerSignature
|
|
5217
5272
|
}).then((sdk) => {
|
|
5218
5273
|
socialRef.current = sdk;
|
|
5219
5274
|
sdk.on(
|