@dexterai/vault 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 +62 -16
- package/dist/connect/index.cjs +455 -0
- package/dist/connect/index.d.cts +145 -0
- package/dist/connect/index.d.ts +145 -0
- package/dist/connect/index.js +419 -0
- package/dist/constants/index.cjs +13 -1
- package/dist/constants/index.d.cts +11 -1
- package/dist/constants/index.d.ts +11 -1
- package/dist/constants/index.js +12 -1
- package/dist/counterfactual.cjs +12 -1
- package/dist/counterfactual.js +12 -1
- package/dist/factoring/index.cjs +246 -0
- package/dist/factoring/index.d.cts +79 -0
- package/dist/factoring/index.d.ts +79 -0
- package/dist/factoring/index.js +220 -0
- package/dist/idl/dexter_vault.json +976 -1
- package/dist/index.cjs +12 -1
- package/dist/index.js +12 -1
- package/dist/instructions/index.cjs +249 -4400
- package/dist/instructions/index.d.cts +172 -1
- package/dist/instructions/index.d.ts +172 -1
- package/dist/instructions/index.js +235 -4396
- package/dist/kit/index.cjs +67 -0
- package/dist/kit/index.d.cts +13 -0
- package/dist/kit/index.d.ts +13 -0
- package/dist/kit/index.js +41 -0
- package/dist/messages/index.cjs +11 -1
- package/dist/messages/index.js +11 -1
- package/dist/precompile/index.cjs +11 -1
- package/dist/precompile/index.js +11 -1
- package/dist/tab/index.cjs +640 -0
- package/dist/tab/index.d.cts +145 -0
- package/dist/tab/index.d.ts +145 -0
- package/dist/tab/index.js +631 -0
- package/package.json +23 -1
package/README.md
CHANGED
|
@@ -21,11 +21,53 @@
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
<!-- GTM-DRAFT: product framing/wording pending GTM agent review before publish -->
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Open a tab for your agent
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
You open a tab with a hard limit. Your agent spends against it, charge by charge, with no signature prompt per charge. When the work is done you settle and the tab closes. The spending limit is enforced by the Solana program at consensus, not by this SDK and not by Dexter. The SDK never holds a key that can overspend it, and you can verify the cap on-chain yourself.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { openTab, settleTab, readTabMeter } from '@dexterai/vault/tab';
|
|
32
|
+
|
|
33
|
+
// arm a tab with a chain-enforced cap
|
|
34
|
+
const open = await openTab({ vaultPda, amount: 5_000_000n, dexterAuthority });
|
|
35
|
+
|
|
36
|
+
// settle a streamed micro-charge (composes precompile + settle + transfer)
|
|
37
|
+
const settle = await settleTab({
|
|
38
|
+
connection, vaultPda, swigAddress, channelId,
|
|
39
|
+
cumulativeAmount, sequenceNumber, sessionSigner, sellerAta, feePayer, dexterAuthority,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// read remaining headroom (the chain is the real guard)
|
|
43
|
+
const meter = await readTabMeter(connection, vaultPda);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That is the whole product loop: `openTab` arms a capped tab, `settleTab` records each streamed charge against it, `readTabMeter` reports the headroom left. The buyer's USDC never leaves their wallet while the tab runs; the program gates their exit until the tab settles. The closest familiar shape is an auth-and-capture credit-card hold, except the hold is enforced on-chain instead of by a processor.
|
|
47
|
+
|
|
48
|
+
A tab can also spend **past** the user's balance, backed by a financier's standby capital: non-custodial, and structured so the buyer cannot rug the financier and the financier cannot seize more than the agreed bound. Every guard in that path is proven on Solana mainnet, including a real draw, a real repayment, a real default-and-seize, and every anti-rug rejection. Same import, three more verbs:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { drawCredit, repayCredit, seizeCollateral } from '@dexterai/vault/tab';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Credit is not a separate product bolted onto a tab. It is a tab that can spend past its balance. That is why it lives in the same import.
|
|
55
|
+
|
|
56
|
+
> **Two sides.** This package is the buyer side. The seller side (verify vouchers, meter consumption, accept payment in about ten lines) lives in `@dexterai/x402`. Together they cover both halves of agent payments on Solana.
|
|
57
|
+
|
|
58
|
+
Every `./tab` verb returns `TransactionInstruction[]`, so you own signing, fees, and sending.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Under the hood: the primitives
|
|
63
|
+
|
|
64
|
+
If the four `./tab` verbs are all you need, you can stop reading here. The rest of this package is the low-level surface those verbs are built from, exposed for the servers that assemble their own transactions.
|
|
65
|
+
|
|
66
|
+
The `dexter-vault` Solana program is a non-custodial passkey-rooted vault: WebAuthn signs every spend, a programmatic Swig role makes the unruggable streaming channel possible, and the entire spend path goes through the vault program, with no master key, no escrow, and no trust.
|
|
67
|
+
|
|
68
|
+
This package is the TypeScript that talks to it. Every byte the on-chain program checks lives here, in exactly one file each: instruction discriminators, the 188-byte V2 session-registration message, the 128-byte revocation message, the 44-byte voucher payload, and the vault account layout. Three repos used to hand-roll these primitives and one of them missed a role; that bug is now structurally impossible.
|
|
69
|
+
|
|
70
|
+
The `./tab` verbs above compose these primitives. The tiers stack on top of the same building blocks: the streaming tab (`settle_tab_voucher`), the credit tab (`draw_credit` / `repay_credit` / `seize_collateral`), the LockedClaim crystallized tier (`@dexterai/vault/instructions`), and factoring / instant payout (`@dexterai/vault/factoring`).
|
|
29
71
|
|
|
30
72
|
If you are about to hand-roll a vault instruction builder, a precompile message, a Swig role list, or a vault account decoder, **stop and import from here instead**.
|
|
31
73
|
|
|
@@ -37,7 +79,7 @@ If you are about to hand-roll a vault instruction builder, a precompile message,
|
|
|
37
79
|
npm install @dexterai/vault
|
|
38
80
|
```
|
|
39
81
|
|
|
40
|
-
|
|
82
|
+
Targets the `dexter-vault` V5 program (21 Anchor discriminators including `prove_passkey`, `settle_tab_voucher`, the session-key register/revoke pair, the LockedClaim set, and the credit set `open_standby` / `draw_credit` / `repay_credit` / `seize_collateral`).
|
|
41
83
|
|
|
42
84
|
---
|
|
43
85
|
|
|
@@ -63,7 +105,7 @@ const bundle = await buildSwigCreationBundle({
|
|
|
63
105
|
const tx = new Transaction().add(...bundle.instructions);
|
|
64
106
|
```
|
|
65
107
|
|
|
66
|
-
The 4-role design
|
|
108
|
+
The 4-role design (role 0 bootstrap, role 1 `ProgramExec(finalize_withdrawal)`, role 2 session master, role 3 `ProgramExec(settle_tab_voucher)`) lives in exactly one function. Tests in this repo lock the role list against the on-chain Anchor discriminators.
|
|
67
109
|
|
|
68
110
|
### Settle a Tab voucher on chain (facilitator-side)
|
|
69
111
|
|
|
@@ -141,14 +183,18 @@ Each subpath is a tree-shakeable entry point. Pull only what you need.
|
|
|
141
183
|
|---|---|
|
|
142
184
|
| `@dexterai/vault` | Re-exports `types` + `counterfactual` for convenience |
|
|
143
185
|
| `@dexterai/vault/types` | `VaultState`, `VaultStateFull`, `ActiveSession`, `PendingWithdrawal`, `SessionKey`, `SessionScope`, `SignedVoucher`, `VoucherPayload`, `AtomicAmount`, `HumanAmount`, `TabNetworkId` |
|
|
144
|
-
| `@dexterai/vault/constants` | `DEXTER_VAULT_PROGRAM_ID`, `SWIG_PROGRAM_ID`, `USDC_MAINNET`/`USDC_DEVNET`, all
|
|
186
|
+
| `@dexterai/vault/constants` | `DEXTER_VAULT_PROGRAM_ID`, `SWIG_PROGRAM_ID`, `USDC_MAINNET`/`USDC_DEVNET`, all 21 `DISCRIMINATORS`, `LOCKED_CLAIM_SEED`, `OTS_SESSION_REGISTER_V1_DOMAIN`, `OTS_SESSION_REGISTER_V2_DOMAIN`, `OTS_SESSION_REVOKE_V1_DOMAIN` |
|
|
145
187
|
| `@dexterai/vault/instructions` | Every builder: `buildInitializeVaultInstruction`, `buildSetSwigInstruction`, `buildRegisterSessionKeyInstruction`, `buildRevokeSessionKeyInstruction`, `buildSettleVoucherInstruction`, `buildSettleTabVoucherInstruction`, `buildRequestWithdrawalInstruction`, `buildFinalizeWithdrawalInstruction`, `buildForceReleaseInstruction`, `buildRotatePasskeyInstruction`, `buildRotateDexterAuthorityInstruction`, `buildProvePasskeyInstruction`, and the canonical `buildSwigCreationBundle` + `expectedSwigAddressFor` + `verifySwigIsOurs` |
|
|
146
|
-
| `@dexterai/vault/messages` | `sessionRegisterMessage` (
|
|
188
|
+
| `@dexterai/vault/messages` | `sessionRegisterMessage` (188 bytes, V2), `sessionRevokeMessage` (128 bytes), `voucherPayloadMessage` / `buildVoucherMessage` (44 bytes), `buildSetSwigOperationMessage` |
|
|
147
189
|
| `@dexterai/vault/reader` | `readVaultOnchain` (slim), `readVaultFull` (with active session) |
|
|
148
190
|
| `@dexterai/vault/precompile` | `buildSecp256r1VerifyInstruction`, `buildPrecompileMessage`, `buildEd25519VerifyInstruction` |
|
|
149
191
|
| `@dexterai/vault/counterfactual` | `deriveCounterfactualAddresses` |
|
|
150
192
|
| `@dexterai/vault/signers` | `Ed25519Signer`, `PasskeySigner` interfaces |
|
|
151
193
|
| `@dexterai/vault/signers/node` | `NodeEd25519Signer` (tweetnacl-backed) |
|
|
194
|
+
| `@dexterai/vault/signers/browser` | `WebAuthnAssertion` (pure-browser P-256 passkey ceremony, shipped 0.2.0), `derSignatureToCompactLowS` |
|
|
195
|
+
| `@dexterai/vault/tab` | Product layer: `openTab`, `settleTab`, `readTabMeter`, `drawCredit`, `repayCredit`, `seizeCollateral`, `defaultAssembleSignV2` |
|
|
196
|
+
| `@dexterai/vault/factoring` | `computeFactoringSplit`, `buildInstantPayoutInstructions` (settle a LockedClaim and split the payout) |
|
|
197
|
+
| `@dexterai/vault/kit` | `kitInstructionsToWeb3`, `getRpc` (Swig-kit↔web3 bridge) |
|
|
152
198
|
|
|
153
199
|
---
|
|
154
200
|
|
|
@@ -156,7 +202,7 @@ Each subpath is a tree-shakeable entry point. Pull only what you need.
|
|
|
156
202
|
|
|
157
203
|
Three places used to hand-roll the same protocol: `dexter-api/src/vault/`, `dexter-facilitator/src/vault/`, and `dexter-vault/tests/`. One of them added role 3 (`ProgramExec` for `settle_tab_voucher`); two didn't. The end-to-end Tab settle smoke kept failing with `Role not found for ID: 3` and it ate hours of debugging on 2026-06-02 before anyone noticed the drift.
|
|
158
204
|
|
|
159
|
-
This package is the structural fix. The canonical 4-role Swig provisioner, every instruction builder, every byte-precise message encoder, the vault account decoder, the precompile helpers
|
|
205
|
+
This package is the structural fix. The canonical 4-role Swig provisioner, every instruction builder, every byte-precise message encoder, the vault account decoder, and the precompile helpers each live in exactly one file. Consumers (`dexter-api`, `dexter-facilitator`, `dexter-vault` tests, `@dexterai/x402/tab`) import from here. The drift bug class is gone.
|
|
160
206
|
|
|
161
207
|
---
|
|
162
208
|
|
|
@@ -164,10 +210,10 @@ This package is the structural fix. The canonical 4-role Swig provisioner, every
|
|
|
164
210
|
|
|
165
211
|
`tests/byte-parity.test.ts`, `tests/precompile.test.ts`, `tests/swigBundle.test.ts`, `tests/counterfactual.test.ts`, and `tests/reader.test.ts` together snapshot:
|
|
166
212
|
|
|
167
|
-
- All **
|
|
168
|
-
- All **3 message layouts
|
|
169
|
-
- Both **precompile builders
|
|
170
|
-
- The **vault account decoder** for every
|
|
213
|
+
- All **21 instruction discriminators**, derived from `sha256("global:<name>")` and checked against the pinned bytes, covering the vault core, the session-key pair, the LockedClaim set, and the credit set.
|
|
214
|
+
- All **3 message layouts**, byte-by-byte: 188-byte V2 session registration, 128-byte revocation, 44-byte voucher payload.
|
|
215
|
+
- Both **precompile builders**, secp256r1 (SIMD-0075) and Ed25519, including the 14-byte offsets table.
|
|
216
|
+
- The **vault account decoder** for every V5 layout combination (with/without pending withdrawal, with/without active session).
|
|
171
217
|
- The **`buildSwigCreationBundle` structural lock**: ≥4 instructions, idempotent for the same `(identitySeed, hmacKey)`, distinct outputs for different inputs, the `settle_tab_voucher` Swig exec marker bytes match the on-chain discriminator.
|
|
172
218
|
- The **counterfactual derivation** for a known seed.
|
|
173
219
|
|
|
@@ -184,7 +230,7 @@ npm test
|
|
|
184
230
|
```
|
|
185
231
|
┌─────────────────────────────────┐
|
|
186
232
|
│ dexter-vault (Anchor program) │ ← source of truth
|
|
187
|
-
│
|
|
233
|
+
│ V5: 21 discriminators, 3 layouts│
|
|
188
234
|
└────────────────┬────────────────┘
|
|
189
235
|
│ defines bytes
|
|
190
236
|
▼
|
|
@@ -229,15 +275,15 @@ interface PasskeySigner {
|
|
|
229
275
|
}
|
|
230
276
|
```
|
|
231
277
|
|
|
232
|
-
`NodeEd25519Signer` ships at `@dexterai/vault/signers/node`.
|
|
278
|
+
`NodeEd25519Signer` ships at `@dexterai/vault/signers/node`. The browser passkey signer shipped in 0.2.0 as `WebAuthnAssertion` at `@dexterai/vault/signers/browser`: a pure-browser P-256 ceremony that runs `navigator.credentials.get()` and returns the three on-chain-ready buffers (64-byte compact lowS signature, raw `clientDataJSON`, raw `authenticatorData`), zero `fetch` calls. It implements the `PasskeySigner` interface; consumers compose it with their own server-policy adapter. This unlocks browser-buyer flows on dexter.cash and anywhere else a user pays through their passkey vault.
|
|
233
279
|
|
|
234
280
|
---
|
|
235
281
|
|
|
236
282
|
## Versioning
|
|
237
283
|
|
|
238
|
-
|
|
284
|
+
The current SDK targets the `dexter-vault` V5 program (21 Anchor instructions; role 3 `ProgramExec` for `settle_tab_voucher` registered on every new Swig).
|
|
239
285
|
|
|
240
|
-
Future program versions will bump the SDK major or document the delta in the CHANGELOG. The byte-parity tests are the structural lock
|
|
286
|
+
Future program versions will bump the SDK major or document the delta in the CHANGELOG. The byte-parity tests are the structural lock: any layout change requires an explicit snapshot update.
|
|
241
287
|
|
|
242
288
|
---
|
|
243
289
|
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/connect/index.ts
|
|
31
|
+
var connect_exports = {};
|
|
32
|
+
__export(connect_exports, {
|
|
33
|
+
connectTab: () => connectTab,
|
|
34
|
+
decodeChallengeTo32Bytes: () => decodeChallengeTo32Bytes,
|
|
35
|
+
verifyConnectProof: () => verifyConnectProof
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(connect_exports);
|
|
38
|
+
|
|
39
|
+
// src/connect/verify.ts
|
|
40
|
+
var import_web34 = require("@solana/web3.js");
|
|
41
|
+
var import_node_crypto = require("crypto");
|
|
42
|
+
|
|
43
|
+
// src/instructions/provePasskey.ts
|
|
44
|
+
var import_web32 = require("@solana/web3.js");
|
|
45
|
+
|
|
46
|
+
// src/constants/index.ts
|
|
47
|
+
var import_web3 = require("@solana/web3.js");
|
|
48
|
+
var DEXTER_VAULT_PROGRAM_ID = new import_web3.PublicKey(
|
|
49
|
+
"Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc"
|
|
50
|
+
);
|
|
51
|
+
var SWIG_PROGRAM_ID = new import_web3.PublicKey(
|
|
52
|
+
"swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB"
|
|
53
|
+
);
|
|
54
|
+
var SECP256R1_PROGRAM_ID = new import_web3.PublicKey(
|
|
55
|
+
"Secp256r1SigVerify1111111111111111111111111"
|
|
56
|
+
);
|
|
57
|
+
var ED25519_PROGRAM_ID = new import_web3.PublicKey(
|
|
58
|
+
"Ed25519SigVerify111111111111111111111111111"
|
|
59
|
+
);
|
|
60
|
+
var INSTRUCTIONS_SYSVAR_ID = new import_web3.PublicKey(
|
|
61
|
+
"Sysvar1nstructions1111111111111111111111111"
|
|
62
|
+
);
|
|
63
|
+
var VAULT_SEED_PREFIX = Buffer.from("vault");
|
|
64
|
+
var LOCKED_CLAIM_SEED = Buffer.from("locked-claim");
|
|
65
|
+
var DISCRIMINATORS = Object.freeze({
|
|
66
|
+
initialize_vault: Uint8Array.from([48, 191, 163, 44, 71, 129, 63, 164]),
|
|
67
|
+
set_swig: Uint8Array.from([253, 229, 89, 206, 192, 118, 137, 165]),
|
|
68
|
+
settle_voucher: Uint8Array.from([144, 176, 128, 220, 156, 79, 41, 54]),
|
|
69
|
+
request_withdrawal: Uint8Array.from([251, 85, 121, 205, 56, 201, 12, 177]),
|
|
70
|
+
finalize_withdrawal: Uint8Array.from([178, 87, 206, 68, 201, 186, 164, 232]),
|
|
71
|
+
force_release: Uint8Array.from([122, 190, 243, 252, 54, 202, 208, 234]),
|
|
72
|
+
rotate_passkey: Uint8Array.from([28, 134, 49, 89, 196, 34, 58, 174]),
|
|
73
|
+
rotate_dexter_authority: Uint8Array.from([145, 60, 4, 119, 180, 205, 236, 134]),
|
|
74
|
+
prove_passkey: Uint8Array.from([35, 175, 41, 143, 201, 118, 49, 184]),
|
|
75
|
+
settle_tab_voucher: Uint8Array.from([173, 22, 98, 31, 110, 129, 59, 161]),
|
|
76
|
+
register_session_key: Uint8Array.from([69, 94, 60, 44, 49, 199, 183, 233]),
|
|
77
|
+
revoke_session_key: Uint8Array.from([81, 192, 32, 110, 104, 116, 144, 151]),
|
|
78
|
+
lock_voucher: Uint8Array.from([91, 138, 5, 227, 119, 239, 48, 254]),
|
|
79
|
+
settle_locked_voucher: Uint8Array.from([44, 80, 216, 43, 247, 253, 101, 45]),
|
|
80
|
+
transfer_lock_ownership: Uint8Array.from([193, 13, 131, 134, 95, 25, 229, 157]),
|
|
81
|
+
recover_abandoned_lock: Uint8Array.from([169, 213, 107, 64, 229, 49, 43, 234]),
|
|
82
|
+
open_standby: Uint8Array.from([234, 184, 232, 135, 246, 191, 90, 250]),
|
|
83
|
+
draw_credit: Uint8Array.from([20, 84, 47, 211, 78, 117, 195, 210]),
|
|
84
|
+
repay_credit: Uint8Array.from([38, 113, 240, 182, 109, 179, 154, 245]),
|
|
85
|
+
seize_collateral: Uint8Array.from([40, 250, 7, 243, 168, 184, 116, 154]),
|
|
86
|
+
migrate_v4_to_v5: Uint8Array.from([226, 105, 140, 184, 101, 39, 235, 116])
|
|
87
|
+
});
|
|
88
|
+
var OTS_SESSION_REGISTER_V1_DOMAIN = (() => {
|
|
89
|
+
const buf = new Uint8Array(32);
|
|
90
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V1"), 0);
|
|
91
|
+
return buf;
|
|
92
|
+
})();
|
|
93
|
+
var OTS_SESSION_REGISTER_V2_DOMAIN = (() => {
|
|
94
|
+
const buf = new Uint8Array(32);
|
|
95
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V2"), 0);
|
|
96
|
+
return buf;
|
|
97
|
+
})();
|
|
98
|
+
var OTS_SESSION_REVOKE_V1_DOMAIN = (() => {
|
|
99
|
+
const buf = new Uint8Array(32);
|
|
100
|
+
buf.set(new TextEncoder().encode("OTS_SESSION_REVOKE_V1"), 0);
|
|
101
|
+
return buf;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
// src/instructions/provePasskey.ts
|
|
105
|
+
function encodeBytesVec(buf) {
|
|
106
|
+
const out = Buffer.alloc(4 + buf.length);
|
|
107
|
+
out.writeUInt32LE(buf.length, 0);
|
|
108
|
+
Buffer.from(buf).copy(out, 4);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function encodeFixedBytes(buf, len) {
|
|
112
|
+
if (buf.length !== len) {
|
|
113
|
+
throw new Error(`expected ${len} bytes, got ${buf.length}`);
|
|
114
|
+
}
|
|
115
|
+
return Buffer.from(buf);
|
|
116
|
+
}
|
|
117
|
+
function buildProvePasskeyInstruction(p) {
|
|
118
|
+
const argsBuf = Buffer.concat([
|
|
119
|
+
encodeFixedBytes(p.challenge, 32),
|
|
120
|
+
encodeBytesVec(p.clientDataJSON),
|
|
121
|
+
encodeBytesVec(p.authenticatorData)
|
|
122
|
+
]);
|
|
123
|
+
const data = Buffer.concat([Buffer.from(DISCRIMINATORS.prove_passkey), argsBuf]);
|
|
124
|
+
return new import_web32.TransactionInstruction({
|
|
125
|
+
programId: DEXTER_VAULT_PROGRAM_ID,
|
|
126
|
+
keys: [
|
|
127
|
+
{ pubkey: p.vaultPda, isSigner: false, isWritable: false },
|
|
128
|
+
{ pubkey: import_web32.SYSVAR_INSTRUCTIONS_PUBKEY, isSigner: false, isWritable: false }
|
|
129
|
+
],
|
|
130
|
+
data
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/precompile/secp256r1.ts
|
|
135
|
+
var import_web33 = require("@solana/web3.js");
|
|
136
|
+
var SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14;
|
|
137
|
+
var SIGNATURE_SERIALIZED_SIZE = 64;
|
|
138
|
+
var COMPRESSED_PUBKEY_SERIALIZED_SIZE = 33;
|
|
139
|
+
var PRECOMPILE_DATA_START = 2;
|
|
140
|
+
function buildSecp256r1VerifyInstruction(publicKey, signature, message) {
|
|
141
|
+
if (publicKey.length !== COMPRESSED_PUBKEY_SERIALIZED_SIZE) {
|
|
142
|
+
throw new Error(`expected ${COMPRESSED_PUBKEY_SERIALIZED_SIZE}-byte pubkey`);
|
|
143
|
+
}
|
|
144
|
+
if (signature.length !== SIGNATURE_SERIALIZED_SIZE) {
|
|
145
|
+
throw new Error(`expected ${SIGNATURE_SERIALIZED_SIZE}-byte signature`);
|
|
146
|
+
}
|
|
147
|
+
const signatureOffset = PRECOMPILE_DATA_START + SIGNATURE_OFFSETS_SERIALIZED_SIZE;
|
|
148
|
+
const publicKeyOffset = signatureOffset + SIGNATURE_SERIALIZED_SIZE;
|
|
149
|
+
const messageOffset = publicKeyOffset + COMPRESSED_PUBKEY_SERIALIZED_SIZE;
|
|
150
|
+
const messageSize = message.length;
|
|
151
|
+
const totalLen = messageOffset + messageSize;
|
|
152
|
+
const data = Buffer.alloc(totalLen);
|
|
153
|
+
data[0] = 1;
|
|
154
|
+
data[1] = 0;
|
|
155
|
+
data.writeUInt16LE(signatureOffset, PRECOMPILE_DATA_START + 0);
|
|
156
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 2);
|
|
157
|
+
data.writeUInt16LE(publicKeyOffset, PRECOMPILE_DATA_START + 4);
|
|
158
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 6);
|
|
159
|
+
data.writeUInt16LE(messageOffset, PRECOMPILE_DATA_START + 8);
|
|
160
|
+
data.writeUInt16LE(messageSize, PRECOMPILE_DATA_START + 10);
|
|
161
|
+
data.writeUInt16LE(65535, PRECOMPILE_DATA_START + 12);
|
|
162
|
+
Buffer.from(signature).copy(data, signatureOffset);
|
|
163
|
+
Buffer.from(publicKey).copy(data, publicKeyOffset);
|
|
164
|
+
Buffer.from(message).copy(data, messageOffset);
|
|
165
|
+
return new import_web33.TransactionInstruction({
|
|
166
|
+
keys: [],
|
|
167
|
+
programId: SECP256R1_PROGRAM_ID,
|
|
168
|
+
data
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function buildPrecompileMessage(clientDataJSON, authenticatorData) {
|
|
172
|
+
const subtle = globalThis.crypto?.subtle;
|
|
173
|
+
let clientDataHash;
|
|
174
|
+
if (subtle) {
|
|
175
|
+
const buf = await subtle.digest("SHA-256", clientDataJSON);
|
|
176
|
+
clientDataHash = new Uint8Array(buf);
|
|
177
|
+
} else {
|
|
178
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
179
|
+
clientDataHash = createHash2("sha256").update(clientDataJSON).digest();
|
|
180
|
+
}
|
|
181
|
+
const out = new Uint8Array(authenticatorData.length + 32);
|
|
182
|
+
out.set(authenticatorData, 0);
|
|
183
|
+
out.set(clientDataHash, authenticatorData.length);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/connect/verify.ts
|
|
188
|
+
function decodeChallengeTo32Bytes(challenge) {
|
|
189
|
+
const decoded = tryBase64urlDecode(challenge);
|
|
190
|
+
if (decoded && decoded.length === 32) return decoded;
|
|
191
|
+
return new Uint8Array((0, import_node_crypto.createHash)("sha256").update(challenge, "utf8").digest());
|
|
192
|
+
}
|
|
193
|
+
function tryBase64urlDecode(s) {
|
|
194
|
+
if (!/^[A-Za-z0-9\-_]+={0,2}$/.test(s)) return null;
|
|
195
|
+
try {
|
|
196
|
+
return new Uint8Array(Buffer.from(s, "base64url"));
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function verifyConnectProof(args) {
|
|
202
|
+
const { connection, challenge, proof } = args;
|
|
203
|
+
try {
|
|
204
|
+
const challengeBytes = decodeChallengeTo32Bytes(challenge);
|
|
205
|
+
const vaultPda = new import_web34.PublicKey(proof.vault);
|
|
206
|
+
const precompileMessage = await buildPrecompileMessage(
|
|
207
|
+
proof.clientDataJSON,
|
|
208
|
+
proof.authenticatorData
|
|
209
|
+
);
|
|
210
|
+
const ix0 = buildSecp256r1VerifyInstruction(
|
|
211
|
+
proof.passkeyPubkey,
|
|
212
|
+
proof.signature,
|
|
213
|
+
precompileMessage
|
|
214
|
+
);
|
|
215
|
+
const ix1 = buildProvePasskeyInstruction({
|
|
216
|
+
vaultPda,
|
|
217
|
+
challenge: challengeBytes,
|
|
218
|
+
clientDataJSON: proof.clientDataJSON,
|
|
219
|
+
authenticatorData: proof.authenticatorData
|
|
220
|
+
});
|
|
221
|
+
const tx = new import_web34.Transaction();
|
|
222
|
+
tx.add(ix0, ix1);
|
|
223
|
+
tx.feePayer = vaultPda;
|
|
224
|
+
tx.recentBlockhash = import_web34.PublicKey.default.toBase58();
|
|
225
|
+
const simulate = args.simulate ?? ((t) => connection.simulateTransaction(t, void 0, false));
|
|
226
|
+
const res = await simulate(tx);
|
|
227
|
+
const err = res?.value?.err ?? null;
|
|
228
|
+
if (err === null) {
|
|
229
|
+
return { ok: true, vault: vaultPda };
|
|
230
|
+
}
|
|
231
|
+
return { ok: false, reason: `simulation rejected: ${stringifyErr(err)}` };
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function stringifyErr(err) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.stringify(err);
|
|
239
|
+
} catch {
|
|
240
|
+
return String(err);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/signers/browser/index.ts
|
|
245
|
+
var WebAuthnAssertionError = class extends Error {
|
|
246
|
+
code;
|
|
247
|
+
constructor(code, message) {
|
|
248
|
+
super(message);
|
|
249
|
+
this.code = code;
|
|
250
|
+
this.name = "WebAuthnAssertionError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var WebAuthnAssertion = class {
|
|
254
|
+
credentialId;
|
|
255
|
+
publicKeyBase64;
|
|
256
|
+
rpId;
|
|
257
|
+
allowCredentials;
|
|
258
|
+
timeoutMs;
|
|
259
|
+
userVerification;
|
|
260
|
+
constructor(config) {
|
|
261
|
+
if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
|
|
262
|
+
throw new WebAuthnAssertionError(
|
|
263
|
+
"invalid_credential_id",
|
|
264
|
+
"credentialId must be a non-empty Uint8Array"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
this.credentialId = config.credentialId;
|
|
268
|
+
this.publicKeyBase64 = config.publicKeyBase64;
|
|
269
|
+
this.rpId = config.rpId;
|
|
270
|
+
this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
|
|
271
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
272
|
+
this.userVerification = config.userVerification ?? "preferred";
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Run `navigator.credentials.get()` over `challenge` and return the
|
|
276
|
+
* three on-chain-ready buffers.
|
|
277
|
+
*
|
|
278
|
+
* The caller is responsible for what `challenge` *is*. For the vault
|
|
279
|
+
* program, this is typically `sha256(opMessage)` minted server-side
|
|
280
|
+
* with replay defense (see DexterApiBrowserPasskeySigner). The SDK
|
|
281
|
+
* does not impose policy here.
|
|
282
|
+
*/
|
|
283
|
+
async assertOver(challenge) {
|
|
284
|
+
ensureBrowser();
|
|
285
|
+
if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
|
|
286
|
+
throw new WebAuthnAssertionError(
|
|
287
|
+
"invalid_challenge",
|
|
288
|
+
"challenge must be a non-empty Uint8Array"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const requestOptions = {
|
|
292
|
+
challenge: toBufferSource(challenge),
|
|
293
|
+
allowCredentials: this.allowCredentials.map((c) => ({
|
|
294
|
+
id: toBufferSource(c.id),
|
|
295
|
+
type: "public-key",
|
|
296
|
+
transports: c.transports
|
|
297
|
+
})),
|
|
298
|
+
timeout: this.timeoutMs,
|
|
299
|
+
userVerification: this.userVerification,
|
|
300
|
+
...this.rpId ? { rpId: this.rpId } : {}
|
|
301
|
+
};
|
|
302
|
+
const credential = await navigator.credentials.get({
|
|
303
|
+
publicKey: requestOptions
|
|
304
|
+
});
|
|
305
|
+
if (!credential) {
|
|
306
|
+
throw new WebAuthnAssertionError(
|
|
307
|
+
"user_cancelled",
|
|
308
|
+
"no assertion returned from authenticator"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (credential.type !== "public-key") {
|
|
312
|
+
throw new WebAuthnAssertionError(
|
|
313
|
+
"credential_invalid",
|
|
314
|
+
`unexpected credential type: ${credential.type}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const assertion = credential.response;
|
|
318
|
+
const derSignature = new Uint8Array(assertion.signature);
|
|
319
|
+
const compactSignature = derSignatureToCompactLowS(derSignature);
|
|
320
|
+
return {
|
|
321
|
+
signature: compactSignature,
|
|
322
|
+
signatureDer: derSignature,
|
|
323
|
+
clientDataJSON: new Uint8Array(assertion.clientDataJSON),
|
|
324
|
+
authenticatorData: new Uint8Array(assertion.authenticatorData)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `PasskeySigner` shape — alias for `assertOver`. Consumers that want
|
|
329
|
+
* to type against `PasskeySigner` (e.g. dexter-fe's
|
|
330
|
+
* `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
|
|
331
|
+
* that want the explicit name call `.assertOver(challenge)`. Same
|
|
332
|
+
* function, two names.
|
|
333
|
+
*/
|
|
334
|
+
sign(challenge) {
|
|
335
|
+
return this.assertOver(challenge);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function ensureBrowser() {
|
|
339
|
+
if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
|
|
340
|
+
throw new WebAuthnAssertionError(
|
|
341
|
+
"not_browser",
|
|
342
|
+
"WebAuthnAssertion requires a browser environment (navigator.credentials)"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const cred = globalThis.navigator.credentials;
|
|
346
|
+
if (!cred || typeof cred.get !== "function") {
|
|
347
|
+
throw new WebAuthnAssertionError(
|
|
348
|
+
"webauthn_unsupported",
|
|
349
|
+
"this environment does not implement navigator.credentials.get"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function toBufferSource(bytes) {
|
|
354
|
+
const out = new ArrayBuffer(bytes.length);
|
|
355
|
+
new Uint8Array(out).set(bytes);
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
var P256_ORDER = BigInt(
|
|
359
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
360
|
+
);
|
|
361
|
+
var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
|
|
362
|
+
function bigintFromBytes(buf) {
|
|
363
|
+
let n = 0n;
|
|
364
|
+
for (const byte of buf) n = n << 8n | BigInt(byte);
|
|
365
|
+
return n;
|
|
366
|
+
}
|
|
367
|
+
function bytesFromBigint(n, length) {
|
|
368
|
+
const out = new Uint8Array(length);
|
|
369
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
370
|
+
out[i] = Number(n & 0xffn);
|
|
371
|
+
n >>= 8n;
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
function derSignatureToCompactLowS(der) {
|
|
376
|
+
let i = 0;
|
|
377
|
+
if (der[i++] !== 48) {
|
|
378
|
+
throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
|
|
379
|
+
}
|
|
380
|
+
i++;
|
|
381
|
+
if (der[i++] !== 2) {
|
|
382
|
+
throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
|
|
383
|
+
}
|
|
384
|
+
const rLen = der[i++];
|
|
385
|
+
if (rLen === void 0) {
|
|
386
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
|
|
387
|
+
}
|
|
388
|
+
let r = der.slice(i, i + rLen);
|
|
389
|
+
i += rLen;
|
|
390
|
+
if (der[i++] !== 2) {
|
|
391
|
+
throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
|
|
392
|
+
}
|
|
393
|
+
const sLen = der[i++];
|
|
394
|
+
if (sLen === void 0) {
|
|
395
|
+
throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
|
|
396
|
+
}
|
|
397
|
+
let s = der.slice(i, i + sLen);
|
|
398
|
+
i += sLen;
|
|
399
|
+
if (r.length > 32 && r[0] === 0) r = r.slice(1);
|
|
400
|
+
if (s.length > 32 && s[0] === 0) s = s.slice(1);
|
|
401
|
+
if (r.length > 32 || s.length > 32) {
|
|
402
|
+
throw new WebAuthnAssertionError(
|
|
403
|
+
"bad_signature",
|
|
404
|
+
"DER component too large for P-256"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const rPadded = new Uint8Array(32);
|
|
408
|
+
rPadded.set(r, 32 - r.length);
|
|
409
|
+
let sN = bigintFromBytes(s);
|
|
410
|
+
if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
|
|
411
|
+
const sPadded = bytesFromBigint(sN, 32);
|
|
412
|
+
const out = new Uint8Array(64);
|
|
413
|
+
out.set(rPadded, 0);
|
|
414
|
+
out.set(sPadded, 32);
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/connect/ceremony.ts
|
|
419
|
+
var SIWX_LOGIN_PREFIX = "siwx_login";
|
|
420
|
+
async function connectTab(args) {
|
|
421
|
+
const challengeBytes = decodeChallengeTo32Bytes(args.challenge);
|
|
422
|
+
const prefix = new TextEncoder().encode(SIWX_LOGIN_PREFIX);
|
|
423
|
+
const opMessage = new Uint8Array(prefix.length + challengeBytes.length);
|
|
424
|
+
opMessage.set(prefix, 0);
|
|
425
|
+
opMessage.set(challengeBytes, prefix.length);
|
|
426
|
+
const signedDigest = await sha256(opMessage);
|
|
427
|
+
const signer = new WebAuthnAssertion({
|
|
428
|
+
credentialId: args.credentialId,
|
|
429
|
+
...args.rpId ? { rpId: args.rpId } : {}
|
|
430
|
+
});
|
|
431
|
+
const assertion = await signer.assertOver(signedDigest);
|
|
432
|
+
return {
|
|
433
|
+
passkeyPubkey: args.passkeyPubkey,
|
|
434
|
+
vault: args.vault,
|
|
435
|
+
clientDataJSON: assertion.clientDataJSON,
|
|
436
|
+
authenticatorData: assertion.authenticatorData,
|
|
437
|
+
signature: assertion.signature
|
|
438
|
+
// 64-byte compact lowS r||s
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function sha256(data) {
|
|
442
|
+
const subtle = globalThis.crypto?.subtle;
|
|
443
|
+
if (subtle) {
|
|
444
|
+
const buf = await subtle.digest("SHA-256", data);
|
|
445
|
+
return new Uint8Array(buf);
|
|
446
|
+
}
|
|
447
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
448
|
+
return new Uint8Array(createHash2("sha256").update(data).digest());
|
|
449
|
+
}
|
|
450
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
451
|
+
0 && (module.exports = {
|
|
452
|
+
connectTab,
|
|
453
|
+
decodeChallengeTo32Bytes,
|
|
454
|
+
verifyConnectProof
|
|
455
|
+
});
|