atproto-better-auth 0.1.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 +348 -0
- package/dist/client.d.ts +42 -0
- package/dist/client.js +52 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +534 -0
- package/dist/schema.d.ts +155 -0
- package/dist/server.d.ts +7 -0
- package/dist/types.d.ts +211 -0
- package/dist/utils.d.ts +88 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { NodeOAuthClient, JoseKey } from "@atproto/oauth-client-node";
|
|
3
|
+
import { Agent } from "@atproto/api";
|
|
4
|
+
import { createAuthEndpoint } from "better-auth/api";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// src/schema.ts
|
|
8
|
+
var atprotoUserSchema = {
|
|
9
|
+
user: {
|
|
10
|
+
fields: {
|
|
11
|
+
atprotoDid: {
|
|
12
|
+
type: "string",
|
|
13
|
+
required: false,
|
|
14
|
+
unique: true
|
|
15
|
+
},
|
|
16
|
+
atprotoHandle: {
|
|
17
|
+
type: "string",
|
|
18
|
+
required: false
|
|
19
|
+
},
|
|
20
|
+
atprotoBio: {
|
|
21
|
+
type: "string",
|
|
22
|
+
required: false
|
|
23
|
+
},
|
|
24
|
+
atprotoBanner: {
|
|
25
|
+
type: "string",
|
|
26
|
+
required: false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var atprotoStateSchema = {
|
|
32
|
+
atprotoState: {
|
|
33
|
+
fields: {
|
|
34
|
+
key: {
|
|
35
|
+
type: "string",
|
|
36
|
+
required: true,
|
|
37
|
+
unique: true
|
|
38
|
+
},
|
|
39
|
+
state: {
|
|
40
|
+
type: "string",
|
|
41
|
+
required: true
|
|
42
|
+
},
|
|
43
|
+
expiresAt: {
|
|
44
|
+
type: "date",
|
|
45
|
+
required: true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var atprotoSessionSchema = {
|
|
51
|
+
atprotoSession: {
|
|
52
|
+
fields: {
|
|
53
|
+
did: {
|
|
54
|
+
type: "string",
|
|
55
|
+
required: true,
|
|
56
|
+
unique: true
|
|
57
|
+
},
|
|
58
|
+
session: {
|
|
59
|
+
type: "string",
|
|
60
|
+
required: true
|
|
61
|
+
},
|
|
62
|
+
userId: {
|
|
63
|
+
type: "string",
|
|
64
|
+
required: true,
|
|
65
|
+
references: {
|
|
66
|
+
model: "user",
|
|
67
|
+
field: "id"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
updatedAt: {
|
|
71
|
+
type: "date",
|
|
72
|
+
required: true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var atprotoSchema = {
|
|
78
|
+
...atprotoUserSchema,
|
|
79
|
+
...atprotoStateSchema,
|
|
80
|
+
...atprotoSessionSchema
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/server.ts
|
|
84
|
+
function getAppOrigin(authBaseURL) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(authBaseURL).origin;
|
|
87
|
+
} catch {
|
|
88
|
+
return authBaseURL;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function atprotoAuth(options) {
|
|
92
|
+
const {
|
|
93
|
+
clientMetadata,
|
|
94
|
+
privateKey,
|
|
95
|
+
mapProfileToUser
|
|
96
|
+
} = options;
|
|
97
|
+
let oauthClient = null;
|
|
98
|
+
let keysetPromise = null;
|
|
99
|
+
let resolvedClientMetadata = null;
|
|
100
|
+
function buildClientMetadata(authBaseURL) {
|
|
101
|
+
if (resolvedClientMetadata)
|
|
102
|
+
return resolvedClientMetadata;
|
|
103
|
+
const appOrigin = getAppOrigin(authBaseURL);
|
|
104
|
+
resolvedClientMetadata = {
|
|
105
|
+
client_id: clientMetadata.clientId ?? `${appOrigin}/client-metadata.json`,
|
|
106
|
+
client_name: clientMetadata.clientName,
|
|
107
|
+
client_uri: clientMetadata.clientUri ?? appOrigin,
|
|
108
|
+
logo_uri: clientMetadata.logoUri,
|
|
109
|
+
tos_uri: clientMetadata.tosUri,
|
|
110
|
+
policy_uri: clientMetadata.policyUri,
|
|
111
|
+
redirect_uris: clientMetadata.redirectUris ?? [`${authBaseURL}/callback/atproto`],
|
|
112
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
113
|
+
response_types: ["code"],
|
|
114
|
+
scope: clientMetadata.scope ?? "atproto transition:generic",
|
|
115
|
+
dpop_bound_access_tokens: true,
|
|
116
|
+
application_type: "web",
|
|
117
|
+
token_endpoint_auth_method: clientMetadata.jwksUri ?? `${appOrigin}/jwks.json` ? "private_key_jwt" : "none",
|
|
118
|
+
token_endpoint_auth_signing_alg: clientMetadata.jwksUri ?? `${appOrigin}/jwks.json` ? "ES256" : undefined,
|
|
119
|
+
jwks_uri: clientMetadata.jwksUri ?? `${appOrigin}/jwks.json`
|
|
120
|
+
};
|
|
121
|
+
return resolvedClientMetadata;
|
|
122
|
+
}
|
|
123
|
+
async function getOAuthClient(adapter, authBaseURL) {
|
|
124
|
+
if (oauthClient)
|
|
125
|
+
return oauthClient;
|
|
126
|
+
if (!keysetPromise) {
|
|
127
|
+
keysetPromise = Promise.all([
|
|
128
|
+
JoseKey.fromImportable(JSON.stringify(privateKey))
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
const keyset = await keysetPromise;
|
|
132
|
+
const fullClientMetadata = buildClientMetadata(authBaseURL);
|
|
133
|
+
oauthClient = new NodeOAuthClient({
|
|
134
|
+
clientMetadata: {
|
|
135
|
+
...fullClientMetadata,
|
|
136
|
+
redirect_uris: fullClientMetadata.redirect_uris
|
|
137
|
+
},
|
|
138
|
+
keyset,
|
|
139
|
+
stateStore: {
|
|
140
|
+
async set(key, state) {
|
|
141
|
+
try {
|
|
142
|
+
await adapter.delete({
|
|
143
|
+
model: "atprotoState",
|
|
144
|
+
where: [{ field: "key", value: key }]
|
|
145
|
+
});
|
|
146
|
+
} catch {}
|
|
147
|
+
await adapter.create({
|
|
148
|
+
model: "atprotoState",
|
|
149
|
+
data: {
|
|
150
|
+
key,
|
|
151
|
+
state: JSON.stringify(state),
|
|
152
|
+
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
async get(key) {
|
|
157
|
+
const record = await adapter.findOne({
|
|
158
|
+
model: "atprotoState",
|
|
159
|
+
where: [{ field: "key", value: key }]
|
|
160
|
+
});
|
|
161
|
+
if (!record)
|
|
162
|
+
return;
|
|
163
|
+
if (new Date(record.expiresAt) < new Date) {
|
|
164
|
+
await adapter.delete({
|
|
165
|
+
model: "atprotoState",
|
|
166
|
+
where: [{ field: "key", value: key }]
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
return JSON.parse(record.state);
|
|
171
|
+
},
|
|
172
|
+
async del(key) {
|
|
173
|
+
await adapter.delete({
|
|
174
|
+
model: "atprotoState",
|
|
175
|
+
where: [{ field: "key", value: key }]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
sessionStore: {
|
|
180
|
+
async set(did, session) {
|
|
181
|
+
const existing = await adapter.findOne({
|
|
182
|
+
model: "atprotoSession",
|
|
183
|
+
where: [{ field: "did", value: did }]
|
|
184
|
+
});
|
|
185
|
+
if (existing) {
|
|
186
|
+
await adapter.update({
|
|
187
|
+
model: "atprotoSession",
|
|
188
|
+
where: [{ field: "did", value: did }],
|
|
189
|
+
update: {
|
|
190
|
+
session: JSON.stringify(session),
|
|
191
|
+
updatedAt: new Date
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
await adapter.create({
|
|
196
|
+
model: "atprotoSession",
|
|
197
|
+
data: {
|
|
198
|
+
did,
|
|
199
|
+
session: JSON.stringify(session),
|
|
200
|
+
userId: "",
|
|
201
|
+
updatedAt: new Date
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
async get(did) {
|
|
207
|
+
const record = await adapter.findOne({
|
|
208
|
+
model: "atprotoSession",
|
|
209
|
+
where: [{ field: "did", value: did }]
|
|
210
|
+
});
|
|
211
|
+
if (!record || !record.session)
|
|
212
|
+
return;
|
|
213
|
+
return JSON.parse(record.session);
|
|
214
|
+
},
|
|
215
|
+
async del(did) {
|
|
216
|
+
await adapter.delete({
|
|
217
|
+
model: "atprotoSession",
|
|
218
|
+
where: [{ field: "did", value: did }]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return oauthClient;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
id: "atproto",
|
|
227
|
+
schema: atprotoSchema,
|
|
228
|
+
endpoints: {
|
|
229
|
+
signInAtproto: createAuthEndpoint("/atproto/sign-in", {
|
|
230
|
+
method: "GET",
|
|
231
|
+
query: z.object({
|
|
232
|
+
handle: z.string().min(1),
|
|
233
|
+
callbackURL: z.string().optional()
|
|
234
|
+
})
|
|
235
|
+
}, async (ctx) => {
|
|
236
|
+
const { handle, callbackURL } = ctx.query;
|
|
237
|
+
const client = await getOAuthClient(ctx.context.adapter, ctx.context.baseURL);
|
|
238
|
+
const state = callbackURL ? JSON.stringify({ callbackURL }) : undefined;
|
|
239
|
+
const authUrl = await client.authorize(handle, { state });
|
|
240
|
+
throw ctx.redirect(authUrl.toString());
|
|
241
|
+
}),
|
|
242
|
+
callbackAtproto: createAuthEndpoint("/callback/atproto", {
|
|
243
|
+
method: "GET",
|
|
244
|
+
query: z.object({
|
|
245
|
+
code: z.string().optional(),
|
|
246
|
+
state: z.string().optional(),
|
|
247
|
+
iss: z.string().optional(),
|
|
248
|
+
error: z.string().optional(),
|
|
249
|
+
error_description: z.string().optional()
|
|
250
|
+
})
|
|
251
|
+
}, async (ctx) => {
|
|
252
|
+
const client = await getOAuthClient(ctx.context.adapter, ctx.context.baseURL);
|
|
253
|
+
const url = new URL(ctx.request?.url ?? "", "http://localhost");
|
|
254
|
+
const params = url.searchParams;
|
|
255
|
+
const { session: atprotoSession, state } = await client.callback(params);
|
|
256
|
+
let callbackURL = "/";
|
|
257
|
+
if (state) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(state);
|
|
260
|
+
if (parsed.callbackURL) {
|
|
261
|
+
callbackURL = parsed.callbackURL;
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
const agent = new Agent(atprotoSession);
|
|
266
|
+
const profileResponse = await agent.getProfile({
|
|
267
|
+
actor: atprotoSession.did
|
|
268
|
+
});
|
|
269
|
+
const profile = {
|
|
270
|
+
did: atprotoSession.did,
|
|
271
|
+
handle: profileResponse.data.handle,
|
|
272
|
+
displayName: profileResponse.data.displayName,
|
|
273
|
+
avatar: profileResponse.data.avatar,
|
|
274
|
+
description: profileResponse.data.description,
|
|
275
|
+
banner: profileResponse.data.banner
|
|
276
|
+
};
|
|
277
|
+
const existingAccount = await ctx.context.adapter.findOne({
|
|
278
|
+
model: "account",
|
|
279
|
+
where: [
|
|
280
|
+
{ field: "providerId", value: "atproto" },
|
|
281
|
+
{ field: "accountId", value: profile.did }
|
|
282
|
+
]
|
|
283
|
+
});
|
|
284
|
+
let userId;
|
|
285
|
+
if (existingAccount) {
|
|
286
|
+
userId = existingAccount.userId;
|
|
287
|
+
await ctx.context.adapter.update({
|
|
288
|
+
model: "account",
|
|
289
|
+
where: [{ field: "id", value: existingAccount.id }],
|
|
290
|
+
update: {
|
|
291
|
+
accessToken: "atproto-session",
|
|
292
|
+
updatedAt: new Date
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
await ctx.context.adapter.update({
|
|
296
|
+
model: "user",
|
|
297
|
+
where: [{ field: "id", value: userId }],
|
|
298
|
+
update: {
|
|
299
|
+
atprotoHandle: profile.handle,
|
|
300
|
+
atprotoBio: profile.description,
|
|
301
|
+
atprotoBanner: profile.banner,
|
|
302
|
+
image: profile.avatar,
|
|
303
|
+
name: profile.displayName ?? profile.handle,
|
|
304
|
+
updatedAt: new Date
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
const currentSession = ctx.context.session;
|
|
309
|
+
if (currentSession?.user) {
|
|
310
|
+
userId = currentSession.user.id;
|
|
311
|
+
} else {
|
|
312
|
+
const userFields = mapProfileToUser ? mapProfileToUser(profile) : {};
|
|
313
|
+
const newUser = await ctx.context.internalAdapter.createUser({
|
|
314
|
+
name: profile.displayName ?? profile.handle,
|
|
315
|
+
email: `${profile.did}@atproto.invalid`,
|
|
316
|
+
image: profile.avatar,
|
|
317
|
+
emailVerified: false,
|
|
318
|
+
atprotoDid: profile.did,
|
|
319
|
+
atprotoHandle: profile.handle,
|
|
320
|
+
atprotoBio: profile.description,
|
|
321
|
+
atprotoBanner: profile.banner,
|
|
322
|
+
...userFields
|
|
323
|
+
});
|
|
324
|
+
userId = newUser.id;
|
|
325
|
+
}
|
|
326
|
+
await ctx.context.adapter.create({
|
|
327
|
+
model: "account",
|
|
328
|
+
data: {
|
|
329
|
+
userId,
|
|
330
|
+
providerId: "atproto",
|
|
331
|
+
accountId: profile.did,
|
|
332
|
+
accessToken: "atproto-session",
|
|
333
|
+
refreshToken: null,
|
|
334
|
+
expiresAt: null,
|
|
335
|
+
scope: clientMetadata.scope ?? "atproto transition:generic",
|
|
336
|
+
createdAt: new Date,
|
|
337
|
+
updatedAt: new Date
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
const existingAtprotoSession = await ctx.context.adapter.findOne({
|
|
342
|
+
model: "atprotoSession",
|
|
343
|
+
where: [{ field: "did", value: profile.did }]
|
|
344
|
+
});
|
|
345
|
+
if (existingAtprotoSession) {
|
|
346
|
+
await ctx.context.adapter.update({
|
|
347
|
+
model: "atprotoSession",
|
|
348
|
+
where: [{ field: "did", value: profile.did }],
|
|
349
|
+
update: {
|
|
350
|
+
userId,
|
|
351
|
+
updatedAt: new Date
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
await ctx.context.adapter.create({
|
|
356
|
+
model: "atprotoSession",
|
|
357
|
+
data: {
|
|
358
|
+
did: profile.did,
|
|
359
|
+
session: "",
|
|
360
|
+
userId,
|
|
361
|
+
updatedAt: new Date
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const session = await ctx.context.internalAdapter.createSession(userId, undefined, undefined);
|
|
366
|
+
const sessionCookie = ctx.context.authCookies.sessionToken;
|
|
367
|
+
await ctx.setSignedCookie(sessionCookie.name, session.token, ctx.context.secret, sessionCookie.attributes);
|
|
368
|
+
throw ctx.redirect(callbackURL);
|
|
369
|
+
}),
|
|
370
|
+
getAtprotoSession: createAuthEndpoint("/atproto/session", {
|
|
371
|
+
method: "GET"
|
|
372
|
+
}, async (ctx) => {
|
|
373
|
+
const currentSession = ctx.context.session;
|
|
374
|
+
if (!currentSession?.user) {
|
|
375
|
+
return ctx.json({ session: null });
|
|
376
|
+
}
|
|
377
|
+
const account = await ctx.context.adapter.findOne({
|
|
378
|
+
model: "account",
|
|
379
|
+
where: [
|
|
380
|
+
{ field: "userId", value: currentSession.user.id },
|
|
381
|
+
{ field: "providerId", value: "atproto" }
|
|
382
|
+
]
|
|
383
|
+
});
|
|
384
|
+
if (!account) {
|
|
385
|
+
return ctx.json({ session: null });
|
|
386
|
+
}
|
|
387
|
+
const client = await getOAuthClient(ctx.context.adapter, ctx.context.baseURL);
|
|
388
|
+
try {
|
|
389
|
+
const atprotoSession = await client.restore(account.accountId);
|
|
390
|
+
const agent = new Agent(atprotoSession);
|
|
391
|
+
const profileResponse = await agent.getProfile({
|
|
392
|
+
actor: atprotoSession.did
|
|
393
|
+
});
|
|
394
|
+
const sessionInfo = {
|
|
395
|
+
did: atprotoSession.did,
|
|
396
|
+
handle: profileResponse.data.handle,
|
|
397
|
+
displayName: profileResponse.data.displayName,
|
|
398
|
+
avatar: profileResponse.data.avatar,
|
|
399
|
+
active: true
|
|
400
|
+
};
|
|
401
|
+
return ctx.json({ session: sessionInfo });
|
|
402
|
+
} catch {
|
|
403
|
+
return ctx.json({
|
|
404
|
+
session: {
|
|
405
|
+
did: account.accountId,
|
|
406
|
+
handle: "",
|
|
407
|
+
active: false
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}),
|
|
412
|
+
restoreAtprotoAgent: createAuthEndpoint("/atproto/restore", {
|
|
413
|
+
method: "POST"
|
|
414
|
+
}, async (ctx) => {
|
|
415
|
+
const currentSession = ctx.context.session;
|
|
416
|
+
if (!currentSession?.user) {
|
|
417
|
+
return ctx.json({ error: "Not authenticated" }, { status: 401 });
|
|
418
|
+
}
|
|
419
|
+
const account = await ctx.context.adapter.findOne({
|
|
420
|
+
model: "account",
|
|
421
|
+
where: [
|
|
422
|
+
{ field: "userId", value: currentSession.user.id },
|
|
423
|
+
{ field: "providerId", value: "atproto" }
|
|
424
|
+
]
|
|
425
|
+
});
|
|
426
|
+
if (!account) {
|
|
427
|
+
return ctx.json({ error: "No ATProto account linked" }, { status: 404 });
|
|
428
|
+
}
|
|
429
|
+
const client = await getOAuthClient(ctx.context.adapter, ctx.context.baseURL);
|
|
430
|
+
try {
|
|
431
|
+
const atprotoSession = await client.restore(account.accountId);
|
|
432
|
+
return ctx.json({
|
|
433
|
+
did: atprotoSession.did,
|
|
434
|
+
active: true
|
|
435
|
+
});
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return ctx.json({
|
|
438
|
+
error: "Failed to restore ATProto session",
|
|
439
|
+
details: error instanceof Error ? error.message : "Unknown error"
|
|
440
|
+
}, { status: 400 });
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// src/utils.ts
|
|
447
|
+
function getPublicJwk(privateKey) {
|
|
448
|
+
const { d: _privateComponent, ...publicKey } = privateKey;
|
|
449
|
+
return publicKey;
|
|
450
|
+
}
|
|
451
|
+
function createJwks(privateKey) {
|
|
452
|
+
return {
|
|
453
|
+
keys: [getPublicJwk(privateKey)]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function createClientMetadata(options) {
|
|
457
|
+
const { clientMetadata } = options;
|
|
458
|
+
const hasJwks = !!clientMetadata.jwksUri;
|
|
459
|
+
return {
|
|
460
|
+
client_id: clientMetadata.clientId,
|
|
461
|
+
client_name: clientMetadata.clientName,
|
|
462
|
+
client_uri: clientMetadata.clientUri,
|
|
463
|
+
logo_uri: clientMetadata.logoUri,
|
|
464
|
+
tos_uri: clientMetadata.tosUri,
|
|
465
|
+
policy_uri: clientMetadata.policyUri,
|
|
466
|
+
redirect_uris: clientMetadata.redirectUris,
|
|
467
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
468
|
+
response_types: ["code"],
|
|
469
|
+
scope: clientMetadata.scope ?? "atproto transition:generic",
|
|
470
|
+
dpop_bound_access_tokens: true,
|
|
471
|
+
application_type: "web",
|
|
472
|
+
token_endpoint_auth_method: hasJwks ? "private_key_jwt" : "none",
|
|
473
|
+
token_endpoint_auth_signing_alg: hasJwks ? "ES256" : undefined,
|
|
474
|
+
jwks_uri: clientMetadata.jwksUri
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function generateES256Key() {
|
|
478
|
+
const keyPair = await crypto.subtle.generateKey({
|
|
479
|
+
name: "ECDSA",
|
|
480
|
+
namedCurve: "P-256"
|
|
481
|
+
}, true, ["sign", "verify"]);
|
|
482
|
+
const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
|
|
483
|
+
return {
|
|
484
|
+
kty: "EC",
|
|
485
|
+
crv: "P-256",
|
|
486
|
+
x: privateJwk.x,
|
|
487
|
+
y: privateJwk.y,
|
|
488
|
+
d: privateJwk.d,
|
|
489
|
+
alg: "ES256",
|
|
490
|
+
kid: crypto.randomUUID()
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function isValidES256PrivateKey(jwk) {
|
|
494
|
+
if (!jwk || typeof jwk !== "object")
|
|
495
|
+
return false;
|
|
496
|
+
const key = jwk;
|
|
497
|
+
return key.kty === "EC" && key.crv === "P-256" && typeof key.x === "string" && typeof key.y === "string" && typeof key.d === "string";
|
|
498
|
+
}
|
|
499
|
+
function createClientMetadataHandler(options) {
|
|
500
|
+
const metadata = createClientMetadata(options);
|
|
501
|
+
return () => {
|
|
502
|
+
return new Response(JSON.stringify(metadata), {
|
|
503
|
+
headers: {
|
|
504
|
+
"Content-Type": "application/json"
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function createJwksHandler(privateKey) {
|
|
510
|
+
const jwks = createJwks(privateKey);
|
|
511
|
+
return () => {
|
|
512
|
+
return new Response(JSON.stringify(jwks), {
|
|
513
|
+
headers: {
|
|
514
|
+
"Content-Type": "application/json"
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
var DEFAULT_ATPROTO_SCOPES = "atproto transition:generic";
|
|
520
|
+
export {
|
|
521
|
+
isValidES256PrivateKey,
|
|
522
|
+
getPublicJwk,
|
|
523
|
+
generateES256Key,
|
|
524
|
+
createJwksHandler,
|
|
525
|
+
createJwks,
|
|
526
|
+
createClientMetadataHandler,
|
|
527
|
+
createClientMetadata,
|
|
528
|
+
atprotoUserSchema,
|
|
529
|
+
atprotoStateSchema,
|
|
530
|
+
atprotoSessionSchema,
|
|
531
|
+
atprotoSchema,
|
|
532
|
+
atprotoAuth,
|
|
533
|
+
DEFAULT_ATPROTO_SCOPES
|
|
534
|
+
};
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database schema definitions for the ATProto better-auth plugin.
|
|
3
|
+
* These tables store OAuth state and session data required by @atproto/oauth-client-node.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Schema extensions for the user table.
|
|
7
|
+
* Adds ATProto-specific fields to store profile data.
|
|
8
|
+
*/
|
|
9
|
+
export declare const atprotoUserSchema: {
|
|
10
|
+
readonly user: {
|
|
11
|
+
readonly fields: {
|
|
12
|
+
readonly atprotoDid: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly required: false;
|
|
15
|
+
readonly unique: true;
|
|
16
|
+
};
|
|
17
|
+
readonly atprotoHandle: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
readonly required: false;
|
|
20
|
+
};
|
|
21
|
+
readonly atprotoBio: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly required: false;
|
|
24
|
+
};
|
|
25
|
+
readonly atprotoBanner: {
|
|
26
|
+
readonly type: "string";
|
|
27
|
+
readonly required: false;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Schema for the atprotoState table.
|
|
34
|
+
* Stores temporary OAuth state during the authorization flow.
|
|
35
|
+
*/
|
|
36
|
+
export declare const atprotoStateSchema: {
|
|
37
|
+
readonly atprotoState: {
|
|
38
|
+
readonly fields: {
|
|
39
|
+
readonly key: {
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
readonly required: true;
|
|
42
|
+
readonly unique: true;
|
|
43
|
+
};
|
|
44
|
+
readonly state: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
readonly required: true;
|
|
47
|
+
};
|
|
48
|
+
readonly expiresAt: {
|
|
49
|
+
readonly type: "date";
|
|
50
|
+
readonly required: true;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Schema for the atprotoSession table.
|
|
57
|
+
* Stores ATProto OAuth sessions including tokens, DPoP keys, etc.
|
|
58
|
+
* This is separate from better-auth's session table because ATProto
|
|
59
|
+
* sessions contain additional cryptographic material needed for API calls.
|
|
60
|
+
*/
|
|
61
|
+
export declare const atprotoSessionSchema: {
|
|
62
|
+
readonly atprotoSession: {
|
|
63
|
+
readonly fields: {
|
|
64
|
+
readonly did: {
|
|
65
|
+
readonly type: "string";
|
|
66
|
+
readonly required: true;
|
|
67
|
+
readonly unique: true;
|
|
68
|
+
};
|
|
69
|
+
readonly session: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly required: true;
|
|
72
|
+
};
|
|
73
|
+
readonly userId: {
|
|
74
|
+
readonly type: "string";
|
|
75
|
+
readonly required: true;
|
|
76
|
+
readonly references: {
|
|
77
|
+
readonly model: "user";
|
|
78
|
+
readonly field: "id";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
readonly updatedAt: {
|
|
82
|
+
readonly type: "date";
|
|
83
|
+
readonly required: true;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Combined schema for the plugin
|
|
90
|
+
*/
|
|
91
|
+
export declare const atprotoSchema: {
|
|
92
|
+
readonly atprotoSession: {
|
|
93
|
+
readonly fields: {
|
|
94
|
+
readonly did: {
|
|
95
|
+
readonly type: "string";
|
|
96
|
+
readonly required: true;
|
|
97
|
+
readonly unique: true;
|
|
98
|
+
};
|
|
99
|
+
readonly session: {
|
|
100
|
+
readonly type: "string";
|
|
101
|
+
readonly required: true;
|
|
102
|
+
};
|
|
103
|
+
readonly userId: {
|
|
104
|
+
readonly type: "string";
|
|
105
|
+
readonly required: true;
|
|
106
|
+
readonly references: {
|
|
107
|
+
readonly model: "user";
|
|
108
|
+
readonly field: "id";
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
readonly updatedAt: {
|
|
112
|
+
readonly type: "date";
|
|
113
|
+
readonly required: true;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
readonly atprotoState: {
|
|
118
|
+
readonly fields: {
|
|
119
|
+
readonly key: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
readonly required: true;
|
|
122
|
+
readonly unique: true;
|
|
123
|
+
};
|
|
124
|
+
readonly state: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
readonly required: true;
|
|
127
|
+
};
|
|
128
|
+
readonly expiresAt: {
|
|
129
|
+
readonly type: "date";
|
|
130
|
+
readonly required: true;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
readonly user: {
|
|
135
|
+
readonly fields: {
|
|
136
|
+
readonly atprotoDid: {
|
|
137
|
+
readonly type: "string";
|
|
138
|
+
readonly required: false;
|
|
139
|
+
readonly unique: true;
|
|
140
|
+
};
|
|
141
|
+
readonly atprotoHandle: {
|
|
142
|
+
readonly type: "string";
|
|
143
|
+
readonly required: false;
|
|
144
|
+
};
|
|
145
|
+
readonly atprotoBio: {
|
|
146
|
+
readonly type: "string";
|
|
147
|
+
readonly required: false;
|
|
148
|
+
};
|
|
149
|
+
readonly atprotoBanner: {
|
|
150
|
+
readonly type: "string";
|
|
151
|
+
readonly required: false;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from "better-auth";
|
|
2
|
+
import type { AtprotoAuthOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates the ATProto better-auth plugin for server-side use.
|
|
5
|
+
*/
|
|
6
|
+
export declare function atprotoAuth(options: AtprotoAuthOptions): BetterAuthPlugin;
|
|
7
|
+
export type { AtprotoAuthOptions };
|