@kingironman2011/better-auth-bsky 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +238 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +22 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/server-DO9pjTl1.d.ts +1860 -0
- package/dist/server-DO9pjTl1.d.ts.map +1 -0
- package/dist/server-DS4UMolW.js +951 -0
- package/dist/server-DS4UMolW.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/package.json +103 -0
- package/src/client.test.ts +137 -0
- package/src/client.ts +24 -0
- package/src/index.ts +10 -0
- package/src/key-utils.test.ts +26 -0
- package/src/key-utils.ts +32 -0
- package/src/server.test.ts +368 -0
- package/src/server.ts +831 -0
- package/src/stores.test.ts +201 -0
- package/src/stores.ts +143 -0
- package/src/types.test.ts +90 -0
- package/src/types.ts +114 -0
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kingironman2011/better-auth-bsky",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A better-auth plugin that adds ATProto/Bluesky OAuth 2.1 authentication (DPoP, PAR, PKCE) via @atcute/oauth-node-client.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"atproto",
|
|
7
|
+
"authentication",
|
|
8
|
+
"better-auth",
|
|
9
|
+
"bluesky",
|
|
10
|
+
"dpop",
|
|
11
|
+
"oauth",
|
|
12
|
+
"oauth2",
|
|
13
|
+
"par",
|
|
14
|
+
"pkce"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/KingIronMan2011/better-auth-bsky#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/KingIronMan2011/better-auth-bsky/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "KingIronMan2011",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/KingIronMan2011/better-auth-bsky.git"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE.md"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./client": {
|
|
42
|
+
"types": "./dist/client.d.ts",
|
|
43
|
+
"import": "./dist/client.js"
|
|
44
|
+
},
|
|
45
|
+
"./server": {
|
|
46
|
+
"types": "./dist/server.d.ts",
|
|
47
|
+
"import": "./dist/server.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsdown",
|
|
55
|
+
"dev": "tsdown --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"format": "prettier --write .",
|
|
59
|
+
"format:check": "prettier --check .",
|
|
60
|
+
"lint": "eslint .",
|
|
61
|
+
"lint:fix": "eslint . --fix",
|
|
62
|
+
"types": "tsc --noEmit",
|
|
63
|
+
"demo": "tsx watch demo/server.ts",
|
|
64
|
+
"demo:local": "LOCAL=1 tsx watch demo/server.ts",
|
|
65
|
+
"prepublishOnly": "pnpm run build",
|
|
66
|
+
"release": "pnpm run build && bumpp"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@atcute/identity-resolver": "^2.0.0",
|
|
70
|
+
"@atcute/lexicons": "^2.0.0",
|
|
71
|
+
"@atcute/oauth-node-client": "^2.0.0"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@atcute/client": "^5.1.0",
|
|
75
|
+
"@atcute/oauth-crypto": "^1.0.0",
|
|
76
|
+
"@eslint/js": "^10.0.1",
|
|
77
|
+
"@hono/node-server": "^2.0.5",
|
|
78
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
79
|
+
"@types/node": "^26.0.0",
|
|
80
|
+
"better-auth": "^1.6.11",
|
|
81
|
+
"better-sqlite3": "^12.0.0",
|
|
82
|
+
"bumpp": "11.1.0",
|
|
83
|
+
"eslint": "^10.5.0",
|
|
84
|
+
"globals": "^17.6.0",
|
|
85
|
+
"hono": "4.12.26",
|
|
86
|
+
"prettier": "^3.8.4",
|
|
87
|
+
"tsdown": "0.22.3",
|
|
88
|
+
"tsx": "^4.19.4",
|
|
89
|
+
"typescript": "^6.0.3",
|
|
90
|
+
"typescript-eslint": "^8.61.1",
|
|
91
|
+
"untun": "0.1.3",
|
|
92
|
+
"valibot": "^1.4.1",
|
|
93
|
+
"vite": "^8.0.14",
|
|
94
|
+
"vitest": "^4.1.7"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"better-auth": ">=1.5.0"
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=20"
|
|
101
|
+
},
|
|
102
|
+
"packageManager": "pnpm@11.0.0"
|
|
103
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { atprotoClient } from "./client.js";
|
|
3
|
+
|
|
4
|
+
describe("atprotoClient", () => {
|
|
5
|
+
it("returns a plugin with id 'atproto'", () => {
|
|
6
|
+
const plugin = atprotoClient();
|
|
7
|
+
expect(plugin.id).toBe("atproto");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("infers server plugin type", () => {
|
|
11
|
+
const plugin = atprotoClient();
|
|
12
|
+
expect(plugin).toHaveProperty("$InferServerPlugin");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("provides getActions function", () => {
|
|
16
|
+
const plugin = atprotoClient();
|
|
17
|
+
expect(typeof plugin.getActions).toBe("function");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("getActions", () => {
|
|
21
|
+
it("returns signIn.atproto action", () => {
|
|
22
|
+
const plugin = atprotoClient();
|
|
23
|
+
const mockFetch = vi.fn();
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
25
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
26
|
+
|
|
27
|
+
expect(actions.signIn).toBeDefined();
|
|
28
|
+
expect(typeof actions.signIn.atproto).toBe("function");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns atproto.getSession action", () => {
|
|
32
|
+
const plugin = atprotoClient();
|
|
33
|
+
const mockFetch = vi.fn();
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
35
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
36
|
+
|
|
37
|
+
expect(actions.atproto).toBeDefined();
|
|
38
|
+
expect(typeof actions.atproto.getSession).toBe("function");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns atproto.restore action", () => {
|
|
42
|
+
const plugin = atprotoClient();
|
|
43
|
+
const mockFetch = vi.fn();
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
45
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
46
|
+
|
|
47
|
+
expect(typeof actions.atproto.restore).toBe("function");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns atproto.signOut action", () => {
|
|
51
|
+
const plugin = atprotoClient();
|
|
52
|
+
const mockFetch = vi.fn();
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
54
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
55
|
+
|
|
56
|
+
expect(typeof actions.atproto.signOut).toBe("function");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("signIn.atproto calls $fetch with correct path and method", async () => {
|
|
60
|
+
const plugin = atprotoClient();
|
|
61
|
+
const mockFetch = vi
|
|
62
|
+
.fn()
|
|
63
|
+
.mockResolvedValue({ url: "https://bsky.social/auth" });
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
65
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
66
|
+
|
|
67
|
+
await actions.signIn.atproto({
|
|
68
|
+
handle: "user.bsky.social",
|
|
69
|
+
callbackURL: "/dashboard",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(mockFetch).toHaveBeenCalledWith("/sign-in/atproto", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: { handle: "user.bsky.social", callbackURL: "/dashboard" },
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("signIn.atproto works without callbackURL", async () => {
|
|
79
|
+
const plugin = atprotoClient();
|
|
80
|
+
const mockFetch = vi
|
|
81
|
+
.fn()
|
|
82
|
+
.mockResolvedValue({ url: "https://bsky.social/auth" });
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
84
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
85
|
+
|
|
86
|
+
await actions.signIn.atproto({ handle: "user.bsky.social" });
|
|
87
|
+
|
|
88
|
+
expect(mockFetch).toHaveBeenCalledWith("/sign-in/atproto", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: { handle: "user.bsky.social" },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("atproto.getSession calls $fetch with correct path and method", async () => {
|
|
95
|
+
const plugin = atprotoClient();
|
|
96
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
97
|
+
did: "did:plc:abc123",
|
|
98
|
+
handle: "user.bsky.social",
|
|
99
|
+
pdsUrl: "https://bsky.network",
|
|
100
|
+
});
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
102
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
103
|
+
|
|
104
|
+
await actions.atproto.getSession();
|
|
105
|
+
|
|
106
|
+
expect(mockFetch).toHaveBeenCalledWith("/atproto/session", {
|
|
107
|
+
method: "GET",
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("atproto.restore calls $fetch with correct path and method", async () => {
|
|
112
|
+
const plugin = atprotoClient();
|
|
113
|
+
const mockFetch = vi.fn().mockResolvedValue({ active: true });
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
115
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
116
|
+
|
|
117
|
+
await actions.atproto.restore();
|
|
118
|
+
|
|
119
|
+
expect(mockFetch).toHaveBeenCalledWith("/atproto/restore", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("atproto.signOut calls $fetch with correct path and method", async () => {
|
|
125
|
+
const plugin = atprotoClient();
|
|
126
|
+
const mockFetch = vi.fn().mockResolvedValue({ success: true });
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test mock
|
|
128
|
+
const actions = plugin.getActions(mockFetch as any);
|
|
129
|
+
|
|
130
|
+
await actions.atproto.signOut();
|
|
131
|
+
|
|
132
|
+
expect(mockFetch).toHaveBeenCalledWith("/atproto/sign-out", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BetterAuthClientPlugin } from "better-auth/client";
|
|
2
|
+
import type { atproto } from "./server.js";
|
|
3
|
+
|
|
4
|
+
export const atprotoClient = () =>
|
|
5
|
+
({
|
|
6
|
+
id: "atproto",
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required by better-auth plugin inference
|
|
8
|
+
$InferServerPlugin: {} as ReturnType<typeof atproto>,
|
|
9
|
+
getActions: ($fetch) => ({
|
|
10
|
+
signIn: {
|
|
11
|
+
atproto: async (data: { handle: string; callbackURL?: string }) => {
|
|
12
|
+
return $fetch("/sign-in/atproto", {
|
|
13
|
+
method: "POST",
|
|
14
|
+
body: data,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
atproto: {
|
|
19
|
+
getSession: async () => $fetch("/atproto/session", { method: "GET" }),
|
|
20
|
+
restore: async () => $fetch("/atproto/restore", { method: "POST" }),
|
|
21
|
+
signOut: async () => $fetch("/atproto/sign-out", { method: "POST" }),
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
}) satisfies BetterAuthClientPlugin;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
atproto,
|
|
3
|
+
fetchAtprotoProfilePublic,
|
|
4
|
+
atprotoPlaceholderEmail,
|
|
5
|
+
} from "./server.js";
|
|
6
|
+
export { atprotoClient } from "./client.js";
|
|
7
|
+
export { generateAtprotoKeypair, extractPublicJwk } from "./key-utils.js";
|
|
8
|
+
export { DbSessionStore, DbStateStore } from "./stores.js";
|
|
9
|
+
export type { AtprotoPluginOptions, AtprotoProfile } from "./types.js";
|
|
10
|
+
export { atprotoSchema } from "./types.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractPublicJwk, generateAtprotoKeypair } from "./key-utils.js";
|
|
3
|
+
|
|
4
|
+
describe("key-utils", () => {
|
|
5
|
+
it("generateAtprotoKeypair produces an ES256 private JWK", async () => {
|
|
6
|
+
const jwk = await generateAtprotoKeypair("test-kid");
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- inspect opaque JWK type in test
|
|
8
|
+
const raw = jwk as unknown as Record<string, unknown>;
|
|
9
|
+
expect(raw.kty).toBe("EC");
|
|
10
|
+
expect(raw.crv).toBe("P-256");
|
|
11
|
+
expect(raw.kid).toBe("test-kid");
|
|
12
|
+
expect(raw.alg).toBe("ES256");
|
|
13
|
+
expect(typeof raw.d).toBe("string");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("extractPublicJwk strips private key fields", async () => {
|
|
17
|
+
const priv = await generateAtprotoKeypair();
|
|
18
|
+
const pub = extractPublicJwk(priv);
|
|
19
|
+
expect(pub).not.toHaveProperty("d");
|
|
20
|
+
expect(pub).not.toHaveProperty("p");
|
|
21
|
+
expect(pub).not.toHaveProperty("q");
|
|
22
|
+
expect(pub).toHaveProperty("x");
|
|
23
|
+
expect(pub).toHaveProperty("y");
|
|
24
|
+
expect(pub).toHaveProperty("kid");
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/key-utils.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PublicJwk } from "@atcute/oauth-crypto";
|
|
2
|
+
import {
|
|
3
|
+
type ClientAssertionPrivateJwk,
|
|
4
|
+
generateClientAssertionKey,
|
|
5
|
+
} from "@atcute/oauth-node-client";
|
|
6
|
+
|
|
7
|
+
/** Private key fields to strip when extracting a public JWK. */
|
|
8
|
+
const PRIVATE_KEY_FIELDS = new Set(["d", "p", "q", "dp", "dq", "qi"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates an ES256 keypair for ATProto confidential client authentication.
|
|
12
|
+
* The returned JWK includes private key material and should be stored securely.
|
|
13
|
+
*/
|
|
14
|
+
export async function generateAtprotoKeypair(
|
|
15
|
+
kid?: string,
|
|
16
|
+
): Promise<ClientAssertionPrivateJwk> {
|
|
17
|
+
return generateClientAssertionKey(kid ?? "atproto-key", "ES256");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the public portion of a JWK by stripping private key fields.
|
|
22
|
+
* Safe to serve at the JWKS endpoint.
|
|
23
|
+
*/
|
|
24
|
+
export function extractPublicJwk(
|
|
25
|
+
privateJwk: ClientAssertionPrivateJwk,
|
|
26
|
+
): PublicJwk {
|
|
27
|
+
const entries = Object.entries(privateJwk).filter(
|
|
28
|
+
([key]) => !PRIVATE_KEY_FIELDS.has(key),
|
|
29
|
+
);
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Object.fromEntries loses type info
|
|
31
|
+
return Object.fromEntries(entries) as PublicJwk;
|
|
32
|
+
}
|