@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { StoredSession, StoredState } from "@atcute/oauth-node-client";
|
|
3
|
+
import { DbSessionStore, DbStateStore, type DbAdapter } from "./stores.js";
|
|
4
|
+
|
|
5
|
+
function createMockAdapter(): DbAdapter {
|
|
6
|
+
return {
|
|
7
|
+
findOne: vi.fn().mockResolvedValue(null),
|
|
8
|
+
create: vi.fn().mockResolvedValue({}),
|
|
9
|
+
update: vi.fn().mockResolvedValue({}),
|
|
10
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
deleteMany: vi.fn().mockResolvedValue(0),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test DID literal
|
|
16
|
+
const TEST_DID = "did:plc:abc123" as `did:${string}:${string}`;
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test fixture
|
|
19
|
+
const TEST_SESSION: StoredSession = {
|
|
20
|
+
dpopKey: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },
|
|
21
|
+
tokenSet: {
|
|
22
|
+
access_token: "access-token",
|
|
23
|
+
token_type: "DPoP",
|
|
24
|
+
expires_at: Date.now() + 60_000,
|
|
25
|
+
sub: TEST_DID,
|
|
26
|
+
iss: "https://bsky.social",
|
|
27
|
+
scope: "atproto transition:generic",
|
|
28
|
+
refresh_token: "refresh-token",
|
|
29
|
+
},
|
|
30
|
+
} as unknown as StoredSession;
|
|
31
|
+
|
|
32
|
+
describe("DbSessionStore", () => {
|
|
33
|
+
let adapter: DbAdapter;
|
|
34
|
+
let store: DbSessionStore;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
adapter = createMockAdapter();
|
|
38
|
+
store = new DbSessionStore(adapter);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("get", () => {
|
|
42
|
+
it("returns undefined when session does not exist", async () => {
|
|
43
|
+
const result = await store.get(TEST_DID);
|
|
44
|
+
expect(result).toBeUndefined();
|
|
45
|
+
expect(adapter.findOne).toHaveBeenCalledWith({
|
|
46
|
+
model: "atprotoSession",
|
|
47
|
+
where: [{ field: "did", value: TEST_DID }],
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns parsed session when found", async () => {
|
|
52
|
+
vi.mocked(adapter.findOne).mockResolvedValueOnce({
|
|
53
|
+
sessionData: JSON.stringify(TEST_SESSION),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await store.get(TEST_DID);
|
|
57
|
+
expect(result).toEqual(TEST_SESSION);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("set", () => {
|
|
62
|
+
it("creates a new session when none exists", async () => {
|
|
63
|
+
vi.mocked(adapter.findOne).mockResolvedValueOnce(null);
|
|
64
|
+
await store.set(TEST_DID, TEST_SESSION);
|
|
65
|
+
|
|
66
|
+
expect(adapter.create).toHaveBeenCalledWith({
|
|
67
|
+
model: "atprotoSession",
|
|
68
|
+
data: expect.objectContaining({
|
|
69
|
+
did: TEST_DID,
|
|
70
|
+
sessionData: JSON.stringify(TEST_SESSION),
|
|
71
|
+
userId: "",
|
|
72
|
+
handle: "",
|
|
73
|
+
pdsUrl: "",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("updates an existing session", async () => {
|
|
79
|
+
vi.mocked(adapter.findOne).mockResolvedValueOnce({ id: "existing-id" });
|
|
80
|
+
await store.set(TEST_DID, TEST_SESSION);
|
|
81
|
+
|
|
82
|
+
expect(adapter.update).toHaveBeenCalledWith({
|
|
83
|
+
model: "atprotoSession",
|
|
84
|
+
where: [{ field: "did", value: TEST_DID }],
|
|
85
|
+
update: expect.objectContaining({
|
|
86
|
+
sessionData: JSON.stringify(TEST_SESSION),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
expect(adapter.create).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("delete", () => {
|
|
94
|
+
it("deletes the session by DID", async () => {
|
|
95
|
+
await store.delete(TEST_DID);
|
|
96
|
+
expect(adapter.delete).toHaveBeenCalledWith({
|
|
97
|
+
model: "atprotoSession",
|
|
98
|
+
where: [{ field: "did", value: TEST_DID }],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("clear", () => {
|
|
104
|
+
it("deletes all sessions", async () => {
|
|
105
|
+
await store.clear();
|
|
106
|
+
expect(adapter.deleteMany).toHaveBeenCalledWith({
|
|
107
|
+
model: "atprotoSession",
|
|
108
|
+
where: [{ field: "did", value: { operator: "ne", value: "" } }],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("DbStateStore", () => {
|
|
115
|
+
let adapter: DbAdapter;
|
|
116
|
+
let store: DbStateStore;
|
|
117
|
+
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test fixture
|
|
119
|
+
const TEST_STATE: StoredState = {
|
|
120
|
+
dpopKey: { kty: "EC", crv: "P-256", x: "x", y: "y", d: "d" },
|
|
121
|
+
expiresAt: Date.now() + 60_000,
|
|
122
|
+
verifier: "test-verifier",
|
|
123
|
+
issuer: "https://bsky.social",
|
|
124
|
+
} as unknown as StoredState;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
adapter = createMockAdapter();
|
|
128
|
+
store = new DbStateStore(adapter);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("get", () => {
|
|
132
|
+
it("returns undefined when state does not exist", async () => {
|
|
133
|
+
const result = await store.get("test-key");
|
|
134
|
+
expect(result).toBeUndefined();
|
|
135
|
+
expect(adapter.findOne).toHaveBeenCalledWith({
|
|
136
|
+
model: "atprotoState",
|
|
137
|
+
where: [{ field: "stateKey", value: "test-key" }],
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns parsed state when found and not expired", async () => {
|
|
142
|
+
const futureState = { ...TEST_STATE, expiresAt: Date.now() + 60_000 };
|
|
143
|
+
vi.mocked(adapter.findOne).mockResolvedValueOnce({
|
|
144
|
+
stateData: JSON.stringify(futureState),
|
|
145
|
+
expiresAt: futureState.expiresAt,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await store.get("test-key");
|
|
149
|
+
expect(result).toEqual(futureState);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns undefined and cleans up when state is expired", async () => {
|
|
153
|
+
const expiredState = { ...TEST_STATE, expiresAt: Date.now() - 1000 };
|
|
154
|
+
vi.mocked(adapter.findOne).mockResolvedValueOnce({
|
|
155
|
+
stateData: JSON.stringify(expiredState),
|
|
156
|
+
expiresAt: expiredState.expiresAt,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const result = await store.get("test-key");
|
|
160
|
+
expect(result).toBeUndefined();
|
|
161
|
+
expect(adapter.delete).toHaveBeenCalledWith({
|
|
162
|
+
model: "atprotoState",
|
|
163
|
+
where: [{ field: "stateKey", value: "test-key" }],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("set", () => {
|
|
169
|
+
it("creates a new state entry", async () => {
|
|
170
|
+
await store.set("test-key", TEST_STATE);
|
|
171
|
+
expect(adapter.create).toHaveBeenCalledWith({
|
|
172
|
+
model: "atprotoState",
|
|
173
|
+
data: {
|
|
174
|
+
stateKey: "test-key",
|
|
175
|
+
stateData: JSON.stringify(TEST_STATE),
|
|
176
|
+
expiresAt: TEST_STATE.expiresAt,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("delete", () => {
|
|
183
|
+
it("deletes state by key", async () => {
|
|
184
|
+
await store.delete("test-key");
|
|
185
|
+
expect(adapter.delete).toHaveBeenCalledWith({
|
|
186
|
+
model: "atprotoState",
|
|
187
|
+
where: [{ field: "stateKey", value: "test-key" }],
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("clear", () => {
|
|
193
|
+
it("deletes all state entries", async () => {
|
|
194
|
+
await store.clear();
|
|
195
|
+
expect(adapter.deleteMany).toHaveBeenCalledWith({
|
|
196
|
+
model: "atprotoState",
|
|
197
|
+
where: [{ field: "stateKey", value: { operator: "ne", value: "" } }],
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
package/src/stores.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Did } from "@atcute/lexicons";
|
|
2
|
+
import type {
|
|
3
|
+
SessionStore,
|
|
4
|
+
StateStore,
|
|
5
|
+
StoredSession,
|
|
6
|
+
StoredState,
|
|
7
|
+
} from "@atcute/oauth-node-client";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A generic better-auth adapter interface matching the subset we need.
|
|
11
|
+
* This avoids importing better-auth's full type tree.
|
|
12
|
+
*/
|
|
13
|
+
export interface DbAdapter {
|
|
14
|
+
findOne: <T>(data: {
|
|
15
|
+
model: string;
|
|
16
|
+
where: { field: string; value: unknown }[];
|
|
17
|
+
}) => Promise<T | null>;
|
|
18
|
+
create: <T>(data: {
|
|
19
|
+
model: string;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}) => Promise<T>;
|
|
22
|
+
update: <T>(data: {
|
|
23
|
+
model: string;
|
|
24
|
+
where: { field: string; value: unknown }[];
|
|
25
|
+
update: Record<string, unknown>;
|
|
26
|
+
}) => Promise<T | null>;
|
|
27
|
+
delete: (data: {
|
|
28
|
+
model: string;
|
|
29
|
+
where: { field: string; value: unknown }[];
|
|
30
|
+
}) => Promise<void>;
|
|
31
|
+
deleteMany: (data: {
|
|
32
|
+
model: string;
|
|
33
|
+
where: { field: string; value: unknown }[];
|
|
34
|
+
}) => Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Database-backed session store for @atcute/oauth-node-client. */
|
|
38
|
+
export class DbSessionStore implements SessionStore {
|
|
39
|
+
constructor(private adapter: DbAdapter) {}
|
|
40
|
+
|
|
41
|
+
async get(did: Did): Promise<StoredSession | undefined> {
|
|
42
|
+
const row = await this.adapter.findOne<{
|
|
43
|
+
sessionData: string;
|
|
44
|
+
}>({
|
|
45
|
+
model: "atprotoSession",
|
|
46
|
+
where: [{ field: "did", value: did }],
|
|
47
|
+
});
|
|
48
|
+
if (!row) return undefined;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DB stores serialised JSON we control
|
|
50
|
+
return JSON.parse(row.sessionData) as StoredSession;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async set(did: Did, session: StoredSession): Promise<void> {
|
|
54
|
+
const data = JSON.stringify(session);
|
|
55
|
+
const existing = await this.adapter.findOne<{ id: string }>({
|
|
56
|
+
model: "atprotoSession",
|
|
57
|
+
where: [{ field: "did", value: did }],
|
|
58
|
+
});
|
|
59
|
+
if (existing) {
|
|
60
|
+
await this.adapter.update({
|
|
61
|
+
model: "atprotoSession",
|
|
62
|
+
where: [{ field: "did", value: did }],
|
|
63
|
+
update: { sessionData: data, updatedAt: new Date() },
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
await this.adapter.create({
|
|
67
|
+
model: "atprotoSession",
|
|
68
|
+
data: {
|
|
69
|
+
did,
|
|
70
|
+
sessionData: data,
|
|
71
|
+
userId: "",
|
|
72
|
+
handle: "",
|
|
73
|
+
pdsUrl: "",
|
|
74
|
+
updatedAt: new Date(),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(did: Did): Promise<void> {
|
|
81
|
+
await this.adapter.delete({
|
|
82
|
+
model: "atprotoSession",
|
|
83
|
+
where: [{ field: "did", value: did }],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async clear(): Promise<void> {
|
|
88
|
+
// Delete all — use a wildcard-like approach by deleting where id exists
|
|
89
|
+
await this.adapter.deleteMany({
|
|
90
|
+
model: "atprotoSession",
|
|
91
|
+
where: [{ field: "did", value: { operator: "ne", value: "" } }],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Database-backed state store for @atcute/oauth-node-client. */
|
|
97
|
+
export class DbStateStore implements StateStore {
|
|
98
|
+
constructor(private adapter: DbAdapter) {}
|
|
99
|
+
|
|
100
|
+
async get(stateKey: string): Promise<StoredState | undefined> {
|
|
101
|
+
const row = await this.adapter.findOne<{
|
|
102
|
+
stateData: string;
|
|
103
|
+
expiresAt: number;
|
|
104
|
+
}>({
|
|
105
|
+
model: "atprotoState",
|
|
106
|
+
where: [{ field: "stateKey", value: stateKey }],
|
|
107
|
+
});
|
|
108
|
+
if (!row) return undefined;
|
|
109
|
+
if (row.expiresAt < Date.now()) {
|
|
110
|
+
// Expired — clean it up
|
|
111
|
+
await this.delete(stateKey);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DB stores serialised JSON we control
|
|
115
|
+
return JSON.parse(row.stateData) as StoredState;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async set(stateKey: string, state: StoredState): Promise<void> {
|
|
119
|
+
const data = JSON.stringify(state);
|
|
120
|
+
await this.adapter.create({
|
|
121
|
+
model: "atprotoState",
|
|
122
|
+
data: {
|
|
123
|
+
stateKey,
|
|
124
|
+
stateData: data,
|
|
125
|
+
expiresAt: state.expiresAt,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async delete(stateKey: string): Promise<void> {
|
|
131
|
+
await this.adapter.delete({
|
|
132
|
+
model: "atprotoState",
|
|
133
|
+
where: [{ field: "stateKey", value: stateKey }],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async clear(): Promise<void> {
|
|
138
|
+
await this.adapter.deleteMany({
|
|
139
|
+
model: "atprotoState",
|
|
140
|
+
where: [{ field: "stateKey", value: { operator: "ne", value: "" } }],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { atprotoSchema } from "./types.js";
|
|
3
|
+
|
|
4
|
+
describe("atprotoSchema", () => {
|
|
5
|
+
it("defines the atprotoSession table", () => {
|
|
6
|
+
expect(atprotoSchema).toHaveProperty("atprotoSession");
|
|
7
|
+
expect(atprotoSchema.atprotoSession).toHaveProperty("fields");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("defines the atprotoState table", () => {
|
|
11
|
+
expect(atprotoSchema).toHaveProperty("atprotoState");
|
|
12
|
+
expect(atprotoSchema.atprotoState).toHaveProperty("fields");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("atprotoSession fields", () => {
|
|
16
|
+
const fields = atprotoSchema.atprotoSession.fields;
|
|
17
|
+
|
|
18
|
+
it("has a unique, required 'did' field of type string", () => {
|
|
19
|
+
expect(fields.did).toEqual({
|
|
20
|
+
type: "string",
|
|
21
|
+
unique: true,
|
|
22
|
+
required: true,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("has a required 'sessionData' field of type string", () => {
|
|
27
|
+
expect(fields.sessionData).toEqual({
|
|
28
|
+
type: "string",
|
|
29
|
+
required: true,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("has a required 'userId' field that references the user table", () => {
|
|
34
|
+
expect(fields.userId.type).toBe("string");
|
|
35
|
+
expect(fields.userId.required).toBe(true);
|
|
36
|
+
expect(fields.userId.references).toEqual({
|
|
37
|
+
model: "user",
|
|
38
|
+
field: "id",
|
|
39
|
+
onDelete: "cascade",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("has a required 'handle' field of type string", () => {
|
|
44
|
+
expect(fields.handle).toEqual({
|
|
45
|
+
type: "string",
|
|
46
|
+
required: true,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("has a required 'pdsUrl' field of type string", () => {
|
|
51
|
+
expect(fields.pdsUrl).toEqual({
|
|
52
|
+
type: "string",
|
|
53
|
+
required: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("has a required 'updatedAt' field of type date", () => {
|
|
58
|
+
expect(fields.updatedAt).toEqual({
|
|
59
|
+
type: "date",
|
|
60
|
+
required: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("atprotoState fields", () => {
|
|
66
|
+
const fields = atprotoSchema.atprotoState.fields;
|
|
67
|
+
|
|
68
|
+
it("has a unique, required 'stateKey' field of type string", () => {
|
|
69
|
+
expect(fields.stateKey).toEqual({
|
|
70
|
+
type: "string",
|
|
71
|
+
unique: true,
|
|
72
|
+
required: true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("has a required 'stateData' field of type string", () => {
|
|
77
|
+
expect(fields.stateData).toEqual({
|
|
78
|
+
type: "string",
|
|
79
|
+
required: true,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("has a required 'expiresAt' field of type number", () => {
|
|
84
|
+
expect(fields.expiresAt).toEqual({
|
|
85
|
+
type: "number",
|
|
86
|
+
required: true,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ClientAssertionPrivateJwk } from "@atcute/oauth-node-client";
|
|
2
|
+
|
|
3
|
+
/** Profile data fetched from the ATProto network after OAuth sign-in. */
|
|
4
|
+
export type AtprotoProfile = {
|
|
5
|
+
/** The user's permanent decentralized identifier (e.g. "did:plc:abc123"). */
|
|
6
|
+
did: string;
|
|
7
|
+
/** The user's current handle (e.g. "user.bsky.social"). */
|
|
8
|
+
handle: string;
|
|
9
|
+
/** Display name, if set. */
|
|
10
|
+
displayName?: string;
|
|
11
|
+
/** Avatar image URL, if set. */
|
|
12
|
+
avatar?: string;
|
|
13
|
+
/** Banner image URL, if set. */
|
|
14
|
+
banner?: string;
|
|
15
|
+
/** Bio / description text, if set. */
|
|
16
|
+
description?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Configuration options for the ATProto OAuth plugin. */
|
|
20
|
+
export type AtprotoPluginOptions = {
|
|
21
|
+
/** Display name shown to users during OAuth authorization. */
|
|
22
|
+
clientName: string;
|
|
23
|
+
/** Homepage URL for the client application. */
|
|
24
|
+
clientUri?: string;
|
|
25
|
+
/** Logo URL shown during authorization. */
|
|
26
|
+
logoUri?: string;
|
|
27
|
+
/** Terms of service URL. */
|
|
28
|
+
tosUri?: string;
|
|
29
|
+
/** Privacy policy URL. */
|
|
30
|
+
policyUri?: string;
|
|
31
|
+
/**
|
|
32
|
+
* OAuth scopes to request. Defaults to "atproto" (identity-only).
|
|
33
|
+
* Accepts a single space-separated string or an array of scope strings
|
|
34
|
+
* (e.g. from @atcute/oauth-types scope builders like `scope.rpc(...)`, `scope.repo(...)`).
|
|
35
|
+
* The base "atproto" scope is always included automatically.
|
|
36
|
+
*/
|
|
37
|
+
scope?: string | string[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Private JWKs for confidential client mode (private_key_jwt auth).
|
|
41
|
+
* If omitted, the plugin runs as a public client (shorter token lifetime).
|
|
42
|
+
*/
|
|
43
|
+
keyset?: ClientAssertionPrivateJwk[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* When true, prevents new user creation via ATProto OAuth.
|
|
47
|
+
* Existing users can still sign in. Returns FORBIDDEN for unknown DIDs.
|
|
48
|
+
*/
|
|
49
|
+
disableSignUp?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Custom mapping from an ATProto profile to better-auth user fields.
|
|
53
|
+
* Called during sign-in/sign-up to populate user name, email, image, etc.
|
|
54
|
+
* If not provided, defaults to mapping displayName to name and avatar to image.
|
|
55
|
+
*/
|
|
56
|
+
mapProfileToUser?: (
|
|
57
|
+
profile: AtprotoProfile,
|
|
58
|
+
) => Partial<{ name: string; email: string; image: string }>;
|
|
59
|
+
|
|
60
|
+
/** Path for the OAuth client metadata document. Default: "/oauth-client-metadata.json" */
|
|
61
|
+
clientMetadataPath?: string;
|
|
62
|
+
/** Path for the JWKS endpoint. Default: "/.well-known/jwks.json" */
|
|
63
|
+
jwksPath?: string;
|
|
64
|
+
/** Path for the OAuth callback. Default: "/atproto/callback" */
|
|
65
|
+
callbackPath?: string;
|
|
66
|
+
/** Path for the sign-in endpoint. Default: "/sign-in/atproto" */
|
|
67
|
+
signInPath?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Database schema field definitions for better-auth plugin schema. */
|
|
71
|
+
export const atprotoSchema = {
|
|
72
|
+
user: {
|
|
73
|
+
fields: {
|
|
74
|
+
atprotoDid: {
|
|
75
|
+
type: "string" as const,
|
|
76
|
+
unique: true,
|
|
77
|
+
required: false,
|
|
78
|
+
returned: true,
|
|
79
|
+
input: false,
|
|
80
|
+
},
|
|
81
|
+
atprotoHandle: {
|
|
82
|
+
type: "string" as const,
|
|
83
|
+
required: false,
|
|
84
|
+
returned: true,
|
|
85
|
+
input: false,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
atprotoSession: {
|
|
90
|
+
fields: {
|
|
91
|
+
did: { type: "string" as const, unique: true, required: true },
|
|
92
|
+
sessionData: { type: "string" as const, required: true },
|
|
93
|
+
userId: {
|
|
94
|
+
type: "string" as const,
|
|
95
|
+
required: true,
|
|
96
|
+
references: {
|
|
97
|
+
model: "user",
|
|
98
|
+
field: "id",
|
|
99
|
+
onDelete: "cascade" as const,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
handle: { type: "string" as const, required: true },
|
|
103
|
+
pdsUrl: { type: "string" as const, required: true },
|
|
104
|
+
updatedAt: { type: "date" as const, required: true },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
atprotoState: {
|
|
108
|
+
fields: {
|
|
109
|
+
stateKey: { type: "string" as const, unique: true, required: true },
|
|
110
|
+
stateData: { type: "string" as const, required: true },
|
|
111
|
+
expiresAt: { type: "number" as const, required: true },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
} as const;
|