pocketbase-passkey 1.0.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/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # 📦 PocketBase Passkey SDK
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/pocketbase-passkey.svg)](https://www.npmjs.com/package/pocketbase-passkey)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A lightweight, TypeScript-first SDK to integrate **Passkey (WebAuthn)** authentication with **PocketBase**.
7
+
8
+ ## Features
9
+
10
+ - **🚀 One-Click Registration & Login**: High-level methods to skip the "Begin/Finish" boilerplate.
11
+ - **🔐 PocketBase Native**: Seamlessly integrates with the official PocketBase SDK to handle `authStore` automatically.
12
+ - **🛠️ Type-Safe**: Written in TypeScript with full JSDoc support for IntelliSense.
13
+ - **⚠️ Custom Error Classes**: Easily handle user cancellations and verification failures.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install pocketbase-passkey
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ### Initialize the SDK
24
+
25
+ ```typescript
26
+ import { PocketBasePasskey } from "pocketbase-passkey";
27
+ import PocketBase from "pocketbase";
28
+
29
+ // 1. Initialize official PocketBase SDK
30
+ const pb = new PocketBase("http://localhost:8090");
31
+
32
+ // 2. Initialize Passkey SDK (passing the pb instance)
33
+ const sdk = new PocketBasePasskey({ pb });
34
+ ```
35
+
36
+ ### One-Click Register
37
+
38
+ Registers a new passkey for the current user ID.
39
+
40
+ ```typescript
41
+ try {
42
+ const result = await sdk.register("user_record_id");
43
+ console.log("Passkey registered!");
44
+ } catch (err) {
45
+ console.error("Registration failed", err);
46
+ }
47
+ ```
48
+
49
+ ### One-Click Login
50
+
51
+ Authenticates the user and **automatically** populates `pb.authStore`.
52
+
53
+ ```typescript
54
+ try {
55
+ const result = await sdk.login("user_record_id");
56
+ // pb.authStore.token is now saved and valid!
57
+ console.log("Authenticated as:", pb.authStore.model.email);
58
+ } catch (err) {
59
+ if (err.name === "PasskeyCancelledError") {
60
+ console.log("User closed the biometric dialog");
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Advanced Error Handling
66
+
67
+ The SDK provides specific error classes for robust UX:
68
+
69
+ ```typescript
70
+ import {
71
+ PasskeyCancelledError,
72
+ PasskeyVerificationError,
73
+ } from "pocketbase-passkey";
74
+
75
+ try {
76
+ await sdk.login(userId);
77
+ } catch (err) {
78
+ if (err instanceof PasskeyCancelledError) {
79
+ // User clicked "Cancel" or closed the Face ID prompt
80
+ } else if (err instanceof PasskeyVerificationError) {
81
+ // Server rejected the passkey signature
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## Support & Integration
87
+
88
+ This SDK is designed to work with the [PocketBase Passkey Server](https://github.com/kaiseo/pocketbase-passkey).
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Base error class for all Passkey-related operations.
3
+ */
4
+ declare class PasskeyError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ /**
8
+ * Thrown when a user explicitly cancels the Face ID / Touch ID / Security Key prompt.
9
+ */
10
+ declare class PasskeyCancelledError extends PasskeyError {
11
+ constructor();
12
+ }
13
+ /**
14
+ * Thrown when the server fails to verify the passkey response.
15
+ */
16
+ declare class PasskeyVerificationError extends PasskeyError {
17
+ constructor(message: string);
18
+ }
19
+ /**
20
+ * Configuration options for the PocketBasePasskey SDK.
21
+ */
22
+ interface PasskeyOptions {
23
+ /** The base URL of your PocketBase server (e.g., 'http://localhost:8090'). */
24
+ apiUrl: string;
25
+ /**
26
+ * Optional: An instance of the official PocketBase JavaScript SDK.
27
+ * If provided, the SDK will automatically authenticate this instance on successful login.
28
+ */
29
+ pb?: any;
30
+ }
31
+ /**
32
+ * The main SDK class for handling Passkey registration and authentication with PocketBase.
33
+ */
34
+ declare class PocketBasePasskey {
35
+ private apiUrl;
36
+ private pb?;
37
+ /**
38
+ * Initializes the Passkey SDK.
39
+ * @param options - A configuration object or just the API URL string.
40
+ */
41
+ constructor(options: PasskeyOptions | string);
42
+ /**
43
+ * Completes the entire Passkey registration flow in a single call.
44
+ * This will trigger the browser's biometric/security key prompt.
45
+ *
46
+ * @param userId - The ID (Record ID) of the user to register the passkey for.
47
+ * @returns A promise that resolves to the registration result from the server.
48
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
49
+ * @throws {PasskeyVerificationError} If the server-side verification fails.
50
+ */
51
+ register(userId: string): Promise<any>;
52
+ /**
53
+ * Completes the entire Passkey login flow in a single call.
54
+ * If a PocketBase instance was provided in the constructor, it will be automatically authenticated.
55
+ *
56
+ * @param userId - The ID (Record ID) of the user to authenticate.
57
+ * @returns A promise that resolves to the authentication result (contains token and record).
58
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
59
+ * @throws {PasskeyVerificationError} If the credentials are invalid or verification fails.
60
+ */
61
+ login(userId: string): Promise<any>;
62
+ /**
63
+ * Fetch registration options from the server.
64
+ * @internal
65
+ */
66
+ private registerBegin;
67
+ /**
68
+ * Create the credential locally and send verification data to the server.
69
+ * @internal
70
+ */
71
+ private registerFinish;
72
+ /**
73
+ * Fetch login options from the server.
74
+ * @internal
75
+ */
76
+ private loginBegin;
77
+ /**
78
+ * Handle biometric prompt for authentication and verify response with the server.
79
+ * @internal
80
+ */
81
+ private loginFinish;
82
+ /**
83
+ * Decodes a base64url string to an ArrayBuffer.
84
+ * @private
85
+ */
86
+ private base64urlToBuffer;
87
+ /**
88
+ * Encodes a Uint8Array to a base64url string.
89
+ * @private
90
+ */
91
+ private bufferToBase64url;
92
+ }
93
+
94
+ export { PasskeyCancelledError, PasskeyError, type PasskeyOptions, PasskeyVerificationError, PocketBasePasskey };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Base error class for all Passkey-related operations.
3
+ */
4
+ declare class PasskeyError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ /**
8
+ * Thrown when a user explicitly cancels the Face ID / Touch ID / Security Key prompt.
9
+ */
10
+ declare class PasskeyCancelledError extends PasskeyError {
11
+ constructor();
12
+ }
13
+ /**
14
+ * Thrown when the server fails to verify the passkey response.
15
+ */
16
+ declare class PasskeyVerificationError extends PasskeyError {
17
+ constructor(message: string);
18
+ }
19
+ /**
20
+ * Configuration options for the PocketBasePasskey SDK.
21
+ */
22
+ interface PasskeyOptions {
23
+ /** The base URL of your PocketBase server (e.g., 'http://localhost:8090'). */
24
+ apiUrl: string;
25
+ /**
26
+ * Optional: An instance of the official PocketBase JavaScript SDK.
27
+ * If provided, the SDK will automatically authenticate this instance on successful login.
28
+ */
29
+ pb?: any;
30
+ }
31
+ /**
32
+ * The main SDK class for handling Passkey registration and authentication with PocketBase.
33
+ */
34
+ declare class PocketBasePasskey {
35
+ private apiUrl;
36
+ private pb?;
37
+ /**
38
+ * Initializes the Passkey SDK.
39
+ * @param options - A configuration object or just the API URL string.
40
+ */
41
+ constructor(options: PasskeyOptions | string);
42
+ /**
43
+ * Completes the entire Passkey registration flow in a single call.
44
+ * This will trigger the browser's biometric/security key prompt.
45
+ *
46
+ * @param userId - The ID (Record ID) of the user to register the passkey for.
47
+ * @returns A promise that resolves to the registration result from the server.
48
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
49
+ * @throws {PasskeyVerificationError} If the server-side verification fails.
50
+ */
51
+ register(userId: string): Promise<any>;
52
+ /**
53
+ * Completes the entire Passkey login flow in a single call.
54
+ * If a PocketBase instance was provided in the constructor, it will be automatically authenticated.
55
+ *
56
+ * @param userId - The ID (Record ID) of the user to authenticate.
57
+ * @returns A promise that resolves to the authentication result (contains token and record).
58
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
59
+ * @throws {PasskeyVerificationError} If the credentials are invalid or verification fails.
60
+ */
61
+ login(userId: string): Promise<any>;
62
+ /**
63
+ * Fetch registration options from the server.
64
+ * @internal
65
+ */
66
+ private registerBegin;
67
+ /**
68
+ * Create the credential locally and send verification data to the server.
69
+ * @internal
70
+ */
71
+ private registerFinish;
72
+ /**
73
+ * Fetch login options from the server.
74
+ * @internal
75
+ */
76
+ private loginBegin;
77
+ /**
78
+ * Handle biometric prompt for authentication and verify response with the server.
79
+ * @internal
80
+ */
81
+ private loginFinish;
82
+ /**
83
+ * Decodes a base64url string to an ArrayBuffer.
84
+ * @private
85
+ */
86
+ private base64urlToBuffer;
87
+ /**
88
+ * Encodes a Uint8Array to a base64url string.
89
+ * @private
90
+ */
91
+ private bufferToBase64url;
92
+ }
93
+
94
+ export { PasskeyCancelledError, PasskeyError, type PasskeyOptions, PasskeyVerificationError, PocketBasePasskey };
package/dist/index.js ADDED
@@ -0,0 +1,263 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PasskeyCancelledError: () => PasskeyCancelledError,
24
+ PasskeyError: () => PasskeyError,
25
+ PasskeyVerificationError: () => PasskeyVerificationError,
26
+ PocketBasePasskey: () => PocketBasePasskey
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var PasskeyError = class extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = "PasskeyError";
33
+ }
34
+ };
35
+ var PasskeyCancelledError = class extends PasskeyError {
36
+ constructor() {
37
+ super("Passkey operation was cancelled by the user");
38
+ this.name = "PasskeyCancelledError";
39
+ }
40
+ };
41
+ var PasskeyVerificationError = class extends PasskeyError {
42
+ constructor(message) {
43
+ super(message);
44
+ this.name = "PasskeyVerificationError";
45
+ }
46
+ };
47
+ var PocketBasePasskey = class {
48
+ apiUrl;
49
+ pb;
50
+ /**
51
+ * Initializes the Passkey SDK.
52
+ * @param options - A configuration object or just the API URL string.
53
+ */
54
+ constructor(options) {
55
+ if (typeof options === "string") {
56
+ this.apiUrl = options;
57
+ } else {
58
+ this.apiUrl = options.apiUrl || options.pb?.baseUrl;
59
+ this.pb = options.pb;
60
+ }
61
+ if (!this.apiUrl) {
62
+ throw new PasskeyError("API URL is required");
63
+ }
64
+ this.apiUrl = this.apiUrl.replace(/\/$/, "");
65
+ }
66
+ /**
67
+ * Completes the entire Passkey registration flow in a single call.
68
+ * This will trigger the browser's biometric/security key prompt.
69
+ *
70
+ * @param userId - The ID (Record ID) of the user to register the passkey for.
71
+ * @returns A promise that resolves to the registration result from the server.
72
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
73
+ * @throws {PasskeyVerificationError} If the server-side verification fails.
74
+ */
75
+ async register(userId) {
76
+ try {
77
+ const options = await this.registerBegin(userId);
78
+ return await this.registerFinish(userId, options);
79
+ } catch (err) {
80
+ if (err.name === "NotAllowedError" || err.message?.includes("cancelled")) {
81
+ throw new PasskeyCancelledError();
82
+ }
83
+ throw err;
84
+ }
85
+ }
86
+ /**
87
+ * Completes the entire Passkey login flow in a single call.
88
+ * If a PocketBase instance was provided in the constructor, it will be automatically authenticated.
89
+ *
90
+ * @param userId - The ID (Record ID) of the user to authenticate.
91
+ * @returns A promise that resolves to the authentication result (contains token and record).
92
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
93
+ * @throws {PasskeyVerificationError} If the credentials are invalid or verification fails.
94
+ */
95
+ async login(userId) {
96
+ try {
97
+ const options = await this.loginBegin(userId);
98
+ const result = await this.loginFinish(userId, options);
99
+ if (this.pb && result.token && result.record) {
100
+ this.pb.authStore.save(result.token, result.record);
101
+ }
102
+ return result;
103
+ } catch (err) {
104
+ if (err.name === "NotAllowedError" || err.message?.includes("cancelled")) {
105
+ throw new PasskeyCancelledError();
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+ // --- Private/Lower-level methods ---
111
+ /**
112
+ * Fetch registration options from the server.
113
+ * @internal
114
+ */
115
+ async registerBegin(userId) {
116
+ const res = await fetch(`${this.apiUrl}/api/passkey/register/begin`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({ userId })
120
+ });
121
+ const data = await res.json();
122
+ if (!res.ok) throw new PasskeyError(data.error || "Failed to begin registration");
123
+ return data;
124
+ }
125
+ /**
126
+ * Create the credential locally and send verification data to the server.
127
+ * @internal
128
+ */
129
+ async registerFinish(userId, data) {
130
+ const options = data.publicKey || data;
131
+ const creationOptions = {
132
+ ...options,
133
+ challenge: this.base64urlToBuffer(options.challenge),
134
+ user: {
135
+ ...options.user,
136
+ id: this.base64urlToBuffer(options.user?.id)
137
+ }
138
+ };
139
+ if (creationOptions.excludeCredentials) {
140
+ creationOptions.excludeCredentials = creationOptions.excludeCredentials.map((cred) => ({
141
+ ...cred,
142
+ id: this.base64urlToBuffer(cred.id)
143
+ }));
144
+ }
145
+ const credential = await navigator.credentials.create({
146
+ publicKey: creationOptions
147
+ });
148
+ if (!credential) throw new PasskeyCancelledError();
149
+ const response = credential.response;
150
+ const body = {
151
+ userId,
152
+ id: credential.id,
153
+ rawId: this.bufferToBase64url(new Uint8Array(credential.rawId)),
154
+ type: credential.type,
155
+ response: {
156
+ attestationObject: this.bufferToBase64url(new Uint8Array(response.attestationObject)),
157
+ clientDataJSON: this.bufferToBase64url(new Uint8Array(response.clientDataJSON))
158
+ }
159
+ };
160
+ const finishRes = await fetch(`${this.apiUrl}/api/passkey/register/finish`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify(body)
164
+ });
165
+ const finishData = await finishRes.json();
166
+ if (!finishRes.ok) throw new PasskeyVerificationError(finishData.error || "Registration failed");
167
+ return finishData;
168
+ }
169
+ /**
170
+ * Fetch login options from the server.
171
+ * @internal
172
+ */
173
+ async loginBegin(userId) {
174
+ const res = await fetch(`${this.apiUrl}/api/passkey/login/begin`, {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/json" },
177
+ body: JSON.stringify({ userId })
178
+ });
179
+ const data = await res.json();
180
+ if (!res.ok) throw new PasskeyError(data.error || "Failed to begin login");
181
+ return data;
182
+ }
183
+ /**
184
+ * Handle biometric prompt for authentication and verify response with the server.
185
+ * @internal
186
+ */
187
+ async loginFinish(userId, data) {
188
+ const options = data.publicKey || data;
189
+ const requestOptions = {
190
+ ...options,
191
+ challenge: this.base64urlToBuffer(options.challenge)
192
+ };
193
+ if (requestOptions.allowCredentials) {
194
+ requestOptions.allowCredentials = requestOptions.allowCredentials.map((cred) => ({
195
+ ...cred,
196
+ id: this.base64urlToBuffer(cred.id)
197
+ }));
198
+ }
199
+ const assertion = await navigator.credentials.get({
200
+ publicKey: requestOptions
201
+ });
202
+ if (!assertion) throw new PasskeyCancelledError();
203
+ const response = assertion.response;
204
+ const body = {
205
+ userId,
206
+ id: assertion.id,
207
+ rawId: this.bufferToBase64url(new Uint8Array(assertion.rawId)),
208
+ type: assertion.type,
209
+ response: {
210
+ authenticatorData: this.bufferToBase64url(new Uint8Array(response.authenticatorData)),
211
+ clientDataJSON: this.bufferToBase64url(new Uint8Array(response.clientDataJSON)),
212
+ signature: this.bufferToBase64url(new Uint8Array(response.signature)),
213
+ userHandle: response.userHandle ? this.bufferToBase64url(new Uint8Array(response.userHandle)) : null
214
+ }
215
+ };
216
+ const finishRes = await fetch(`${this.apiUrl}/api/passkey/login/finish`, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify(body)
220
+ });
221
+ const finishData = await finishRes.json();
222
+ if (!finishRes.ok) throw new PasskeyVerificationError(finishData.error || "Login failed");
223
+ if (!finishData.token) {
224
+ throw new PasskeyVerificationError("No token received");
225
+ }
226
+ return finishData;
227
+ }
228
+ // --- Utils ---
229
+ /**
230
+ * Decodes a base64url string to an ArrayBuffer.
231
+ * @private
232
+ */
233
+ base64urlToBuffer(base64url) {
234
+ if (!base64url || typeof base64url !== "string") {
235
+ return new ArrayBuffer(0);
236
+ }
237
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
238
+ const padLen = (4 - base64.length % 4) % 4;
239
+ const paddedBase64 = base64 + "=".repeat(padLen);
240
+ const binary = atob(paddedBase64);
241
+ const bytes = new Uint8Array(binary.length);
242
+ for (let i = 0; i < binary.length; i++) {
243
+ bytes[i] = binary.charCodeAt(i);
244
+ }
245
+ return bytes.buffer;
246
+ }
247
+ /**
248
+ * Encodes a Uint8Array to a base64url string.
249
+ * @private
250
+ */
251
+ bufferToBase64url(buffer) {
252
+ const binary = String.fromCharCode(...buffer);
253
+ const base64 = btoa(binary);
254
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
255
+ }
256
+ };
257
+ // Annotate the CommonJS export names for ESM import in node:
258
+ 0 && (module.exports = {
259
+ PasskeyCancelledError,
260
+ PasskeyError,
261
+ PasskeyVerificationError,
262
+ PocketBasePasskey
263
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,235 @@
1
+ // src/index.ts
2
+ var PasskeyError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "PasskeyError";
6
+ }
7
+ };
8
+ var PasskeyCancelledError = class extends PasskeyError {
9
+ constructor() {
10
+ super("Passkey operation was cancelled by the user");
11
+ this.name = "PasskeyCancelledError";
12
+ }
13
+ };
14
+ var PasskeyVerificationError = class extends PasskeyError {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "PasskeyVerificationError";
18
+ }
19
+ };
20
+ var PocketBasePasskey = class {
21
+ apiUrl;
22
+ pb;
23
+ /**
24
+ * Initializes the Passkey SDK.
25
+ * @param options - A configuration object or just the API URL string.
26
+ */
27
+ constructor(options) {
28
+ if (typeof options === "string") {
29
+ this.apiUrl = options;
30
+ } else {
31
+ this.apiUrl = options.apiUrl || options.pb?.baseUrl;
32
+ this.pb = options.pb;
33
+ }
34
+ if (!this.apiUrl) {
35
+ throw new PasskeyError("API URL is required");
36
+ }
37
+ this.apiUrl = this.apiUrl.replace(/\/$/, "");
38
+ }
39
+ /**
40
+ * Completes the entire Passkey registration flow in a single call.
41
+ * This will trigger the browser's biometric/security key prompt.
42
+ *
43
+ * @param userId - The ID (Record ID) of the user to register the passkey for.
44
+ * @returns A promise that resolves to the registration result from the server.
45
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
46
+ * @throws {PasskeyVerificationError} If the server-side verification fails.
47
+ */
48
+ async register(userId) {
49
+ try {
50
+ const options = await this.registerBegin(userId);
51
+ return await this.registerFinish(userId, options);
52
+ } catch (err) {
53
+ if (err.name === "NotAllowedError" || err.message?.includes("cancelled")) {
54
+ throw new PasskeyCancelledError();
55
+ }
56
+ throw err;
57
+ }
58
+ }
59
+ /**
60
+ * Completes the entire Passkey login flow in a single call.
61
+ * If a PocketBase instance was provided in the constructor, it will be automatically authenticated.
62
+ *
63
+ * @param userId - The ID (Record ID) of the user to authenticate.
64
+ * @returns A promise that resolves to the authentication result (contains token and record).
65
+ * @throws {PasskeyCancelledError} If the user cancels the prompt.
66
+ * @throws {PasskeyVerificationError} If the credentials are invalid or verification fails.
67
+ */
68
+ async login(userId) {
69
+ try {
70
+ const options = await this.loginBegin(userId);
71
+ const result = await this.loginFinish(userId, options);
72
+ if (this.pb && result.token && result.record) {
73
+ this.pb.authStore.save(result.token, result.record);
74
+ }
75
+ return result;
76
+ } catch (err) {
77
+ if (err.name === "NotAllowedError" || err.message?.includes("cancelled")) {
78
+ throw new PasskeyCancelledError();
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+ // --- Private/Lower-level methods ---
84
+ /**
85
+ * Fetch registration options from the server.
86
+ * @internal
87
+ */
88
+ async registerBegin(userId) {
89
+ const res = await fetch(`${this.apiUrl}/api/passkey/register/begin`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ userId })
93
+ });
94
+ const data = await res.json();
95
+ if (!res.ok) throw new PasskeyError(data.error || "Failed to begin registration");
96
+ return data;
97
+ }
98
+ /**
99
+ * Create the credential locally and send verification data to the server.
100
+ * @internal
101
+ */
102
+ async registerFinish(userId, data) {
103
+ const options = data.publicKey || data;
104
+ const creationOptions = {
105
+ ...options,
106
+ challenge: this.base64urlToBuffer(options.challenge),
107
+ user: {
108
+ ...options.user,
109
+ id: this.base64urlToBuffer(options.user?.id)
110
+ }
111
+ };
112
+ if (creationOptions.excludeCredentials) {
113
+ creationOptions.excludeCredentials = creationOptions.excludeCredentials.map((cred) => ({
114
+ ...cred,
115
+ id: this.base64urlToBuffer(cred.id)
116
+ }));
117
+ }
118
+ const credential = await navigator.credentials.create({
119
+ publicKey: creationOptions
120
+ });
121
+ if (!credential) throw new PasskeyCancelledError();
122
+ const response = credential.response;
123
+ const body = {
124
+ userId,
125
+ id: credential.id,
126
+ rawId: this.bufferToBase64url(new Uint8Array(credential.rawId)),
127
+ type: credential.type,
128
+ response: {
129
+ attestationObject: this.bufferToBase64url(new Uint8Array(response.attestationObject)),
130
+ clientDataJSON: this.bufferToBase64url(new Uint8Array(response.clientDataJSON))
131
+ }
132
+ };
133
+ const finishRes = await fetch(`${this.apiUrl}/api/passkey/register/finish`, {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify(body)
137
+ });
138
+ const finishData = await finishRes.json();
139
+ if (!finishRes.ok) throw new PasskeyVerificationError(finishData.error || "Registration failed");
140
+ return finishData;
141
+ }
142
+ /**
143
+ * Fetch login options from the server.
144
+ * @internal
145
+ */
146
+ async loginBegin(userId) {
147
+ const res = await fetch(`${this.apiUrl}/api/passkey/login/begin`, {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({ userId })
151
+ });
152
+ const data = await res.json();
153
+ if (!res.ok) throw new PasskeyError(data.error || "Failed to begin login");
154
+ return data;
155
+ }
156
+ /**
157
+ * Handle biometric prompt for authentication and verify response with the server.
158
+ * @internal
159
+ */
160
+ async loginFinish(userId, data) {
161
+ const options = data.publicKey || data;
162
+ const requestOptions = {
163
+ ...options,
164
+ challenge: this.base64urlToBuffer(options.challenge)
165
+ };
166
+ if (requestOptions.allowCredentials) {
167
+ requestOptions.allowCredentials = requestOptions.allowCredentials.map((cred) => ({
168
+ ...cred,
169
+ id: this.base64urlToBuffer(cred.id)
170
+ }));
171
+ }
172
+ const assertion = await navigator.credentials.get({
173
+ publicKey: requestOptions
174
+ });
175
+ if (!assertion) throw new PasskeyCancelledError();
176
+ const response = assertion.response;
177
+ const body = {
178
+ userId,
179
+ id: assertion.id,
180
+ rawId: this.bufferToBase64url(new Uint8Array(assertion.rawId)),
181
+ type: assertion.type,
182
+ response: {
183
+ authenticatorData: this.bufferToBase64url(new Uint8Array(response.authenticatorData)),
184
+ clientDataJSON: this.bufferToBase64url(new Uint8Array(response.clientDataJSON)),
185
+ signature: this.bufferToBase64url(new Uint8Array(response.signature)),
186
+ userHandle: response.userHandle ? this.bufferToBase64url(new Uint8Array(response.userHandle)) : null
187
+ }
188
+ };
189
+ const finishRes = await fetch(`${this.apiUrl}/api/passkey/login/finish`, {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify(body)
193
+ });
194
+ const finishData = await finishRes.json();
195
+ if (!finishRes.ok) throw new PasskeyVerificationError(finishData.error || "Login failed");
196
+ if (!finishData.token) {
197
+ throw new PasskeyVerificationError("No token received");
198
+ }
199
+ return finishData;
200
+ }
201
+ // --- Utils ---
202
+ /**
203
+ * Decodes a base64url string to an ArrayBuffer.
204
+ * @private
205
+ */
206
+ base64urlToBuffer(base64url) {
207
+ if (!base64url || typeof base64url !== "string") {
208
+ return new ArrayBuffer(0);
209
+ }
210
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
211
+ const padLen = (4 - base64.length % 4) % 4;
212
+ const paddedBase64 = base64 + "=".repeat(padLen);
213
+ const binary = atob(paddedBase64);
214
+ const bytes = new Uint8Array(binary.length);
215
+ for (let i = 0; i < binary.length; i++) {
216
+ bytes[i] = binary.charCodeAt(i);
217
+ }
218
+ return bytes.buffer;
219
+ }
220
+ /**
221
+ * Encodes a Uint8Array to a base64url string.
222
+ * @private
223
+ */
224
+ bufferToBase64url(buffer) {
225
+ const binary = String.fromCharCode(...buffer);
226
+ const base64 = btoa(binary);
227
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
228
+ }
229
+ };
230
+ export {
231
+ PasskeyCancelledError,
232
+ PasskeyError,
233
+ PasskeyVerificationError,
234
+ PocketBasePasskey
235
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "pocketbase-passkey",
3
+ "version": "1.0.0",
4
+ "description": "PocketBase Passkey (WebAuthn) SDK",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean"
12
+ },
13
+ "keywords": [
14
+ "pocketbase",
15
+ "passkey",
16
+ "webauthn",
17
+ "auth"
18
+ ],
19
+ "author": "kaiseo",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "tsup": "^8.0.0",
23
+ "typescript": "^5.0.0"
24
+ }
25
+ }