@dexterai/vault 0.1.2 → 0.2.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.
@@ -39,7 +39,7 @@ var import_lib2 = require("@swig-wallet/lib");
39
39
  var import_node_crypto = require("crypto");
40
40
  var import_kit = require("@swig-wallet/kit");
41
41
  var import_lib = require("@swig-wallet/lib");
42
- var import_bs58 = __toESM(require("bs58"), 1);
42
+ var bs58Module = __toESM(require("bs58"), 1);
43
43
  var import_web32 = require("@solana/web3.js");
44
44
 
45
45
  // src/constants/index.ts
@@ -15,7 +15,7 @@ import {
15
15
  createEd25519SessionAuthorityInfo,
16
16
  getCreateSwigWithMultipleAuthoritiesInstructionContextBuilder
17
17
  } from "@swig-wallet/lib";
18
- import bs58 from "bs58";
18
+ import * as bs58Module from "bs58";
19
19
  import { PublicKey as PublicKey2 } from "@solana/web3.js";
20
20
 
21
21
  // src/constants/index.ts
package/dist/index.cjs CHANGED
@@ -41,7 +41,7 @@ var import_lib2 = require("@swig-wallet/lib");
41
41
  var import_node_crypto = require("crypto");
42
42
  var import_kit = require("@swig-wallet/kit");
43
43
  var import_lib = require("@swig-wallet/lib");
44
- var import_bs58 = __toESM(require("bs58"), 1);
44
+ var bs58Module = __toESM(require("bs58"), 1);
45
45
  var import_web32 = require("@solana/web3.js");
46
46
 
47
47
  // src/constants/index.ts
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  createEd25519SessionAuthorityInfo,
16
16
  getCreateSwigWithMultipleAuthoritiesInstructionContextBuilder
17
17
  } from "@swig-wallet/lib";
18
- import bs58 from "bs58";
18
+ import * as bs58Module from "bs58";
19
19
  import { PublicKey as PublicKey2 } from "@solana/web3.js";
20
20
 
21
21
  // src/constants/index.ts
@@ -4869,8 +4869,9 @@ function createSolanaRpcFromTransport(transport) {
4869
4869
  }
4870
4870
 
4871
4871
  // src/instructions/swigBundle.ts
4872
- var import_bs58 = __toESM(require("bs58"), 1);
4872
+ var bs58Module = __toESM(require("bs58"), 1);
4873
4873
  var import_web311 = require("@solana/web3.js");
4874
+ var bs58 = bs58Module.default ?? bs58Module;
4874
4875
  var SWIG_ID_DOMAIN = "dexter-swig-id:v1:";
4875
4876
  var DEFAULT_SESSION_TTL_SECONDS = BigInt(30 * 24 * 60 * 60);
4876
4877
  var DEFAULT_SPEND_LIMIT_ATOMIC = BigInt(1e9);
@@ -4916,9 +4917,9 @@ async function buildSwigCreationBundle(params) {
4916
4917
  const swigId = deriveSwigId(identitySeed, hmacKey);
4917
4918
  const swigPda = await (0, import_kit.findSwigPda)(swigId);
4918
4919
  const swigAddressStr = String(swigPda);
4919
- const feePayerBytes = import_bs58.default.decode(feePayer);
4920
+ const feePayerBytes = bs58.decode(feePayer);
4920
4921
  const vaultProgramIdBytes = Uint8Array.from(DEXTER_VAULT_PROGRAM_ID.toBytes());
4921
- const dexterPubkeyBytes = import_bs58.default.decode(dexterMasterPubkey);
4922
+ const dexterPubkeyBytes = bs58.decode(dexterMasterPubkey);
4922
4923
  const bootstrapAuthorityInfo = (0, import_lib.createEd25519AuthorityInfo)(feePayerBytes);
4923
4924
  const bootstrapActions = import_lib.Actions.set().manageAuthority().get();
4924
4925
  const vaultAuthorityInfo = (0, import_lib.createProgramExecAuthorityInfo)(
@@ -4935,7 +4936,7 @@ async function buildSwigCreationBundle(params) {
4935
4936
  dexterPubkeyBytes,
4936
4937
  sessionTtlSeconds
4937
4938
  );
4938
- const sessionActions = import_lib.Actions.set().tokenLimit({ mint: import_bs58.default.decode(USDC_MAINNET), amount: spendLimitAtomic }).programAll().get();
4939
+ const sessionActions = import_lib.Actions.set().tokenLimit({ mint: bs58.decode(USDC_MAINNET), amount: spendLimitAtomic }).programAll().get();
4939
4940
  const builder = (0, import_lib.getCreateSwigWithMultipleAuthoritiesInstructionContextBuilder)({
4940
4941
  payer: address(feePayer),
4941
4942
  swigAddress: address(swigAddressStr),
@@ -4948,7 +4949,7 @@ async function buildSwigCreationBundle(params) {
4948
4949
  const instructions = contexts.flatMap((ctx) => (0, import_kit.getInstructionsFromContext)(ctx));
4949
4950
  return {
4950
4951
  swigAddress: swigAddressStr,
4951
- swigIdBase58: import_bs58.default.encode(swigId),
4952
+ swigIdBase58: bs58.encode(swigId),
4952
4953
  instructions
4953
4954
  };
4954
4955
  }
@@ -4969,7 +4970,7 @@ async function verifySwigIsOurs(params) {
4969
4970
  const rpc = createSolanaRpc(rpcEndpoint);
4970
4971
  const swig = await (0, import_kit.fetchNullableSwig)(rpc, address(swigAddress));
4971
4972
  if (swig) {
4972
- const ourRoles = swig.findRolesByAuthorityAddress(import_bs58.default.decode(dexterMasterPubkey));
4973
+ const ourRoles = swig.findRolesByAuthorityAddress(bs58.decode(dexterMasterPubkey));
4973
4974
  if (!ourRoles || ourRoles.length === 0) {
4974
4975
  return {
4975
4976
  ok: false,
@@ -4843,8 +4843,9 @@ function createSolanaRpcFromTransport(transport) {
4843
4843
  }
4844
4844
 
4845
4845
  // src/instructions/swigBundle.ts
4846
- import bs58 from "bs58";
4846
+ import * as bs58Module from "bs58";
4847
4847
  import { PublicKey as PublicKey11 } from "@solana/web3.js";
4848
+ var bs58 = bs58Module.default ?? bs58Module;
4848
4849
  var SWIG_ID_DOMAIN = "dexter-swig-id:v1:";
4849
4850
  var DEFAULT_SESSION_TTL_SECONDS = BigInt(30 * 24 * 60 * 60);
4850
4851
  var DEFAULT_SPEND_LIMIT_ATOMIC = BigInt(1e9);
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/signers/browser/index.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ WebAuthnAssertion: () => WebAuthnAssertion,
24
+ WebAuthnAssertionError: () => WebAuthnAssertionError,
25
+ derSignatureToCompactLowS: () => derSignatureToCompactLowS
26
+ });
27
+ module.exports = __toCommonJS(browser_exports);
28
+ var WebAuthnAssertionError = class extends Error {
29
+ code;
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.code = code;
33
+ this.name = "WebAuthnAssertionError";
34
+ }
35
+ };
36
+ var WebAuthnAssertion = class {
37
+ credentialId;
38
+ publicKeyBase64;
39
+ rpId;
40
+ allowCredentials;
41
+ timeoutMs;
42
+ userVerification;
43
+ constructor(config) {
44
+ if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
45
+ throw new WebAuthnAssertionError(
46
+ "invalid_credential_id",
47
+ "credentialId must be a non-empty Uint8Array"
48
+ );
49
+ }
50
+ this.credentialId = config.credentialId;
51
+ this.publicKeyBase64 = config.publicKeyBase64;
52
+ this.rpId = config.rpId;
53
+ this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
54
+ this.timeoutMs = config.timeoutMs ?? 6e4;
55
+ this.userVerification = config.userVerification ?? "preferred";
56
+ }
57
+ /**
58
+ * Run `navigator.credentials.get()` over `challenge` and return the
59
+ * three on-chain-ready buffers.
60
+ *
61
+ * The caller is responsible for what `challenge` *is*. For the vault
62
+ * program, this is typically `sha256(opMessage)` minted server-side
63
+ * with replay defense (see DexterApiBrowserPasskeySigner). The SDK
64
+ * does not impose policy here.
65
+ */
66
+ async assertOver(challenge) {
67
+ ensureBrowser();
68
+ if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
69
+ throw new WebAuthnAssertionError(
70
+ "invalid_challenge",
71
+ "challenge must be a non-empty Uint8Array"
72
+ );
73
+ }
74
+ const requestOptions = {
75
+ challenge: toBufferSource(challenge),
76
+ allowCredentials: this.allowCredentials.map((c) => ({
77
+ id: toBufferSource(c.id),
78
+ type: "public-key",
79
+ transports: c.transports
80
+ })),
81
+ timeout: this.timeoutMs,
82
+ userVerification: this.userVerification,
83
+ ...this.rpId ? { rpId: this.rpId } : {}
84
+ };
85
+ const credential = await navigator.credentials.get({
86
+ publicKey: requestOptions
87
+ });
88
+ if (!credential) {
89
+ throw new WebAuthnAssertionError(
90
+ "user_cancelled",
91
+ "no assertion returned from authenticator"
92
+ );
93
+ }
94
+ if (credential.type !== "public-key") {
95
+ throw new WebAuthnAssertionError(
96
+ "credential_invalid",
97
+ `unexpected credential type: ${credential.type}`
98
+ );
99
+ }
100
+ const assertion = credential.response;
101
+ const derSignature = new Uint8Array(assertion.signature);
102
+ const compactSignature = derSignatureToCompactLowS(derSignature);
103
+ return {
104
+ signature: compactSignature,
105
+ clientDataJSON: new Uint8Array(assertion.clientDataJSON),
106
+ authenticatorData: new Uint8Array(assertion.authenticatorData)
107
+ };
108
+ }
109
+ /**
110
+ * `PasskeySigner` shape — alias for `assertOver`. Consumers that want
111
+ * to type against `PasskeySigner` (e.g. dexter-fe's
112
+ * `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
113
+ * that want the explicit name call `.assertOver(challenge)`. Same
114
+ * function, two names.
115
+ */
116
+ sign(challenge) {
117
+ return this.assertOver(challenge);
118
+ }
119
+ };
120
+ function ensureBrowser() {
121
+ if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
122
+ throw new WebAuthnAssertionError(
123
+ "not_browser",
124
+ "WebAuthnAssertion requires a browser environment (navigator.credentials)"
125
+ );
126
+ }
127
+ const cred = globalThis.navigator.credentials;
128
+ if (!cred || typeof cred.get !== "function") {
129
+ throw new WebAuthnAssertionError(
130
+ "webauthn_unsupported",
131
+ "this environment does not implement navigator.credentials.get"
132
+ );
133
+ }
134
+ }
135
+ function toBufferSource(bytes) {
136
+ const out = new ArrayBuffer(bytes.length);
137
+ new Uint8Array(out).set(bytes);
138
+ return out;
139
+ }
140
+ var P256_ORDER = BigInt(
141
+ "0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
142
+ );
143
+ var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
144
+ function bigintFromBytes(buf) {
145
+ let n = 0n;
146
+ for (const byte of buf) n = n << 8n | BigInt(byte);
147
+ return n;
148
+ }
149
+ function bytesFromBigint(n, length) {
150
+ const out = new Uint8Array(length);
151
+ for (let i = length - 1; i >= 0; i -= 1) {
152
+ out[i] = Number(n & 0xffn);
153
+ n >>= 8n;
154
+ }
155
+ return out;
156
+ }
157
+ function derSignatureToCompactLowS(der) {
158
+ let i = 0;
159
+ if (der[i++] !== 48) {
160
+ throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
161
+ }
162
+ i++;
163
+ if (der[i++] !== 2) {
164
+ throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
165
+ }
166
+ const rLen = der[i++];
167
+ if (rLen === void 0) {
168
+ throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
169
+ }
170
+ let r = der.slice(i, i + rLen);
171
+ i += rLen;
172
+ if (der[i++] !== 2) {
173
+ throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
174
+ }
175
+ const sLen = der[i++];
176
+ if (sLen === void 0) {
177
+ throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
178
+ }
179
+ let s = der.slice(i, i + sLen);
180
+ i += sLen;
181
+ if (r.length > 32 && r[0] === 0) r = r.slice(1);
182
+ if (s.length > 32 && s[0] === 0) s = s.slice(1);
183
+ if (r.length > 32 || s.length > 32) {
184
+ throw new WebAuthnAssertionError(
185
+ "bad_signature",
186
+ "DER component too large for P-256"
187
+ );
188
+ }
189
+ const rPadded = new Uint8Array(32);
190
+ rPadded.set(r, 32 - r.length);
191
+ let sN = bigintFromBytes(s);
192
+ if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
193
+ const sPadded = bytesFromBigint(sN, 32);
194
+ const out = new Uint8Array(64);
195
+ out.set(rPadded, 0);
196
+ out.set(sPadded, 32);
197
+ return out;
198
+ }
199
+ // Annotate the CommonJS export names for ESM import in node:
200
+ 0 && (module.exports = {
201
+ WebAuthnAssertion,
202
+ WebAuthnAssertionError,
203
+ derSignatureToCompactLowS
204
+ });
@@ -0,0 +1,95 @@
1
+ import { PasskeySigner } from '../types.cjs';
2
+
3
+ /**
4
+ * WebAuthnAssertion — pure-browser P-256 passkey ceremony.
5
+ *
6
+ * Runs `navigator.credentials.get()` over a server-issued challenge and
7
+ * returns the three bytes the on-chain secp256r1 precompile + vault
8
+ * program need:
9
+ *
10
+ * - signature (64-byte compact r||s with low-S enforcement)
11
+ * - clientDataJSON (raw, what the authenticator hashed)
12
+ * - authenticatorData (raw, what the authenticator signed)
13
+ *
14
+ * Zero `fetch` calls. The consumer composes this with whatever server
15
+ * policy they enforce (replay defense, signature counter, AAGUID
16
+ * capture). For Dexter that policy lives in dexter-fe's
17
+ * `DexterApiBrowserPasskeySigner` adapter.
18
+ *
19
+ * The DER → compact lowS conversion is the canonical implementation —
20
+ * it lifts verbatim from dexter-fe/app/lib/passkey.ts (which had it
21
+ * duplicated in passkey-anon.ts). After v0.2 lands and dexter-fe
22
+ * swaps, those two copies go away.
23
+ */
24
+
25
+ interface WebAuthnAssertionConfig {
26
+ /** Raw credential ID bytes (NOT base64-encoded). */
27
+ credentialId: Uint8Array;
28
+ /** 33-byte SEC1 compressed P-256 pubkey, base64. Kept for symmetry / future use; not consumed by `assertOver`. */
29
+ publicKeyBase64?: string;
30
+ /** WebAuthn relying-party identifier. Defaults to omitting the field (browser uses the page's RP ID). */
31
+ rpId?: string;
32
+ /** Optional allow-list. Default: just `credentialId`. */
33
+ allowCredentials?: Array<{
34
+ id: Uint8Array;
35
+ transports?: AuthenticatorTransport[];
36
+ }>;
37
+ /** WebAuthn timeout in milliseconds. Default 60_000. */
38
+ timeoutMs?: number;
39
+ /** UV requirement. Default "preferred". */
40
+ userVerification?: UserVerificationRequirement;
41
+ }
42
+ interface WebAuthnAssertionResult {
43
+ /** 64-byte compact r||s P-256 signature, lowS-normalized (SIMD-0075 requires lowS). */
44
+ signature: Uint8Array;
45
+ /** Raw clientDataJSON as returned by the authenticator. */
46
+ clientDataJSON: Uint8Array;
47
+ /** Raw authenticatorData as returned by the authenticator. */
48
+ authenticatorData: Uint8Array;
49
+ }
50
+ declare class WebAuthnAssertionError extends Error {
51
+ readonly code: string;
52
+ constructor(code: string, message: string);
53
+ }
54
+ /**
55
+ * Pure-browser WebAuthn assertion driver. Implements `PasskeySigner` so
56
+ * adapters that compose with server policy can plug straight in.
57
+ */
58
+ declare class WebAuthnAssertion implements PasskeySigner {
59
+ readonly credentialId: Uint8Array;
60
+ readonly publicKeyBase64?: string;
61
+ private readonly rpId?;
62
+ private readonly allowCredentials;
63
+ private readonly timeoutMs;
64
+ private readonly userVerification;
65
+ constructor(config: WebAuthnAssertionConfig);
66
+ /**
67
+ * Run `navigator.credentials.get()` over `challenge` and return the
68
+ * three on-chain-ready buffers.
69
+ *
70
+ * The caller is responsible for what `challenge` *is*. For the vault
71
+ * program, this is typically `sha256(opMessage)` minted server-side
72
+ * with replay defense (see DexterApiBrowserPasskeySigner). The SDK
73
+ * does not impose policy here.
74
+ */
75
+ assertOver(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
76
+ /**
77
+ * `PasskeySigner` shape — alias for `assertOver`. Consumers that want
78
+ * to type against `PasskeySigner` (e.g. dexter-fe's
79
+ * `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
80
+ * that want the explicit name call `.assertOver(challenge)`. Same
81
+ * function, two names.
82
+ */
83
+ sign(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
84
+ }
85
+ /**
86
+ * Parse an ASN.1 DER ECDSA signature and return the 64-byte (r||s) form
87
+ * with s normalized to lowS (s ≤ n/2). SIMD-0075 rejects high-S
88
+ * signatures to prevent malleability replay.
89
+ *
90
+ * Exported so byte-parity tests can lock the conversion against the
91
+ * dexter-fe implementation it replaces.
92
+ */
93
+ declare function derSignatureToCompactLowS(der: Uint8Array): Uint8Array;
94
+
95
+ export { WebAuthnAssertion, type WebAuthnAssertionConfig, WebAuthnAssertionError, type WebAuthnAssertionResult, derSignatureToCompactLowS };
@@ -0,0 +1,95 @@
1
+ import { PasskeySigner } from '../types.js';
2
+
3
+ /**
4
+ * WebAuthnAssertion — pure-browser P-256 passkey ceremony.
5
+ *
6
+ * Runs `navigator.credentials.get()` over a server-issued challenge and
7
+ * returns the three bytes the on-chain secp256r1 precompile + vault
8
+ * program need:
9
+ *
10
+ * - signature (64-byte compact r||s with low-S enforcement)
11
+ * - clientDataJSON (raw, what the authenticator hashed)
12
+ * - authenticatorData (raw, what the authenticator signed)
13
+ *
14
+ * Zero `fetch` calls. The consumer composes this with whatever server
15
+ * policy they enforce (replay defense, signature counter, AAGUID
16
+ * capture). For Dexter that policy lives in dexter-fe's
17
+ * `DexterApiBrowserPasskeySigner` adapter.
18
+ *
19
+ * The DER → compact lowS conversion is the canonical implementation —
20
+ * it lifts verbatim from dexter-fe/app/lib/passkey.ts (which had it
21
+ * duplicated in passkey-anon.ts). After v0.2 lands and dexter-fe
22
+ * swaps, those two copies go away.
23
+ */
24
+
25
+ interface WebAuthnAssertionConfig {
26
+ /** Raw credential ID bytes (NOT base64-encoded). */
27
+ credentialId: Uint8Array;
28
+ /** 33-byte SEC1 compressed P-256 pubkey, base64. Kept for symmetry / future use; not consumed by `assertOver`. */
29
+ publicKeyBase64?: string;
30
+ /** WebAuthn relying-party identifier. Defaults to omitting the field (browser uses the page's RP ID). */
31
+ rpId?: string;
32
+ /** Optional allow-list. Default: just `credentialId`. */
33
+ allowCredentials?: Array<{
34
+ id: Uint8Array;
35
+ transports?: AuthenticatorTransport[];
36
+ }>;
37
+ /** WebAuthn timeout in milliseconds. Default 60_000. */
38
+ timeoutMs?: number;
39
+ /** UV requirement. Default "preferred". */
40
+ userVerification?: UserVerificationRequirement;
41
+ }
42
+ interface WebAuthnAssertionResult {
43
+ /** 64-byte compact r||s P-256 signature, lowS-normalized (SIMD-0075 requires lowS). */
44
+ signature: Uint8Array;
45
+ /** Raw clientDataJSON as returned by the authenticator. */
46
+ clientDataJSON: Uint8Array;
47
+ /** Raw authenticatorData as returned by the authenticator. */
48
+ authenticatorData: Uint8Array;
49
+ }
50
+ declare class WebAuthnAssertionError extends Error {
51
+ readonly code: string;
52
+ constructor(code: string, message: string);
53
+ }
54
+ /**
55
+ * Pure-browser WebAuthn assertion driver. Implements `PasskeySigner` so
56
+ * adapters that compose with server policy can plug straight in.
57
+ */
58
+ declare class WebAuthnAssertion implements PasskeySigner {
59
+ readonly credentialId: Uint8Array;
60
+ readonly publicKeyBase64?: string;
61
+ private readonly rpId?;
62
+ private readonly allowCredentials;
63
+ private readonly timeoutMs;
64
+ private readonly userVerification;
65
+ constructor(config: WebAuthnAssertionConfig);
66
+ /**
67
+ * Run `navigator.credentials.get()` over `challenge` and return the
68
+ * three on-chain-ready buffers.
69
+ *
70
+ * The caller is responsible for what `challenge` *is*. For the vault
71
+ * program, this is typically `sha256(opMessage)` minted server-side
72
+ * with replay defense (see DexterApiBrowserPasskeySigner). The SDK
73
+ * does not impose policy here.
74
+ */
75
+ assertOver(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
76
+ /**
77
+ * `PasskeySigner` shape — alias for `assertOver`. Consumers that want
78
+ * to type against `PasskeySigner` (e.g. dexter-fe's
79
+ * `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
80
+ * that want the explicit name call `.assertOver(challenge)`. Same
81
+ * function, two names.
82
+ */
83
+ sign(challenge: Uint8Array): Promise<WebAuthnAssertionResult>;
84
+ }
85
+ /**
86
+ * Parse an ASN.1 DER ECDSA signature and return the 64-byte (r||s) form
87
+ * with s normalized to lowS (s ≤ n/2). SIMD-0075 rejects high-S
88
+ * signatures to prevent malleability replay.
89
+ *
90
+ * Exported so byte-parity tests can lock the conversion against the
91
+ * dexter-fe implementation it replaces.
92
+ */
93
+ declare function derSignatureToCompactLowS(der: Uint8Array): Uint8Array;
94
+
95
+ export { WebAuthnAssertion, type WebAuthnAssertionConfig, WebAuthnAssertionError, type WebAuthnAssertionResult, derSignatureToCompactLowS };
@@ -0,0 +1,177 @@
1
+ // src/signers/browser/index.ts
2
+ var WebAuthnAssertionError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "WebAuthnAssertionError";
8
+ }
9
+ };
10
+ var WebAuthnAssertion = class {
11
+ credentialId;
12
+ publicKeyBase64;
13
+ rpId;
14
+ allowCredentials;
15
+ timeoutMs;
16
+ userVerification;
17
+ constructor(config) {
18
+ if (!(config.credentialId instanceof Uint8Array) || config.credentialId.length === 0) {
19
+ throw new WebAuthnAssertionError(
20
+ "invalid_credential_id",
21
+ "credentialId must be a non-empty Uint8Array"
22
+ );
23
+ }
24
+ this.credentialId = config.credentialId;
25
+ this.publicKeyBase64 = config.publicKeyBase64;
26
+ this.rpId = config.rpId;
27
+ this.allowCredentials = config.allowCredentials && config.allowCredentials.length > 0 ? config.allowCredentials : [{ id: config.credentialId }];
28
+ this.timeoutMs = config.timeoutMs ?? 6e4;
29
+ this.userVerification = config.userVerification ?? "preferred";
30
+ }
31
+ /**
32
+ * Run `navigator.credentials.get()` over `challenge` and return the
33
+ * three on-chain-ready buffers.
34
+ *
35
+ * The caller is responsible for what `challenge` *is*. For the vault
36
+ * program, this is typically `sha256(opMessage)` minted server-side
37
+ * with replay defense (see DexterApiBrowserPasskeySigner). The SDK
38
+ * does not impose policy here.
39
+ */
40
+ async assertOver(challenge) {
41
+ ensureBrowser();
42
+ if (!(challenge instanceof Uint8Array) || challenge.length === 0) {
43
+ throw new WebAuthnAssertionError(
44
+ "invalid_challenge",
45
+ "challenge must be a non-empty Uint8Array"
46
+ );
47
+ }
48
+ const requestOptions = {
49
+ challenge: toBufferSource(challenge),
50
+ allowCredentials: this.allowCredentials.map((c) => ({
51
+ id: toBufferSource(c.id),
52
+ type: "public-key",
53
+ transports: c.transports
54
+ })),
55
+ timeout: this.timeoutMs,
56
+ userVerification: this.userVerification,
57
+ ...this.rpId ? { rpId: this.rpId } : {}
58
+ };
59
+ const credential = await navigator.credentials.get({
60
+ publicKey: requestOptions
61
+ });
62
+ if (!credential) {
63
+ throw new WebAuthnAssertionError(
64
+ "user_cancelled",
65
+ "no assertion returned from authenticator"
66
+ );
67
+ }
68
+ if (credential.type !== "public-key") {
69
+ throw new WebAuthnAssertionError(
70
+ "credential_invalid",
71
+ `unexpected credential type: ${credential.type}`
72
+ );
73
+ }
74
+ const assertion = credential.response;
75
+ const derSignature = new Uint8Array(assertion.signature);
76
+ const compactSignature = derSignatureToCompactLowS(derSignature);
77
+ return {
78
+ signature: compactSignature,
79
+ clientDataJSON: new Uint8Array(assertion.clientDataJSON),
80
+ authenticatorData: new Uint8Array(assertion.authenticatorData)
81
+ };
82
+ }
83
+ /**
84
+ * `PasskeySigner` shape — alias for `assertOver`. Consumers that want
85
+ * to type against `PasskeySigner` (e.g. dexter-fe's
86
+ * `DexterApiBrowserPasskeySigner`) call `.sign(challenge)`; consumers
87
+ * that want the explicit name call `.assertOver(challenge)`. Same
88
+ * function, two names.
89
+ */
90
+ sign(challenge) {
91
+ return this.assertOver(challenge);
92
+ }
93
+ };
94
+ function ensureBrowser() {
95
+ if (typeof globalThis === "undefined" || typeof globalThis.navigator === "undefined") {
96
+ throw new WebAuthnAssertionError(
97
+ "not_browser",
98
+ "WebAuthnAssertion requires a browser environment (navigator.credentials)"
99
+ );
100
+ }
101
+ const cred = globalThis.navigator.credentials;
102
+ if (!cred || typeof cred.get !== "function") {
103
+ throw new WebAuthnAssertionError(
104
+ "webauthn_unsupported",
105
+ "this environment does not implement navigator.credentials.get"
106
+ );
107
+ }
108
+ }
109
+ function toBufferSource(bytes) {
110
+ const out = new ArrayBuffer(bytes.length);
111
+ new Uint8Array(out).set(bytes);
112
+ return out;
113
+ }
114
+ var P256_ORDER = BigInt(
115
+ "0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
116
+ );
117
+ var P256_HALF_ORDER = P256_ORDER >> BigInt(1);
118
+ function bigintFromBytes(buf) {
119
+ let n = 0n;
120
+ for (const byte of buf) n = n << 8n | BigInt(byte);
121
+ return n;
122
+ }
123
+ function bytesFromBigint(n, length) {
124
+ const out = new Uint8Array(length);
125
+ for (let i = length - 1; i >= 0; i -= 1) {
126
+ out[i] = Number(n & 0xffn);
127
+ n >>= 8n;
128
+ }
129
+ return out;
130
+ }
131
+ function derSignatureToCompactLowS(der) {
132
+ let i = 0;
133
+ if (der[i++] !== 48) {
134
+ throw new WebAuthnAssertionError("bad_signature", "expected DER SEQUENCE");
135
+ }
136
+ i++;
137
+ if (der[i++] !== 2) {
138
+ throw new WebAuthnAssertionError("bad_signature", "expected r INTEGER");
139
+ }
140
+ const rLen = der[i++];
141
+ if (rLen === void 0) {
142
+ throw new WebAuthnAssertionError("bad_signature", "truncated DER (no r length)");
143
+ }
144
+ let r = der.slice(i, i + rLen);
145
+ i += rLen;
146
+ if (der[i++] !== 2) {
147
+ throw new WebAuthnAssertionError("bad_signature", "expected s INTEGER");
148
+ }
149
+ const sLen = der[i++];
150
+ if (sLen === void 0) {
151
+ throw new WebAuthnAssertionError("bad_signature", "truncated DER (no s length)");
152
+ }
153
+ let s = der.slice(i, i + sLen);
154
+ i += sLen;
155
+ if (r.length > 32 && r[0] === 0) r = r.slice(1);
156
+ if (s.length > 32 && s[0] === 0) s = s.slice(1);
157
+ if (r.length > 32 || s.length > 32) {
158
+ throw new WebAuthnAssertionError(
159
+ "bad_signature",
160
+ "DER component too large for P-256"
161
+ );
162
+ }
163
+ const rPadded = new Uint8Array(32);
164
+ rPadded.set(r, 32 - r.length);
165
+ let sN = bigintFromBytes(s);
166
+ if (sN > P256_HALF_ORDER) sN = P256_ORDER - sN;
167
+ const sPadded = bytesFromBigint(sN, 32);
168
+ const out = new Uint8Array(64);
169
+ out.set(rPadded, 0);
170
+ out.set(sPadded, 32);
171
+ return out;
172
+ }
173
+ export {
174
+ WebAuthnAssertion,
175
+ WebAuthnAssertionError,
176
+ derSignatureToCompactLowS
177
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexterai/vault",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Canonical off-chain mirror of the dexter-vault Solana Anchor program — Solana instruction builders, byte-precise message encoders, account decoders, secp256r1/Ed25519 precompile helpers, counterfactual Swig derivation, and signer interfaces. The single source of truth for any TypeScript code that produces bytes the on-chain program will verify.",
5
5
  "author": "Dexter",
6
6
  "license": "MIT",
@@ -15,7 +15,8 @@
15
15
  "./reader": { "types": "./dist/reader/index.d.ts", "import": "./dist/reader/index.js", "require": "./dist/reader/index.cjs" },
16
16
  "./precompile": { "types": "./dist/precompile/index.d.ts", "import": "./dist/precompile/index.js", "require": "./dist/precompile/index.cjs" },
17
17
  "./signers": { "types": "./dist/signers/types.d.ts", "import": "./dist/signers/types.js", "require": "./dist/signers/types.cjs" },
18
- "./signers/node": { "types": "./dist/signers/node/index.d.ts", "import": "./dist/signers/node/index.js", "require": "./dist/signers/node/index.cjs" }
18
+ "./signers/node": { "types": "./dist/signers/node/index.d.ts", "import": "./dist/signers/node/index.js", "require": "./dist/signers/node/index.cjs" },
19
+ "./signers/browser": { "types": "./dist/signers/browser/index.d.ts", "import": "./dist/signers/browser/index.js", "require": "./dist/signers/browser/index.cjs" }
19
20
  },
20
21
  "files": ["dist", "README.md", "LICENSE", "assets"],
21
22
  "scripts": {