@waaskey/sdk 0.4.2 → 0.6.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 +113 -3
- package/dist/index.cjs +570 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +653 -3
- package/dist/index.d.ts +653 -3
- package/dist/index.js +562 -22
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -80,14 +80,41 @@ const waaskey = new Waaskey({
|
|
|
80
80
|
baseUrl: process.env.WAASKEY_API_URL!,
|
|
81
81
|
mpc,
|
|
82
82
|
shareStore: new EncryptedShareStore(new MemoryKeyValueStore(), process.env.SHARE_SECRET!),
|
|
83
|
-
|
|
83
|
+
// `concurrency` only helps a WORKER-BACKED core: inline WASM generation shares the calling
|
|
84
|
+
// thread, so overlapping the searches finishes no sooner and delays the first usable prime.
|
|
85
|
+
primePool: new PrimePool(mpc, { concurrency: 4 }),
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
// Off the hot path (boot/idle): pre-generate the primes so create() takes seconds.
|
|
87
89
|
await waaskey.wallets.prewarm('ethereum');
|
|
88
90
|
|
|
89
|
-
const recoveryCode = generateRecoveryCode();
|
|
90
|
-
|
|
91
|
+
const recoveryCode = generateRecoveryCode();
|
|
92
|
+
let mustShowTheCode = true;
|
|
93
|
+
|
|
94
|
+
const wallet = await waaskey.wallets.create(
|
|
95
|
+
{ chain: 'ethereum' },
|
|
96
|
+
{
|
|
97
|
+
backup: {
|
|
98
|
+
recoveryCode,
|
|
99
|
+
totpSecret,
|
|
100
|
+
email,
|
|
101
|
+
// Enrols a passkey INSTEAD of the code where the device can produce a PRF, and the code
|
|
102
|
+
// where it cannot. Never both: the recovery gate is an AND of every enrolled factor, so a
|
|
103
|
+
// wallet holding both would be bricked by losing either one.
|
|
104
|
+
passkeyEnroller: () => new PasskeyPrfSecretProvider().enroll({ userName: email }),
|
|
105
|
+
},
|
|
106
|
+
// Which one actually happened — the app cannot infer it, because enrolment can fall back for
|
|
107
|
+
// reasons it cannot see.
|
|
108
|
+
onRecoveryEnrolled: ({ strongFactor }) => {
|
|
109
|
+
mustShowTheCode = strongFactor === 'recovery_code';
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Show the code ONLY if it is what guards the wallet. Telling a passkey user to write down a code
|
|
115
|
+
// nobody will ever ask them for is how a recovery screen teaches people to ignore it.
|
|
116
|
+
if (mustShowTheCode) showRecoveryCode(recoveryCode);
|
|
117
|
+
|
|
91
118
|
const signature = await wallet.sign(digestHex);
|
|
92
119
|
```
|
|
93
120
|
|
|
@@ -168,6 +195,8 @@ const { items } = await wallet.signatures({ page: 1, limit: 20 });
|
|
|
168
195
|
| `wallet.send(params, options?)` | Build + MPC-sign a tx. Returns the **signed raw tx** (you broadcast). |
|
|
169
196
|
| `wallet.signatures(query?, options?)` | The wallet's signing activity (paginated). |
|
|
170
197
|
| `waaskey.broadcast(signedTx, { rpcUrl })` | **Optional** best-effort submit of a signed tx from your own node. |
|
|
198
|
+
| `waaskey.sessionKeys.list(walletId?, signal?)` | The tenant's active session keys (delegated, scoped permissions). |
|
|
199
|
+
| `waaskey.sessionKeys.send(params, signal?)` | Sign a call with a session key and submit it as a UserOperation. |
|
|
171
200
|
|
|
172
201
|
`create` / `sign` accept `{ signal }` for cancellation; `create` also takes
|
|
173
202
|
`{ waitForActive?, activationTimeoutMs?, pollIntervalMs? }`.
|
|
@@ -176,6 +205,87 @@ Errors are thrown as `WaaskeyError` with a typed `code` (e.g. `unauthorized`,
|
|
|
176
205
|
`forbidden`, `validation`, `device_core_required`, `keygen_failed`, `aborted`) plus
|
|
177
206
|
`status?` and `details?` — branch on `error.code`, never on the message text.
|
|
178
207
|
|
|
208
|
+
### Session keys — acting with a delegated permission
|
|
209
|
+
|
|
210
|
+
A session key is a scoped, time-limited signer a wallet granted: certain contracts, certain
|
|
211
|
+
selectors, up to a value, a bounded number of calls. `send` prepares the UserOperation server-side,
|
|
212
|
+
signs its hash with the key, and submits exactly those fields — the gas and paymaster values are
|
|
213
|
+
part of what the signature covers, so a locally-guessed op would produce a signature for an
|
|
214
|
+
operation the bundler never sees.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const { userOpHash } = await waaskey.sessionKeys.send({
|
|
218
|
+
sessionKeyId,
|
|
219
|
+
privateKey, // the granted key's PRIVATE half — signs locally, never sent
|
|
220
|
+
sender, // the smart account it acts for
|
|
221
|
+
chainId: 'evm:1',
|
|
222
|
+
callData, // ABI-encoded execute calldata
|
|
223
|
+
contract, // checked against the key's allowlist
|
|
224
|
+
selector,
|
|
225
|
+
valueWei: '0x0', // checked against the key's cap
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Asking a user for a permission
|
|
230
|
+
|
|
231
|
+
An app that wants a permission asks for one and sends the user to their wallet to answer:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const waaskey = new Waaskey({ apiKey, baseUrl, walletUrl: 'https://wallet.example.com' });
|
|
235
|
+
|
|
236
|
+
const req = await waaskey.sessionKeys.request({
|
|
237
|
+
walletId,
|
|
238
|
+
label: 'Dungeon Quest — one battle',
|
|
239
|
+
scope: { allowedContracts: [game], maxCalls: 50, periodSeconds: 3600, maxCallsPerPeriod: 10 },
|
|
240
|
+
expiresAt: new Date(Date.now() + 24 * 3600_000),
|
|
241
|
+
requesterId, // your registered app — the wallet shows the domain you proved you own
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
showQrCode(req.url); // or open it; this is where the user answers
|
|
245
|
+
|
|
246
|
+
const { status, key, narrowed } = await waaskey.sessionKeys.waitForDecision(req.sessionKeyId, {
|
|
247
|
+
asked: scope, // supply it to learn whether the user accepted less
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**The private key never travels.** `request` generates the keypair locally and registers only its
|
|
252
|
+
public half, so nothing secret is in the request, in the link, or in the wallet — `req.privateKey`
|
|
253
|
+
is yours to keep, and there is no way to fetch it later. That is also why the user is never asked to
|
|
254
|
+
copy a key out of a wallet UI.
|
|
255
|
+
|
|
256
|
+
**The user may narrow the request, never widen it.** They can shorten the duration or cut the
|
|
257
|
+
amounts; the server refuses anything wider than what was asked. `narrowed: true` says they accepted
|
|
258
|
+
less, so your app can adapt rather than failing opaquely on its first call outside the smaller scope.
|
|
259
|
+
|
|
260
|
+
`waitForDecision` polls rather than waiting for a redirect back — a user answering on their phone,
|
|
261
|
+
or closing the tab, would otherwise strand you with no answer. A refusal comes back as
|
|
262
|
+
`status: 'declined'` (an answer, not an error); a request nobody answers throws
|
|
263
|
+
`permission_request_timeout`.
|
|
264
|
+
|
|
265
|
+
**Two credentials, and they are not interchangeable.** The client's `apiKey` must carry the `SIGN`
|
|
266
|
+
scope — the session private key alone does not reach the API — while the private key is what
|
|
267
|
+
actually authorizes the operation on-chain. This surprises everyone once.
|
|
268
|
+
|
|
269
|
+
A refused call says WHY, because the fixes are opposite: `permission_expired`,
|
|
270
|
+
`permission_revoked`, `permission_scope`, `permission_value_exceeded`, `permission_exhausted`,
|
|
271
|
+
`permission_rate_limited`. The first three mean "re-request the permission", `permission_scope` and
|
|
272
|
+
`permission_value_exceeded` mean "change the call", and `permission_rate_limited` means "the same
|
|
273
|
+
call succeeds in the next period" — a grant may cap both how much it is worth in total and how fast
|
|
274
|
+
it may be spent, so a spent period is not a spent permission.
|
|
275
|
+
|
|
276
|
+
A listed permission carries the name it was granted under (`label`), the app that asked for it
|
|
277
|
+
(`requesterId`) and the policy paying its gas (`paymasterPolicyId`). Resolve the requester to show
|
|
278
|
+
who is asking:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const app = await waaskey.sessionKeys.requester(key.requesterId!);
|
|
282
|
+
// app.origin is identity ONLY when app.status === 'verified' — that means the domain itself
|
|
283
|
+
// published a document naming the tenant. `name` and `iconUrl` are the app's own claims.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The code comes from the server, not from its wording: it is read off the refusal's `code` field,
|
|
287
|
+
and matching the message is only a fallback for a server that predates it.
|
|
288
|
+
|
|
179
289
|
### Secure share storage
|
|
180
290
|
|
|
181
291
|
The device key share is the user's half of the key. `EncryptedShareStore` seals it
|