@voltro/plugin-auth 0.1.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,75 @@
1
+ export declare type AssertionOutcome = {
2
+ readonly ok: true;
3
+ readonly result: AssertionResult;
4
+ } | {
5
+ readonly ok: false;
6
+ readonly error: WebAuthnError;
7
+ };
8
+
9
+ export declare interface AssertionResult {
10
+ /** The new signature counter to persist. */
11
+ readonly newCounter: number;
12
+ }
13
+
14
+ export declare interface AssertionVerifyInput {
15
+ readonly clientDataJSON: string;
16
+ readonly authenticatorData: string;
17
+ /** base64url signature over `authenticatorData ‖ sha256(clientDataJSON)`. */
18
+ readonly signature: string;
19
+ readonly expectedChallenge: string;
20
+ readonly expectedOrigin: string;
21
+ readonly rpId: string;
22
+ /** The stored COSE public key (base64url) for the asserted credential. */
23
+ readonly storedPublicKey: string;
24
+ /** The stored signature counter for the credential. */
25
+ readonly storedCounter: number;
26
+ }
27
+
28
+ /** Mint a fresh WebAuthn challenge (base64url, 32 random bytes). The
29
+ * server stashes it (keyed by user / ceremony) and hands it to the
30
+ * browser; `verifyRegistration` / `verifyAssertion` check it back. */
31
+ export declare const generateChallenge: () => string;
32
+
33
+ export declare type RegistrationOutcome = {
34
+ readonly ok: true;
35
+ readonly result: RegistrationResult;
36
+ } | {
37
+ readonly ok: false;
38
+ readonly error: WebAuthnError;
39
+ };
40
+
41
+ export declare interface RegistrationResult {
42
+ /** base64url credential id to store. */
43
+ readonly credentialId: string;
44
+ /** base64url COSE public key to store. */
45
+ readonly publicKey: string;
46
+ /** Initial signature counter. */
47
+ readonly counter: number;
48
+ }
49
+
50
+ export declare interface RegistrationVerifyInput {
51
+ /** base64url clientDataJSON from `navigator.credentials.create`. */
52
+ readonly clientDataJSON: string;
53
+ /** base64url authenticatorData (or attestationObject's authData — see
54
+ * the client helper, which forwards the raw authenticatorData). */
55
+ readonly authenticatorData: string;
56
+ /** The challenge the server issued for this ceremony (base64url). */
57
+ readonly expectedChallenge: string;
58
+ /** The exact origin the ceremony must have run on (e.g.
59
+ * `https://app.example.com`). */
60
+ readonly expectedOrigin: string;
61
+ /** The relying-party id (registrable domain, e.g. `example.com`). */
62
+ readonly rpId: string;
63
+ }
64
+
65
+ /** Verify a passkey assertion (sign-in). */
66
+ export declare const verifyAssertion: (input: AssertionVerifyInput) => AssertionOutcome;
67
+
68
+ /** Verify a passkey registration. Attestation is intentionally NOT
69
+ * checked (90% path); everything binding the credential to this
70
+ * origin/rpId IS. */
71
+ export declare const verifyRegistration: (input: RegistrationVerifyInput) => RegistrationOutcome;
72
+
73
+ export declare type WebAuthnError = 'bad-client-data' | 'wrong-type' | 'challenge-mismatch' | 'origin-mismatch' | 'rpid-mismatch' | 'user-not-present' | 'bad-auth-data' | 'no-credential' | 'unsupported-key' | 'bad-signature' | 'counter-replay';
74
+
75
+ export { }
@@ -0,0 +1,318 @@
1
+ import { createHash as e, createPublicKey as t, createVerify as n, randomBytes as r, timingSafeEqual as i, verify as a } from "node:crypto";
2
+ //#region src/webauthn.ts
3
+ var o = (e) => {
4
+ let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4);
5
+ return Buffer.from(t, "base64");
6
+ }, s = (e) => e.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), c = (t) => e("sha256").update(t).digest(), l = (e) => {
7
+ try {
8
+ let t = JSON.parse(o(e).toString("utf8"));
9
+ return typeof t.type != "string" || typeof t.challenge != "string" || typeof t.origin != "string" ? null : {
10
+ type: t.type,
11
+ challenge: t.challenge,
12
+ origin: t.origin
13
+ };
14
+ } catch {
15
+ return null;
16
+ }
17
+ }, u = 1, d = 64, f = (e) => {
18
+ if (e.length < 37) return null;
19
+ let t = e.subarray(0, 32), n = e[32], r = e.readUInt32BE(33);
20
+ if ((n & d) === 0) return {
21
+ rpIdHash: t,
22
+ flags: n,
23
+ signCount: r
24
+ };
25
+ if (e.length < 55) return null;
26
+ let i = 55 + e.readUInt16BE(53);
27
+ return e.length < i ? null : {
28
+ rpIdHash: t,
29
+ flags: n,
30
+ signCount: r,
31
+ credentialId: e.subarray(55, i),
32
+ cosePublicKey: e.subarray(i)
33
+ };
34
+ }, p = (e, t) => {
35
+ if (t >= e.length) return null;
36
+ let n = e[t], r = n >> 5, i = n & 31, a = (t) => i < 24 ? {
37
+ len: i,
38
+ next: t + 1
39
+ } : i === 24 ? e.length > t + 1 ? {
40
+ len: e[t + 1],
41
+ next: t + 2
42
+ } : null : i === 25 ? e.length > t + 2 ? {
43
+ len: e.readUInt16BE(t + 1),
44
+ next: t + 3
45
+ } : null : i === 26 && e.length > t + 4 ? {
46
+ len: e.readUInt32BE(t + 1),
47
+ next: t + 5
48
+ } : null;
49
+ switch (r) {
50
+ case 0: {
51
+ let e = a(t);
52
+ return e ? {
53
+ value: e.len,
54
+ next: e.next
55
+ } : null;
56
+ }
57
+ case 1: {
58
+ let e = a(t);
59
+ return e ? {
60
+ value: -1 - e.len,
61
+ next: e.next
62
+ } : null;
63
+ }
64
+ case 2: {
65
+ let n = a(t);
66
+ if (!n) return null;
67
+ let r = n.next + n.len;
68
+ return r > e.length ? null : {
69
+ value: e.subarray(n.next, r),
70
+ next: r
71
+ };
72
+ }
73
+ case 5: {
74
+ let n = a(t);
75
+ if (!n) return null;
76
+ let r = /* @__PURE__ */ new Map(), i = n.next;
77
+ for (let t = 0; t < n.len; t++) {
78
+ let t = p(e, i);
79
+ if (!t) return null;
80
+ let n = p(e, t.next);
81
+ if (!n) return null;
82
+ r.set(typeof t.value == "number" ? t.value : JSON.stringify(t.value), n.value), i = n.next;
83
+ }
84
+ return {
85
+ value: r,
86
+ next: i
87
+ };
88
+ }
89
+ default: return null;
90
+ }
91
+ }, m = (e) => {
92
+ let t = p(e, 0);
93
+ if (!t || !(t.value instanceof Map)) return null;
94
+ let n = t.value, r = (e) => {
95
+ let t = n.get(e);
96
+ return typeof t == "number" ? t : void 0;
97
+ }, i = (e) => {
98
+ let t = n.get(e);
99
+ return t instanceof Buffer ? t : void 0;
100
+ }, a = r(1), o = r(3);
101
+ if (a === void 0 || o === void 0) return null;
102
+ let s = r(-1), c = i(-2), l = i(-3), u = a === 3 ? i(-1) : void 0, d = a === 3 ? i(-2) : void 0;
103
+ return {
104
+ kty: a,
105
+ alg: o,
106
+ ...s === void 0 ? {} : { crv: s },
107
+ ...c === void 0 ? {} : { x: c },
108
+ ...l === void 0 ? {} : { y: l },
109
+ ...u === void 0 ? {} : { n: u },
110
+ ...d === void 0 ? {} : { e: d }
111
+ };
112
+ }, h = (e) => {
113
+ let n = m(e);
114
+ if (!n) return null;
115
+ if (n.kty === 2 && n.crv === 1 && n.x && n.y) {
116
+ let e = x(n.x, n.y);
117
+ try {
118
+ return {
119
+ key: t({
120
+ key: e,
121
+ format: "der",
122
+ type: "spki"
123
+ }),
124
+ alg: n.alg
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ if (n.kty === 3 && n.n && n.e) {
131
+ let e = S(n.n, n.e);
132
+ try {
133
+ return {
134
+ key: t({
135
+ key: e,
136
+ format: "der",
137
+ type: "spki"
138
+ }),
139
+ alg: n.alg
140
+ };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ return null;
146
+ }, g = (e) => {
147
+ if (e < 128) return Buffer.from([e]);
148
+ let t = [], n = e;
149
+ for (; n > 0;) t.unshift(n & 255), n >>= 8;
150
+ return Buffer.from([128 | t.length, ...t]);
151
+ }, _ = (...e) => {
152
+ let t = Buffer.concat(e);
153
+ return Buffer.concat([
154
+ Buffer.from([48]),
155
+ g(t.length),
156
+ t
157
+ ]);
158
+ }, v = (e) => {
159
+ let t = Buffer.concat([Buffer.from([0]), e]);
160
+ return Buffer.concat([
161
+ Buffer.from([3]),
162
+ g(t.length),
163
+ t
164
+ ]);
165
+ }, y = (e) => {
166
+ let t = e;
167
+ for (; t.length > 1 && t[0] === 0;) t = t.subarray(1);
168
+ return t[0] & 128 && (t = Buffer.concat([Buffer.from([0]), t])), Buffer.concat([
169
+ Buffer.from([2]),
170
+ g(t.length),
171
+ t
172
+ ]);
173
+ }, b = (e) => Buffer.concat([
174
+ Buffer.from([6]),
175
+ g(e.length),
176
+ Buffer.from(e)
177
+ ]), x = (e, t) => {
178
+ let n = Buffer.concat([
179
+ Buffer.from([4]),
180
+ e,
181
+ t
182
+ ]);
183
+ return _(_(b([
184
+ 42,
185
+ 134,
186
+ 72,
187
+ 206,
188
+ 61,
189
+ 2,
190
+ 1
191
+ ]), b([
192
+ 42,
193
+ 134,
194
+ 72,
195
+ 206,
196
+ 61,
197
+ 3,
198
+ 1,
199
+ 7
200
+ ])), v(n));
201
+ }, S = (e, t) => _(_(b([
202
+ 42,
203
+ 134,
204
+ 72,
205
+ 134,
206
+ 247,
207
+ 13,
208
+ 1,
209
+ 1,
210
+ 1
211
+ ]), Buffer.from([5, 0])), v(_(y(e), y(t)))), C = (e) => {
212
+ let t = l(e.clientDataJSON);
213
+ if (!t) return {
214
+ ok: !1,
215
+ error: "bad-client-data"
216
+ };
217
+ if (t.type !== "webauthn.create") return {
218
+ ok: !1,
219
+ error: "wrong-type"
220
+ };
221
+ if (!E(t.challenge, e.expectedChallenge)) return {
222
+ ok: !1,
223
+ error: "challenge-mismatch"
224
+ };
225
+ if (t.origin !== e.expectedOrigin) return {
226
+ ok: !1,
227
+ error: "origin-mismatch"
228
+ };
229
+ let n = f(o(e.authenticatorData));
230
+ return n ? n.rpIdHash.equals(c(Buffer.from(e.rpId, "utf8"))) ? (n.flags & u) === 0 ? {
231
+ ok: !1,
232
+ error: "user-not-present"
233
+ } : !n.credentialId || !n.cosePublicKey ? {
234
+ ok: !1,
235
+ error: "no-credential"
236
+ } : h(n.cosePublicKey) ? {
237
+ ok: !0,
238
+ result: {
239
+ credentialId: s(n.credentialId),
240
+ publicKey: s(n.cosePublicKey),
241
+ counter: n.signCount
242
+ }
243
+ } : {
244
+ ok: !1,
245
+ error: "unsupported-key"
246
+ } : {
247
+ ok: !1,
248
+ error: "rpid-mismatch"
249
+ } : {
250
+ ok: !1,
251
+ error: "bad-auth-data"
252
+ };
253
+ }, w = (e) => {
254
+ let t = l(e.clientDataJSON);
255
+ if (!t) return {
256
+ ok: !1,
257
+ error: "bad-client-data"
258
+ };
259
+ if (t.type !== "webauthn.get") return {
260
+ ok: !1,
261
+ error: "wrong-type"
262
+ };
263
+ if (!E(t.challenge, e.expectedChallenge)) return {
264
+ ok: !1,
265
+ error: "challenge-mismatch"
266
+ };
267
+ if (t.origin !== e.expectedOrigin) return {
268
+ ok: !1,
269
+ error: "origin-mismatch"
270
+ };
271
+ let n = o(e.authenticatorData), r = f(n);
272
+ if (!r) return {
273
+ ok: !1,
274
+ error: "bad-auth-data"
275
+ };
276
+ if (!r.rpIdHash.equals(c(Buffer.from(e.rpId, "utf8")))) return {
277
+ ok: !1,
278
+ error: "rpid-mismatch"
279
+ };
280
+ if ((r.flags & u) === 0) return {
281
+ ok: !1,
282
+ error: "user-not-present"
283
+ };
284
+ let i = h(o(e.storedPublicKey));
285
+ if (!i) return {
286
+ ok: !1,
287
+ error: "unsupported-key"
288
+ };
289
+ let a = Buffer.concat([n, c(o(e.clientDataJSON))]), s = o(e.signature);
290
+ return T(i.key, i.alg, a, s) ? !(r.signCount === 0 && e.storedCounter === 0) && r.signCount <= e.storedCounter ? {
291
+ ok: !1,
292
+ error: "counter-replay"
293
+ } : {
294
+ ok: !0,
295
+ result: { newCounter: r.signCount }
296
+ } : {
297
+ ok: !1,
298
+ error: "bad-signature"
299
+ };
300
+ }, T = (e, t, r, i) => {
301
+ try {
302
+ if (t === -7) {
303
+ let t = n("SHA256");
304
+ return t.update(r), t.end(), t.verify({
305
+ key: e,
306
+ dsaEncoding: "der"
307
+ }, i);
308
+ }
309
+ return t === -257 && a("RSA-SHA256", r, e, i);
310
+ } catch {
311
+ return !1;
312
+ }
313
+ }, E = (e, t) => {
314
+ let n = Buffer.from(e), r = Buffer.from(t);
315
+ return n.length === r.length && i(n, r);
316
+ }, D = () => s(r(32));
317
+ //#endregion
318
+ export { D as generateChallenge, w as verifyAssertion, C as verifyRegistration };
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@voltro/plugin-auth",
3
+ "version": "0.1.0",
4
+ "description": "Authentication primitives: password hashing, session creation, schema mixin + tables. Pairs with the `auth` app template for the UI; both independently usable. Server-side only (uses node:crypto).",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./password": {
27
+ "types": "./dist/password.d.ts",
28
+ "import": "./dist/password.js",
29
+ "default": "./dist/password.js"
30
+ },
31
+ "./session": {
32
+ "types": "./dist/session.d.ts",
33
+ "import": "./dist/session.js",
34
+ "default": "./dist/session.js"
35
+ },
36
+ "./strategy": {
37
+ "types": "./dist/strategy.d.ts",
38
+ "import": "./dist/strategy.js",
39
+ "default": "./dist/strategy.js"
40
+ },
41
+ "./plugin": {
42
+ "types": "./dist/plugin.d.ts",
43
+ "import": "./dist/plugin.js",
44
+ "default": "./dist/plugin.js"
45
+ },
46
+ "./webauthn": {
47
+ "types": "./dist/webauthn.d.ts",
48
+ "import": "./dist/webauthn.js",
49
+ "default": "./dist/webauthn.js"
50
+ },
51
+ "./tokens": {
52
+ "types": "./dist/tokens.d.ts",
53
+ "import": "./dist/tokens.js",
54
+ "default": "./dist/tokens.js"
55
+ },
56
+ "./csrf": {
57
+ "types": "./dist/csrf.d.ts",
58
+ "import": "./dist/csrf.js",
59
+ "default": "./dist/csrf.js"
60
+ },
61
+ "./schema": {
62
+ "types": "./dist/schema.d.ts",
63
+ "import": "./dist/schema.js",
64
+ "default": "./dist/schema.js"
65
+ },
66
+ "./web": {
67
+ "types": "./dist/web.d.ts",
68
+ "import": "./dist/web.js",
69
+ "default": "./dist/web.js"
70
+ }
71
+ },
72
+ "main": "./dist/index.js",
73
+ "module": "./dist/index.js",
74
+ "types": "./dist/index.d.ts",
75
+ "sideEffects": false,
76
+ "engines": {
77
+ "node": ">=24.0.0"
78
+ },
79
+ "dependencies": {
80
+ "@effect/sql": "^0.51.1",
81
+ "@voltro/database": "0.1.0",
82
+ "@voltro/protocol": "0.1.0"
83
+ },
84
+ "peerDependencies": {
85
+ "effect": "^3.21.4",
86
+ "react": "^19.0.0"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "react": {
90
+ "optional": true
91
+ }
92
+ },
93
+ "publishConfig": {
94
+ "access": "public"
95
+ }
96
+ }