@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.
- package/dist/counterfactual.cjs +1 -1
- package/dist/counterfactual.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/instructions/index.cjs +7 -6
- package/dist/instructions/index.js +2 -1
- package/dist/signers/browser/index.cjs +204 -0
- package/dist/signers/browser/index.d.cts +95 -0
- package/dist/signers/browser/index.d.ts +95 -0
- package/dist/signers/browser/index.js +177 -0
- package/package.json +3 -2
package/dist/counterfactual.cjs
CHANGED
|
@@ -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
|
|
42
|
+
var bs58Module = __toESM(require("bs58"), 1);
|
|
43
43
|
var import_web32 = require("@solana/web3.js");
|
|
44
44
|
|
|
45
45
|
// src/constants/index.ts
|
package/dist/counterfactual.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createEd25519SessionAuthorityInfo,
|
|
16
16
|
getCreateSwigWithMultipleAuthoritiesInstructionContextBuilder
|
|
17
17
|
} from "@swig-wallet/lib";
|
|
18
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4920
|
+
const feePayerBytes = bs58.decode(feePayer);
|
|
4920
4921
|
const vaultProgramIdBytes = Uint8Array.from(DEXTER_VAULT_PROGRAM_ID.toBytes());
|
|
4921
|
-
const dexterPubkeyBytes =
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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.
|
|
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": {
|