@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.
@@ -0,0 +1,145 @@
1
+ import { PublicKey, Transaction, Connection } from '@solana/web3.js';
2
+
3
+ /**
4
+ * verifyConnectProof — the relying-app VERIFIER for "Connect a Tab" step 1.
5
+ *
6
+ * A third-party app uses this to confirm a user controls a named Dexter vault.
7
+ * It reconstructs the read-only two-instruction proof transaction
8
+ * [secp256r1_verify, prove_passkey]
9
+ * from the proof + the challenge it issued + the vault's passkey pubkey, and
10
+ * simulates it against the caller-supplied Connection (Helius mainnet) via the
11
+ * legacy `Transaction` simulate overload — which does NOT verify signatures, so
12
+ * the placeholder blockhash and formal feePayer are never checked.
13
+ * `err === null` → the holder controls the vault.
14
+ * (`connection.simulateTransaction(tx, undefined, false)`: signature is
15
+ * `(transaction, signers?, includeAccounts?)`; the third arg is
16
+ * `includeAccounts`, set false. There is no `sigVerify` param on this
17
+ * overload — `sigVerify` exists only on the VersionedTransaction config.)
18
+ *
19
+ * This is THE canonical method documented in provePasskey.ts: a verifier treats
20
+ * a passing simulate of [secp256r1_verify, prove_passkey] (err === null) as
21
+ * proof of control.
22
+ * It reuses the exact on-chain P-256 semantics rather than re-implementing
23
+ * verification — a forged/wrong-key/wrong-challenge proof makes the on-chain
24
+ * precompile (or prove_passkey's op-message check) reject, and simulate
25
+ * returns a non-null err. The reject path is genuinely simulate-driven; it is
26
+ * NOT a bypassable string compare.
27
+ *
28
+ * The `simulate` step is injectable with a real default (the same
29
+ * injectable-default-real pattern ./tab and ./factoring use for assembleSignV2
30
+ * / readPriorSpent) so the assembly + decision logic is unit-testable offline,
31
+ * while production hits the real chain.
32
+ */
33
+
34
+ interface ConnectProof {
35
+ /** 33-byte compressed P-256 passkey pubkey bound to the vault. */
36
+ passkeyPubkey: Uint8Array;
37
+ /** base58 vault PDA the proof claims control of. */
38
+ vault: string;
39
+ /** WebAuthn ceremony outputs (from WebAuthnAssertionResult). */
40
+ clientDataJSON: Uint8Array;
41
+ authenticatorData: Uint8Array;
42
+ /** 64-byte compact lowS r||s P-256 signature. */
43
+ signature: Uint8Array;
44
+ }
45
+ interface ConnectVerifyResult {
46
+ ok: boolean;
47
+ vault?: PublicKey;
48
+ reason?: string;
49
+ }
50
+ /** Injectable simulate fn — defaults to the real connection.simulateTransaction.
51
+ * Matches the on-chain verifier method documented in provePasskey.ts.
52
+ *
53
+ * The return shape is intentionally MINIMAL: only `value.err` is consumed by
54
+ * the decision path. The real `simulateTransaction` response is richer —
55
+ * `{ context, value: { err, logs, accounts, unitsConsumed, ... } }`. A future
56
+ * maintainer wanting richer `reason` diagnostics can read `value.logs` (it is
57
+ * present on the real response and on the optional `logs?` below). Keeping the
58
+ * type narrow also keeps the tests' fake simulate trivial. */
59
+ type SimulateFn = (tx: Transaction) => Promise<{
60
+ value: {
61
+ err: unknown;
62
+ logs?: string[] | null;
63
+ };
64
+ }>;
65
+ /**
66
+ * CHALLENGE-ENCODING CONTRACT (C2 — the ceremony — MUST match this):
67
+ *
68
+ * The on-chain prove_passkey takes a 32-byte `challenge` (the SIWX nonce/digest);
69
+ * its op-message is "siwx_login" || challenge, and the WebAuthn
70
+ * clientDataJSON.challenge field must base64url-decode to
71
+ * sha256("siwx_login" || challenge).
72
+ *
73
+ * The op-message is exactly utf8("siwx_login") concatenated DIRECTLY with the
74
+ * 32 challenge bytes — no separator, no length prefix, no padding between them
75
+ * (it is rebuilt on-chain by plain byte concatenation; see provePasskey.ts).
76
+ *
77
+ * The relying-app `challenge` STRING that this verifier receives maps to those
78
+ * 32 bytes by this rule:
79
+ * - if it base64url-decodes to EXACTLY 32 bytes, those bytes ARE the challenge;
80
+ * - otherwise, sha256(utf8(challenge)) → 32 bytes.
81
+ * The base64url form is accepted with OR without `=` padding; the canonical
82
+ * issuer form is unpadded `base64url(random 32 bytes)`.
83
+ *
84
+ * So a relying app SHOULD issue `base64url(random 32 bytes)` (the canonical,
85
+ * zero-ambiguity form). The fallback (sha256 of an arbitrary string) keeps any
86
+ * other issuer deterministic. C2 produces the matching ceremony challenge:
87
+ * clientDataJSON.challenge = base64url(sha256("siwx_login" || challengeBytes)).
88
+ */
89
+ declare function decodeChallengeTo32Bytes(challenge: string): Uint8Array;
90
+ declare function verifyConnectProof(args: {
91
+ connection: Connection;
92
+ /** The challenge the relying app issued (raw string; the SAME one C2 signed). */
93
+ challenge: string;
94
+ proof: ConnectProof;
95
+ /** Default: real connection.simulateTransaction (injectable for tests). */
96
+ simulate?: SimulateFn;
97
+ }): Promise<ConnectVerifyResult>;
98
+
99
+ /**
100
+ * connectTab — the browser-side ceremony for "Connect a Tab" step 1 (auth).
101
+ *
102
+ * Runs the WebAuthn passkey assertion and returns a `ConnectProof` that the C1
103
+ * verifier (`verifyConnectProof`) accepts. connectTab + verifyConnectProof must
104
+ * agree byte-for-byte on the challenge contract — the round-trip test is the
105
+ * proof they do.
106
+ *
107
+ * THE CHALLENGE CONTRACT (must match verify.ts / provePasskey.ts / the IDL):
108
+ * - The relying-app `challenge` STRING maps to 32 bytes via the SAME
109
+ * `decodeChallengeTo32Bytes` the verifier uses.
110
+ * - The on-chain prove_passkey op-message is utf8("siwx_login") concatenated
111
+ * DIRECTLY with those 32 challenge bytes (no separator, no length prefix).
112
+ * - The WebAuthn `clientDataJSON.challenge` field must equal
113
+ * base64url(sha256("siwx_login" || challengeBytes)).
114
+ *
115
+ * Since `WebAuthnAssertion.assertOver(X)` causes the browser to write
116
+ * base64url(X) into clientDataJSON.challenge, the bytes we pass to assertOver
117
+ * are sha256("siwx_login" || challengeBytes) — the 32-byte digest, NOT the raw
118
+ * challenge. Then clientDataJSON.challenge == base64url(sha256(opMessage)),
119
+ * exactly what prove_passkey reconstructs and the precompile signature is over.
120
+ *
121
+ * Framework-agnostic: a plain browser function. The C3 button is documented as
122
+ * a snippet, not a shipped React component (React is not a dependency).
123
+ */
124
+
125
+ interface ConnectTabArgs {
126
+ /** The challenge the relying app issued (same string the server will pass to verifyConnectProof). */
127
+ challenge: string;
128
+ /** base58 vault PDA being connected. */
129
+ vault: string;
130
+ /** 33-byte compressed P-256 passkey pubkey bound to the vault. */
131
+ passkeyPubkey: Uint8Array;
132
+ /** Raw WebAuthn credential ID bytes for the vault's passkey. */
133
+ credentialId: Uint8Array;
134
+ /** Optional WebAuthn RP id (defaults to the page's RP). */
135
+ rpId?: string;
136
+ }
137
+ /**
138
+ * Run the passkey assertion and return a verifier-ready ConnectProof.
139
+ *
140
+ * Browser-only (requires navigator.credentials). The returned proof feeds
141
+ * straight into verifyConnectProof with the SAME challenge string.
142
+ */
143
+ declare function connectTab(args: ConnectTabArgs): Promise<ConnectProof>;
144
+
145
+ export { type ConnectProof, type ConnectTabArgs, type ConnectVerifyResult, type SimulateFn, connectTab, decodeChallengeTo32Bytes, verifyConnectProof };
@@ -0,0 +1,145 @@
1
+ import { PublicKey, Transaction, Connection } from '@solana/web3.js';
2
+
3
+ /**
4
+ * verifyConnectProof — the relying-app VERIFIER for "Connect a Tab" step 1.
5
+ *
6
+ * A third-party app uses this to confirm a user controls a named Dexter vault.
7
+ * It reconstructs the read-only two-instruction proof transaction
8
+ * [secp256r1_verify, prove_passkey]
9
+ * from the proof + the challenge it issued + the vault's passkey pubkey, and
10
+ * simulates it against the caller-supplied Connection (Helius mainnet) via the
11
+ * legacy `Transaction` simulate overload — which does NOT verify signatures, so
12
+ * the placeholder blockhash and formal feePayer are never checked.
13
+ * `err === null` → the holder controls the vault.
14
+ * (`connection.simulateTransaction(tx, undefined, false)`: signature is
15
+ * `(transaction, signers?, includeAccounts?)`; the third arg is
16
+ * `includeAccounts`, set false. There is no `sigVerify` param on this
17
+ * overload — `sigVerify` exists only on the VersionedTransaction config.)
18
+ *
19
+ * This is THE canonical method documented in provePasskey.ts: a verifier treats
20
+ * a passing simulate of [secp256r1_verify, prove_passkey] (err === null) as
21
+ * proof of control.
22
+ * It reuses the exact on-chain P-256 semantics rather than re-implementing
23
+ * verification — a forged/wrong-key/wrong-challenge proof makes the on-chain
24
+ * precompile (or prove_passkey's op-message check) reject, and simulate
25
+ * returns a non-null err. The reject path is genuinely simulate-driven; it is
26
+ * NOT a bypassable string compare.
27
+ *
28
+ * The `simulate` step is injectable with a real default (the same
29
+ * injectable-default-real pattern ./tab and ./factoring use for assembleSignV2
30
+ * / readPriorSpent) so the assembly + decision logic is unit-testable offline,
31
+ * while production hits the real chain.
32
+ */
33
+
34
+ interface ConnectProof {
35
+ /** 33-byte compressed P-256 passkey pubkey bound to the vault. */
36
+ passkeyPubkey: Uint8Array;
37
+ /** base58 vault PDA the proof claims control of. */
38
+ vault: string;
39
+ /** WebAuthn ceremony outputs (from WebAuthnAssertionResult). */
40
+ clientDataJSON: Uint8Array;
41
+ authenticatorData: Uint8Array;
42
+ /** 64-byte compact lowS r||s P-256 signature. */
43
+ signature: Uint8Array;
44
+ }
45
+ interface ConnectVerifyResult {
46
+ ok: boolean;
47
+ vault?: PublicKey;
48
+ reason?: string;
49
+ }
50
+ /** Injectable simulate fn — defaults to the real connection.simulateTransaction.
51
+ * Matches the on-chain verifier method documented in provePasskey.ts.
52
+ *
53
+ * The return shape is intentionally MINIMAL: only `value.err` is consumed by
54
+ * the decision path. The real `simulateTransaction` response is richer —
55
+ * `{ context, value: { err, logs, accounts, unitsConsumed, ... } }`. A future
56
+ * maintainer wanting richer `reason` diagnostics can read `value.logs` (it is
57
+ * present on the real response and on the optional `logs?` below). Keeping the
58
+ * type narrow also keeps the tests' fake simulate trivial. */
59
+ type SimulateFn = (tx: Transaction) => Promise<{
60
+ value: {
61
+ err: unknown;
62
+ logs?: string[] | null;
63
+ };
64
+ }>;
65
+ /**
66
+ * CHALLENGE-ENCODING CONTRACT (C2 — the ceremony — MUST match this):
67
+ *
68
+ * The on-chain prove_passkey takes a 32-byte `challenge` (the SIWX nonce/digest);
69
+ * its op-message is "siwx_login" || challenge, and the WebAuthn
70
+ * clientDataJSON.challenge field must base64url-decode to
71
+ * sha256("siwx_login" || challenge).
72
+ *
73
+ * The op-message is exactly utf8("siwx_login") concatenated DIRECTLY with the
74
+ * 32 challenge bytes — no separator, no length prefix, no padding between them
75
+ * (it is rebuilt on-chain by plain byte concatenation; see provePasskey.ts).
76
+ *
77
+ * The relying-app `challenge` STRING that this verifier receives maps to those
78
+ * 32 bytes by this rule:
79
+ * - if it base64url-decodes to EXACTLY 32 bytes, those bytes ARE the challenge;
80
+ * - otherwise, sha256(utf8(challenge)) → 32 bytes.
81
+ * The base64url form is accepted with OR without `=` padding; the canonical
82
+ * issuer form is unpadded `base64url(random 32 bytes)`.
83
+ *
84
+ * So a relying app SHOULD issue `base64url(random 32 bytes)` (the canonical,
85
+ * zero-ambiguity form). The fallback (sha256 of an arbitrary string) keeps any
86
+ * other issuer deterministic. C2 produces the matching ceremony challenge:
87
+ * clientDataJSON.challenge = base64url(sha256("siwx_login" || challengeBytes)).
88
+ */
89
+ declare function decodeChallengeTo32Bytes(challenge: string): Uint8Array;
90
+ declare function verifyConnectProof(args: {
91
+ connection: Connection;
92
+ /** The challenge the relying app issued (raw string; the SAME one C2 signed). */
93
+ challenge: string;
94
+ proof: ConnectProof;
95
+ /** Default: real connection.simulateTransaction (injectable for tests). */
96
+ simulate?: SimulateFn;
97
+ }): Promise<ConnectVerifyResult>;
98
+
99
+ /**
100
+ * connectTab — the browser-side ceremony for "Connect a Tab" step 1 (auth).
101
+ *
102
+ * Runs the WebAuthn passkey assertion and returns a `ConnectProof` that the C1
103
+ * verifier (`verifyConnectProof`) accepts. connectTab + verifyConnectProof must
104
+ * agree byte-for-byte on the challenge contract — the round-trip test is the
105
+ * proof they do.
106
+ *
107
+ * THE CHALLENGE CONTRACT (must match verify.ts / provePasskey.ts / the IDL):
108
+ * - The relying-app `challenge` STRING maps to 32 bytes via the SAME
109
+ * `decodeChallengeTo32Bytes` the verifier uses.
110
+ * - The on-chain prove_passkey op-message is utf8("siwx_login") concatenated
111
+ * DIRECTLY with those 32 challenge bytes (no separator, no length prefix).
112
+ * - The WebAuthn `clientDataJSON.challenge` field must equal
113
+ * base64url(sha256("siwx_login" || challengeBytes)).
114
+ *
115
+ * Since `WebAuthnAssertion.assertOver(X)` causes the browser to write
116
+ * base64url(X) into clientDataJSON.challenge, the bytes we pass to assertOver
117
+ * are sha256("siwx_login" || challengeBytes) — the 32-byte digest, NOT the raw
118
+ * challenge. Then clientDataJSON.challenge == base64url(sha256(opMessage)),
119
+ * exactly what prove_passkey reconstructs and the precompile signature is over.
120
+ *
121
+ * Framework-agnostic: a plain browser function. The C3 button is documented as
122
+ * a snippet, not a shipped React component (React is not a dependency).
123
+ */
124
+
125
+ interface ConnectTabArgs {
126
+ /** The challenge the relying app issued (same string the server will pass to verifyConnectProof). */
127
+ challenge: string;
128
+ /** base58 vault PDA being connected. */
129
+ vault: string;
130
+ /** 33-byte compressed P-256 passkey pubkey bound to the vault. */
131
+ passkeyPubkey: Uint8Array;
132
+ /** Raw WebAuthn credential ID bytes for the vault's passkey. */
133
+ credentialId: Uint8Array;
134
+ /** Optional WebAuthn RP id (defaults to the page's RP). */
135
+ rpId?: string;
136
+ }
137
+ /**
138
+ * Run the passkey assertion and return a verifier-ready ConnectProof.
139
+ *
140
+ * Browser-only (requires navigator.credentials). The returned proof feeds
141
+ * straight into verifyConnectProof with the SAME challenge string.
142
+ */
143
+ declare function connectTab(args: ConnectTabArgs): Promise<ConnectProof>;
144
+
145
+ export { type ConnectProof, type ConnectTabArgs, type ConnectVerifyResult, type SimulateFn, connectTab, decodeChallengeTo32Bytes, verifyConnectProof };