@vonosan/auth 0.2.1
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/__tests__/passkey.test.d.ts +11 -0
- package/dist/__tests__/passkey.test.d.ts.map +1 -0
- package/dist/__tests__/passkey.test.js +87 -0
- package/dist/__tests__/passkey.test.js.map +1 -0
- package/dist/composables/useAuth.d.ts +43 -0
- package/dist/composables/useAuth.d.ts.map +1 -0
- package/dist/composables/useAuth.js +133 -0
- package/dist/composables/useAuth.js.map +1 -0
- package/dist/composables/usePasskey.d.ts +72 -0
- package/dist/composables/usePasskey.d.ts.map +1 -0
- package/dist/composables/usePasskey.js +289 -0
- package/dist/composables/usePasskey.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/jwt.d.ts +30 -0
- package/dist/lib/jwt.d.ts.map +1 -0
- package/dist/lib/jwt.js +43 -0
- package/dist/lib/jwt.js.map +1 -0
- package/dist/lib/otp.d.ts +23 -0
- package/dist/lib/otp.d.ts.map +1 -0
- package/dist/lib/otp.js +50 -0
- package/dist/lib/otp.js.map +1 -0
- package/dist/lib/passkey.d.ts +139 -0
- package/dist/lib/passkey.d.ts.map +1 -0
- package/dist/lib/passkey.js +401 -0
- package/dist/lib/passkey.js.map +1 -0
- package/dist/lib/password.d.ts +20 -0
- package/dist/lib/password.d.ts.map +1 -0
- package/dist/lib/password.js +77 -0
- package/dist/lib/password.js.map +1 -0
- package/dist/middleware/auth.middleware.d.ts +50 -0
- package/dist/middleware/auth.middleware.d.ts.map +1 -0
- package/dist/middleware/auth.middleware.js +194 -0
- package/dist/middleware/auth.middleware.js.map +1 -0
- package/dist/passkey-schema.d.ts +375 -0
- package/dist/passkey-schema.d.ts.map +1 -0
- package/dist/passkey-schema.js +63 -0
- package/dist/passkey-schema.js.map +1 -0
- package/dist/routes/auth.routes.d.ts +16 -0
- package/dist/routes/auth.routes.d.ts.map +1 -0
- package/dist/routes/auth.routes.js +81 -0
- package/dist/routes/auth.routes.js.map +1 -0
- package/dist/routes/passkey.routes.d.ts +16 -0
- package/dist/routes/passkey.routes.d.ts.map +1 -0
- package/dist/routes/passkey.routes.js +127 -0
- package/dist/routes/passkey.routes.js.map +1 -0
- package/dist/schema.d.ts +547 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +81 -0
- package/dist/schema.js.map +1 -0
- package/dist/service/auth.service.d.ts +73 -0
- package/dist/service/auth.service.d.ts.map +1 -0
- package/dist/service/auth.service.js +249 -0
- package/dist/service/auth.service.js.map +1 -0
- package/dist/service/passkey.service.d.ts +65 -0
- package/dist/service/passkey.service.d.ts.map +1 -0
- package/dist/service/passkey.service.js +202 -0
- package/dist/service/passkey.service.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=passkey.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/passkey.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'bun:test';
|
|
11
|
+
import { generateChallenge, buildRegistrationOptions, buildAuthenticationOptions, bufferToBase64url, base64urlToBuffer, } from '../lib/passkey.js';
|
|
12
|
+
describe('Passkey helpers', () => {
|
|
13
|
+
describe('generateChallenge', () => {
|
|
14
|
+
it('returns a base64url-encoded challenge and expiry', () => {
|
|
15
|
+
const { challenge, expiresAt } = generateChallenge();
|
|
16
|
+
expect(typeof challenge).toBe('string');
|
|
17
|
+
expect(challenge.length).toBeGreaterThan(0);
|
|
18
|
+
expect(expiresAt).toBeGreaterThan(Date.now());
|
|
19
|
+
});
|
|
20
|
+
it('generates unique challenges on each call', () => {
|
|
21
|
+
const a = generateChallenge();
|
|
22
|
+
const b = generateChallenge();
|
|
23
|
+
expect(a.challenge).not.toBe(b.challenge);
|
|
24
|
+
});
|
|
25
|
+
it('challenge decodes to 32 bytes', () => {
|
|
26
|
+
const { challenge } = generateChallenge();
|
|
27
|
+
const bytes = base64urlToBuffer(challenge);
|
|
28
|
+
expect(bytes.length).toBe(32);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('buildRegistrationOptions', () => {
|
|
32
|
+
it('returns correct rp, user, and pubKeyCredParams', () => {
|
|
33
|
+
const opts = buildRegistrationOptions({
|
|
34
|
+
rpId: 'example.com',
|
|
35
|
+
rpName: 'Example App',
|
|
36
|
+
userId: bufferToBase64url(new Uint8Array([1, 2, 3])),
|
|
37
|
+
userName: 'alice@example.com',
|
|
38
|
+
userDisplayName: 'Alice',
|
|
39
|
+
challenge: generateChallenge().challenge,
|
|
40
|
+
});
|
|
41
|
+
expect(opts.rp).toEqual({ id: 'example.com', name: 'Example App' });
|
|
42
|
+
expect(Array.isArray(opts.pubKeyCredParams)).toBe(true);
|
|
43
|
+
expect(opts.pubKeyCredParams.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
it('includes ES256 and RS256 in pubKeyCredParams', () => {
|
|
46
|
+
const opts = buildRegistrationOptions({
|
|
47
|
+
rpId: 'example.com',
|
|
48
|
+
rpName: 'Example',
|
|
49
|
+
userId: 'dXNlcg',
|
|
50
|
+
userName: 'user',
|
|
51
|
+
userDisplayName: 'User',
|
|
52
|
+
challenge: 'abc123',
|
|
53
|
+
});
|
|
54
|
+
const algs = opts.pubKeyCredParams.map((p) => p.alg);
|
|
55
|
+
expect(algs).toContain(-7); // ES256
|
|
56
|
+
expect(algs).toContain(-257); // RS256
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('buildAuthenticationOptions', () => {
|
|
60
|
+
it('returns rpId, challenge, and userVerification', () => {
|
|
61
|
+
const opts = buildAuthenticationOptions({
|
|
62
|
+
rpId: 'example.com',
|
|
63
|
+
challenge: generateChallenge().challenge,
|
|
64
|
+
userVerification: 'preferred',
|
|
65
|
+
});
|
|
66
|
+
expect(opts.rpId).toBe('example.com');
|
|
67
|
+
expect(opts.userVerification).toBe('preferred');
|
|
68
|
+
expect(typeof opts.challenge).toBe('string');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('base64url encoding round-trip', () => {
|
|
72
|
+
it('bufferToBase64url → base64urlToBuffer produces original bytes', () => {
|
|
73
|
+
const original = new Uint8Array([1, 2, 3, 4, 5, 255, 0, 128]);
|
|
74
|
+
const encoded = bufferToBase64url(original);
|
|
75
|
+
const decoded = base64urlToBuffer(encoded);
|
|
76
|
+
expect(decoded).toEqual(original);
|
|
77
|
+
});
|
|
78
|
+
it('does not contain +, /, or = characters', () => {
|
|
79
|
+
const bytes = crypto.getRandomValues(new Uint8Array(64));
|
|
80
|
+
const encoded = bufferToBase64url(bytes);
|
|
81
|
+
expect(encoded).not.toContain('+');
|
|
82
|
+
expect(encoded).not.toContain('/');
|
|
83
|
+
expect(encoded).not.toContain('=');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=passkey.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey.test.js","sourceRoot":"","sources":["../../src/__tests__/passkey.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,0BAA0B,EAC1B,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,mBAAmB,CAAA;AAE1B,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;QACjC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;YAC1D,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,iBAAiB,EAAE,CAAA;YACpD,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACvC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;YAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAC/C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;YAClD,MAAM,CAAC,GAAG,iBAAiB,EAAE,CAAA;YAC7B,MAAM,CAAC,GAAG,iBAAiB,EAAE,CAAA;YAC7B,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC3C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;YACvC,MAAM,EAAE,SAAS,EAAE,GAAG,iBAAiB,EAAE,CAAA;YACzC,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAA;YAC1C,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;QACxC,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;YACxD,MAAM,IAAI,GAAG,wBAAwB,CAAC;gBACpC,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,aAAa;gBACrB,MAAM,EAAE,iBAAiB,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACpD,QAAQ,EAAE,mBAAmB;gBAC7B,eAAe,EAAE,OAAO;gBACxB,SAAS,EAAE,iBAAiB,EAAE,CAAC,SAAS;aACzC,CAAC,CAAA;YAEF,MAAM,CAAE,IAAgC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAA;YAChG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAE,IAAgC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACpF,MAAM,CAAG,IAAgC,CAAC,gBAA8B,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QACrG,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACtD,MAAM,IAAI,GAAG,wBAAwB,CAAC;gBACpC,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,QAAQ;gBAChB,QAAQ,EAAE,MAAM;gBAChB,eAAe,EAAE,MAAM;gBACvB,SAAS,EAAE,QAAQ;aACpB,CAAiD,CAAA;YAElD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YACpD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA,CAAG,QAAQ;YACrC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAA,CAAC,QAAQ;QACvC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;QAC1C,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;YACvD,MAAM,IAAI,GAAG,0BAA0B,CAAC;gBACtC,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,iBAAiB,EAAE,CAAC,SAAS;gBACxC,gBAAgB,EAAE,WAAW;aAC9B,CAA4B,CAAA;YAE7B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;YACrC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC/C,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC9C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;QAC7C,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;YACvE,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC7D,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAA;YAC3C,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;YAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACnC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;YAChD,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;YACxD,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAA;YACxC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;YAClC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;YAClC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
interface AuthUser {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
username: string;
|
|
14
|
+
currentRole: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `useAuth()` — Vue composable for authentication state and actions.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { user, isAuthenticated, login, logout } = useAuth()
|
|
21
|
+
*/
|
|
22
|
+
export declare function useAuth(): {
|
|
23
|
+
user: import("vue").Ref<{
|
|
24
|
+
id: string;
|
|
25
|
+
email: string;
|
|
26
|
+
username: string;
|
|
27
|
+
currentRole: string;
|
|
28
|
+
} | null, AuthUser | {
|
|
29
|
+
id: string;
|
|
30
|
+
email: string;
|
|
31
|
+
username: string;
|
|
32
|
+
currentRole: string;
|
|
33
|
+
} | null>;
|
|
34
|
+
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
35
|
+
isLoading: import("vue").Ref<boolean, boolean>;
|
|
36
|
+
error: import("vue").Ref<string | null, string | null>;
|
|
37
|
+
accessToken: import("vue").Ref<string | null, string | null>;
|
|
38
|
+
login: (email: string, password: string) => Promise<void>;
|
|
39
|
+
register: (email: string, password: string, username?: string) => Promise<void>;
|
|
40
|
+
logout: () => Promise<void>;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=useAuth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/composables/useAuth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AA+BD;;;;;GAKG;AACH,wBAAgB,OAAO;;YAzCjB,MAAM;eACH,MAAM;kBACH,MAAM;qBACH,MAAM;;YAHf,MAAM;eACH,MAAM;kBACH,MAAM;qBACH,MAAM;;;;;;mBAkES,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;sBAoBrC,MAAM,YAAY,MAAM,aAAa,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;kBAmBjE,OAAO,CAAC,IAAI,CAAC;EA8BvC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
import { ref, computed } from 'vue';
|
|
11
|
+
import { useRouter } from 'vue-router';
|
|
12
|
+
// ─── Shared state (module-level singleton) ────────────────────────────────────
|
|
13
|
+
const user = ref(null);
|
|
14
|
+
const accessToken = ref(null);
|
|
15
|
+
const sessionId = ref(null);
|
|
16
|
+
// Restore from localStorage on module load (client-side only)
|
|
17
|
+
if (typeof window !== 'undefined') {
|
|
18
|
+
const stored = localStorage.getItem('vono_auth');
|
|
19
|
+
if (stored) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(stored);
|
|
22
|
+
user.value = parsed.user;
|
|
23
|
+
accessToken.value = parsed.accessToken;
|
|
24
|
+
sessionId.value = parsed.sessionId;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
localStorage.removeItem('vono_auth');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ─── useAuth composable ───────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* `useAuth()` — Vue composable for authentication state and actions.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const { user, isAuthenticated, login, logout } = useAuth()
|
|
37
|
+
*/
|
|
38
|
+
export function useAuth() {
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const isLoading = ref(false);
|
|
41
|
+
const error = ref(null);
|
|
42
|
+
const isAuthenticated = computed(() => user.value !== null);
|
|
43
|
+
function _persist(tokens, accountData) {
|
|
44
|
+
user.value = accountData;
|
|
45
|
+
accessToken.value = tokens.accessToken;
|
|
46
|
+
sessionId.value = tokens.sessionId;
|
|
47
|
+
if (typeof window !== 'undefined') {
|
|
48
|
+
localStorage.setItem('vono_auth', JSON.stringify({ user: accountData, accessToken: tokens.accessToken, sessionId: tokens.sessionId }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function _clear() {
|
|
52
|
+
user.value = null;
|
|
53
|
+
accessToken.value = null;
|
|
54
|
+
sessionId.value = null;
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
localStorage.removeItem('vono_auth');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function login(email, password) {
|
|
60
|
+
isLoading.value = true;
|
|
61
|
+
error.value = null;
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch('/api/v1/auth/login', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ email, password }),
|
|
67
|
+
});
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
if (!body.success)
|
|
70
|
+
throw new Error(body.message);
|
|
71
|
+
_persist(body.data, body.data.user);
|
|
72
|
+
router.push('/');
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
error.value = err.message ?? 'Login failed';
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
isLoading.value = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function register(email, password, username) {
|
|
82
|
+
isLoading.value = true;
|
|
83
|
+
error.value = null;
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch('/api/v1/auth/register', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ email, password, username }),
|
|
89
|
+
});
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
if (!body.success)
|
|
92
|
+
throw new Error(body.message);
|
|
93
|
+
router.push('/login');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
error.value = err.message ?? 'Registration failed';
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
isLoading.value = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function logout() {
|
|
103
|
+
isLoading.value = true;
|
|
104
|
+
try {
|
|
105
|
+
if (sessionId.value) {
|
|
106
|
+
await fetch('/api/v1/auth/logout', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
...(accessToken.value ? { Authorization: `Bearer ${accessToken.value}` } : {}),
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({ sessionId: sessionId.value }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
_clear();
|
|
118
|
+
isLoading.value = false;
|
|
119
|
+
router.push('/login');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
user,
|
|
124
|
+
isAuthenticated,
|
|
125
|
+
isLoading,
|
|
126
|
+
error,
|
|
127
|
+
accessToken,
|
|
128
|
+
login,
|
|
129
|
+
register,
|
|
130
|
+
logout,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=useAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../src/composables/useAuth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
export interface PasskeyCredentialInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
credential_id: string;
|
|
13
|
+
name: string | null;
|
|
14
|
+
device_type: string;
|
|
15
|
+
backed_up: boolean;
|
|
16
|
+
last_used_at: string | null;
|
|
17
|
+
created_at: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* usePasskey — Vue composable for WebAuthn/Passkey authentication.
|
|
21
|
+
*
|
|
22
|
+
* Handles the full registration and authentication ceremony:
|
|
23
|
+
* 1. Fetches challenge from the server
|
|
24
|
+
* 2. Calls the browser's WebAuthn API
|
|
25
|
+
* 3. Sends the response back to the server
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ```ts
|
|
29
|
+
* const { registerPasskey, loginWithPasskey, credentials, loading, error } = usePasskey()
|
|
30
|
+
*
|
|
31
|
+
* // Register a new passkey (user must be logged in)
|
|
32
|
+
* await registerPasskey('My iPhone')
|
|
33
|
+
*
|
|
34
|
+
* // Login with a passkey (no username needed)
|
|
35
|
+
* const tokens = await loginWithPasskey()
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function usePasskey(): {
|
|
39
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
40
|
+
error: import("vue").Ref<string | null, string | null>;
|
|
41
|
+
credentials: import("vue").Ref<{
|
|
42
|
+
id: string;
|
|
43
|
+
credential_id: string;
|
|
44
|
+
name: string | null;
|
|
45
|
+
device_type: string;
|
|
46
|
+
backed_up: boolean;
|
|
47
|
+
last_used_at: string | null;
|
|
48
|
+
created_at: string;
|
|
49
|
+
}[], PasskeyCredentialInfo[] | {
|
|
50
|
+
id: string;
|
|
51
|
+
credential_id: string;
|
|
52
|
+
name: string | null;
|
|
53
|
+
device_type: string;
|
|
54
|
+
backed_up: boolean;
|
|
55
|
+
last_used_at: string | null;
|
|
56
|
+
created_at: string;
|
|
57
|
+
}[]>;
|
|
58
|
+
registerPasskey: (name?: string) => Promise<{
|
|
59
|
+
credentialId: string;
|
|
60
|
+
} | null>;
|
|
61
|
+
loginWithPasskey: (accountId?: string) => Promise<{
|
|
62
|
+
accessToken: string;
|
|
63
|
+
refreshToken: string;
|
|
64
|
+
} | null>;
|
|
65
|
+
fetchCredentials: () => Promise<void>;
|
|
66
|
+
renameCredential: (credentialId: string, name: string) => Promise<boolean>;
|
|
67
|
+
deleteCredential: (credentialId: string) => Promise<boolean>;
|
|
68
|
+
isSupported: typeof isWebAuthnSupported;
|
|
69
|
+
};
|
|
70
|
+
declare function isWebAuthnSupported(): boolean;
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=usePasskey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePasskey.d.ts","sourceRoot":"","sources":["../../src/composables/usePasskey.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU;;;;YA5BpB,MAAM;uBACK,MAAM;cACf,MAAM,GAAG,IAAI;qBACN,MAAM;mBACR,OAAO;sBACJ,MAAM,GAAG,IAAI;oBACf,MAAM;;YANd,MAAM;uBACK,MAAM;cACf,MAAM,GAAG,IAAI;qBACN,MAAM;mBACR,OAAO;sBACJ,MAAM,GAAG,IAAI;oBACf,MAAM;;6BAiCoB,MAAM,KAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;mCAyE3C,MAAM,KAAG,OAAO,CAAC;QAC3D,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,IAAI,CAAC;4BA2D0B,OAAO,CAAC,IAAI,CAAC;qCAcF,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;qCAiBvC,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;;EAwBxE;AAID,iBAAS,mBAAmB,IAAI,OAAO,CAMtC"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ──────────────────────────────────────────────────────────────────
|
|
3
|
+
* 🏢 Company Name: Bonifade Technologies
|
|
4
|
+
* 👨💻 Developer: Bowofade Oyerinde
|
|
5
|
+
* 🐙 GitHub: oyenet1
|
|
6
|
+
* 📅 Created Date: 2026-04-05
|
|
7
|
+
* 🔄 Updated Date: 2026-04-05
|
|
8
|
+
* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
*/
|
|
10
|
+
import { ref } from 'vue';
|
|
11
|
+
/**
|
|
12
|
+
* usePasskey — Vue composable for WebAuthn/Passkey authentication.
|
|
13
|
+
*
|
|
14
|
+
* Handles the full registration and authentication ceremony:
|
|
15
|
+
* 1. Fetches challenge from the server
|
|
16
|
+
* 2. Calls the browser's WebAuthn API
|
|
17
|
+
* 3. Sends the response back to the server
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* ```ts
|
|
21
|
+
* const { registerPasskey, loginWithPasskey, credentials, loading, error } = usePasskey()
|
|
22
|
+
*
|
|
23
|
+
* // Register a new passkey (user must be logged in)
|
|
24
|
+
* await registerPasskey('My iPhone')
|
|
25
|
+
*
|
|
26
|
+
* // Login with a passkey (no username needed)
|
|
27
|
+
* const tokens = await loginWithPasskey()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function usePasskey() {
|
|
31
|
+
const loading = ref(false);
|
|
32
|
+
const error = ref(null);
|
|
33
|
+
const credentials = ref([]);
|
|
34
|
+
// ── Registration ──────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Register a new passkey for the currently authenticated user.
|
|
37
|
+
* Requires the user to be logged in (JWT in Authorization header).
|
|
38
|
+
*/
|
|
39
|
+
async function registerPasskey(name) {
|
|
40
|
+
if (!isWebAuthnSupported()) {
|
|
41
|
+
error.value = 'Passkeys are not supported in this browser';
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
loading.value = true;
|
|
45
|
+
error.value = null;
|
|
46
|
+
try {
|
|
47
|
+
// 1. Get challenge from server
|
|
48
|
+
const beginRes = await fetch('/api/v1/auth/passkey/register/begin', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
Authorization: `Bearer ${getStoredToken()}`,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (!beginRes.ok) {
|
|
56
|
+
throw new Error('Failed to begin passkey registration');
|
|
57
|
+
}
|
|
58
|
+
const { data: options } = await beginRes.json();
|
|
59
|
+
// 2. Call browser WebAuthn API
|
|
60
|
+
const credential = await navigator.credentials.create({
|
|
61
|
+
publicKey: deserializeCreationOptions(options),
|
|
62
|
+
});
|
|
63
|
+
if (!credential) {
|
|
64
|
+
throw new Error('Passkey creation was cancelled');
|
|
65
|
+
}
|
|
66
|
+
// 3. Send response to server
|
|
67
|
+
const finishRes = await fetch('/api/v1/auth/passkey/register/finish', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
Authorization: `Bearer ${getStoredToken()}`,
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
response: serializeCredential(credential),
|
|
75
|
+
name,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!finishRes.ok) {
|
|
79
|
+
const body = await finishRes.json();
|
|
80
|
+
throw new Error(body.message || 'Failed to finish passkey registration');
|
|
81
|
+
}
|
|
82
|
+
const { data } = await finishRes.json();
|
|
83
|
+
// Refresh credentials list
|
|
84
|
+
await fetchCredentials();
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
error.value = err instanceof Error ? err.message : 'Passkey registration failed';
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
loading.value = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Authentication ────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Authenticate with a passkey.
|
|
98
|
+
* Supports both usernameless (discoverable credential) and
|
|
99
|
+
* username-first flows.
|
|
100
|
+
*/
|
|
101
|
+
async function loginWithPasskey(accountId) {
|
|
102
|
+
if (!isWebAuthnSupported()) {
|
|
103
|
+
error.value = 'Passkeys are not supported in this browser';
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
loading.value = true;
|
|
107
|
+
error.value = null;
|
|
108
|
+
try {
|
|
109
|
+
// 1. Get challenge from server
|
|
110
|
+
const beginRes = await fetch('/api/v1/auth/passkey/auth/begin', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ accountId }),
|
|
114
|
+
});
|
|
115
|
+
if (!beginRes.ok) {
|
|
116
|
+
throw new Error('Failed to begin passkey authentication');
|
|
117
|
+
}
|
|
118
|
+
const { data: options } = await beginRes.json();
|
|
119
|
+
// 2. Call browser WebAuthn API
|
|
120
|
+
const assertion = await navigator.credentials.get({
|
|
121
|
+
publicKey: deserializeRequestOptions(options),
|
|
122
|
+
});
|
|
123
|
+
if (!assertion) {
|
|
124
|
+
throw new Error('Passkey authentication was cancelled');
|
|
125
|
+
}
|
|
126
|
+
// 3. Send response to server
|
|
127
|
+
const finishRes = await fetch('/api/v1/auth/passkey/auth/finish', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ response: serializeAssertion(assertion) }),
|
|
131
|
+
});
|
|
132
|
+
if (!finishRes.ok) {
|
|
133
|
+
const body = await finishRes.json();
|
|
134
|
+
throw new Error(body.message || 'Passkey authentication failed');
|
|
135
|
+
}
|
|
136
|
+
const { data } = await finishRes.json();
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
error.value = err instanceof Error ? err.message : 'Passkey authentication failed';
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
loading.value = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── Credential management ─────────────────────────────────────────
|
|
148
|
+
async function fetchCredentials() {
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch('/api/v1/auth/passkey/credentials', {
|
|
151
|
+
headers: { Authorization: `Bearer ${getStoredToken()}` },
|
|
152
|
+
});
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
const { data } = await res.json();
|
|
155
|
+
credentials.value = data;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// silently fail
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function renameCredential(credentialId, name) {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(`/api/v1/auth/passkey/credentials/${credentialId}`, {
|
|
165
|
+
method: 'PATCH',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
Authorization: `Bearer ${getStoredToken()}`,
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({ name }),
|
|
171
|
+
});
|
|
172
|
+
if (res.ok)
|
|
173
|
+
await fetchCredentials();
|
|
174
|
+
return res.ok;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function deleteCredential(credentialId) {
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch(`/api/v1/auth/passkey/credentials/${credentialId}`, {
|
|
183
|
+
method: 'DELETE',
|
|
184
|
+
headers: { Authorization: `Bearer ${getStoredToken()}` },
|
|
185
|
+
});
|
|
186
|
+
if (res.ok)
|
|
187
|
+
await fetchCredentials();
|
|
188
|
+
return res.ok;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
loading,
|
|
196
|
+
error,
|
|
197
|
+
credentials,
|
|
198
|
+
registerPasskey,
|
|
199
|
+
loginWithPasskey,
|
|
200
|
+
fetchCredentials,
|
|
201
|
+
renameCredential,
|
|
202
|
+
deleteCredential,
|
|
203
|
+
isSupported: isWebAuthnSupported,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
207
|
+
function isWebAuthnSupported() {
|
|
208
|
+
return (typeof window !== 'undefined' &&
|
|
209
|
+
typeof window.PublicKeyCredential !== 'undefined' &&
|
|
210
|
+
typeof navigator.credentials !== 'undefined');
|
|
211
|
+
}
|
|
212
|
+
function getStoredToken() {
|
|
213
|
+
if (typeof localStorage === 'undefined')
|
|
214
|
+
return '';
|
|
215
|
+
return localStorage.getItem('access_token') ?? '';
|
|
216
|
+
}
|
|
217
|
+
/** Convert base64url strings from server to ArrayBuffers for the browser API */
|
|
218
|
+
function deserializeCreationOptions(options) {
|
|
219
|
+
return {
|
|
220
|
+
...options,
|
|
221
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
222
|
+
user: {
|
|
223
|
+
...options.user,
|
|
224
|
+
id: base64urlToBuffer(options.user.id),
|
|
225
|
+
},
|
|
226
|
+
excludeCredentials: (options.excludeCredentials ?? []).map((c) => ({
|
|
227
|
+
...c,
|
|
228
|
+
id: base64urlToBuffer(c.id),
|
|
229
|
+
})),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function deserializeRequestOptions(options) {
|
|
233
|
+
return {
|
|
234
|
+
...options,
|
|
235
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
236
|
+
allowCredentials: (options.allowCredentials ?? []).map((c) => ({
|
|
237
|
+
...c,
|
|
238
|
+
id: base64urlToBuffer(c.id),
|
|
239
|
+
})),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Serialize a PublicKeyCredential (registration) for sending to the server */
|
|
243
|
+
function serializeCredential(credential) {
|
|
244
|
+
const response = credential.response;
|
|
245
|
+
return {
|
|
246
|
+
id: credential.id,
|
|
247
|
+
rawId: bufferToBase64url(new Uint8Array(credential.rawId)),
|
|
248
|
+
type: credential.type,
|
|
249
|
+
response: {
|
|
250
|
+
clientDataJSON: bufferToBase64url(new Uint8Array(response.clientDataJSON)),
|
|
251
|
+
attestationObject: bufferToBase64url(new Uint8Array(response.attestationObject)),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Serialize a PublicKeyCredential (authentication) for sending to the server */
|
|
256
|
+
function serializeAssertion(credential) {
|
|
257
|
+
const response = credential.response;
|
|
258
|
+
return {
|
|
259
|
+
id: credential.id,
|
|
260
|
+
rawId: bufferToBase64url(new Uint8Array(credential.rawId)),
|
|
261
|
+
type: credential.type,
|
|
262
|
+
response: {
|
|
263
|
+
clientDataJSON: bufferToBase64url(new Uint8Array(response.clientDataJSON)),
|
|
264
|
+
authenticatorData: bufferToBase64url(new Uint8Array(response.authenticatorData)),
|
|
265
|
+
signature: bufferToBase64url(new Uint8Array(response.signature)),
|
|
266
|
+
userHandle: response.userHandle
|
|
267
|
+
? bufferToBase64url(new Uint8Array(response.userHandle))
|
|
268
|
+
: undefined,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function bufferToBase64url(buffer) {
|
|
273
|
+
let binary = '';
|
|
274
|
+
for (let i = 0; i < buffer.length; i++)
|
|
275
|
+
binary += String.fromCharCode(buffer[i]);
|
|
276
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
277
|
+
}
|
|
278
|
+
function base64urlToBuffer(base64url) {
|
|
279
|
+
const base64 = base64url
|
|
280
|
+
.replace(/-/g, '+')
|
|
281
|
+
.replace(/_/g, '/')
|
|
282
|
+
.padEnd(base64url.length + ((4 - (base64url.length % 4)) % 4), '=');
|
|
283
|
+
const binary = atob(base64);
|
|
284
|
+
const bytes = new Uint8Array(binary.length);
|
|
285
|
+
for (let i = 0; i < binary.length; i++)
|
|
286
|
+
bytes[i] = binary.charCodeAt(i);
|
|
287
|
+
return bytes.buffer;
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=usePasskey.js.map
|