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 +92 -0
- package/dist/index.d.mts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +263 -0
- package/dist/index.mjs +235 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# 📦 PocketBase Passkey SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pocketbase-passkey)
|
|
4
|
+
[](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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|