@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.
@@ -0,0 +1,951 @@
1
+ import { OAuthCallbackError, OAuthClient, buildPublicClientMetadata } from "@atcute/oauth-node-client";
2
+ import { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } from "@atcute/identity-resolver";
3
+ import { Client } from "@atcute/client";
4
+ import { APIError, createAuthEndpoint } from "better-auth/api";
5
+ import { setSessionCookie } from "better-auth/cookies";
6
+ import { isDid } from "@atcute/lexicons/syntax";
7
+ const DEFAULT_CONFIG = {
8
+ lang: void 0,
9
+ message: void 0,
10
+ abortEarly: void 0,
11
+ abortPipeEarly: void 0
12
+ };
13
+ /**
14
+ * Returns the global configuration.
15
+ *
16
+ * @param config The config to merge.
17
+ *
18
+ * @returns The configuration.
19
+ */
20
+ /* @__NO_SIDE_EFFECTS__ */
21
+ function getGlobalConfig(config$1) {
22
+ if (!config$1 && true) return DEFAULT_CONFIG;
23
+ return {
24
+ lang: config$1?.lang ?? void 0,
25
+ message: config$1?.message,
26
+ abortEarly: config$1?.abortEarly ?? void 0,
27
+ abortPipeEarly: config$1?.abortPipeEarly ?? void 0
28
+ };
29
+ }
30
+ /**
31
+ * Stringifies an unknown input to a literal or type string.
32
+ *
33
+ * @param input The unknown input.
34
+ *
35
+ * @returns A literal or type string.
36
+ *
37
+ * @internal
38
+ */
39
+ /* @__NO_SIDE_EFFECTS__ */
40
+ function _stringify(input) {
41
+ const type = typeof input;
42
+ if (type === "string") return `"${input}"`;
43
+ if (type === "number" || type === "bigint" || type === "boolean") return `${input}`;
44
+ if (type === "object" || type === "function") return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
45
+ return type;
46
+ }
47
+ /**
48
+ * Adds an issue to the dataset.
49
+ *
50
+ * @param context The issue context.
51
+ * @param label The issue label.
52
+ * @param dataset The input dataset.
53
+ * @param config The configuration.
54
+ * @param other The optional props.
55
+ *
56
+ * @internal
57
+ */
58
+ function _addIssue(context, label, dataset, config$1, other) {
59
+ const input = other && "input" in other ? other.input : dataset.value;
60
+ const expected = other?.expected ?? context.expects ?? null;
61
+ const received = other?.received ?? /* @__PURE__ */ _stringify(input);
62
+ const issue = {
63
+ kind: context.kind,
64
+ type: context.type,
65
+ input,
66
+ expected,
67
+ received,
68
+ message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
69
+ requirement: context.requirement,
70
+ path: other?.path,
71
+ issues: other?.issues,
72
+ lang: config$1.lang,
73
+ abortEarly: config$1.abortEarly,
74
+ abortPipeEarly: config$1.abortPipeEarly
75
+ };
76
+ const isSchema = context.kind === "schema";
77
+ const message$1 = other?.message ?? context.message ?? (context.reference, issue.lang, void 0) ?? (isSchema ? (issue.lang, void 0) : null) ?? config$1.message ?? (issue.lang, void 0);
78
+ if (message$1 !== void 0) issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
79
+ if (isSchema) dataset.typed = false;
80
+ if (dataset.issues) dataset.issues.push(issue);
81
+ else dataset.issues = [issue];
82
+ }
83
+ const _standardCache = /* @__PURE__ */ new WeakMap();
84
+ /**
85
+ * Returns the Standard Schema properties.
86
+ *
87
+ * @param context The schema context.
88
+ *
89
+ * @returns The Standard Schema properties.
90
+ */
91
+ /* @__NO_SIDE_EFFECTS__ */
92
+ function _getStandardProps(context) {
93
+ let cached = _standardCache.get(context);
94
+ if (!cached) {
95
+ cached = {
96
+ version: 1,
97
+ vendor: "valibot",
98
+ validate(value$1) {
99
+ return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
100
+ }
101
+ };
102
+ _standardCache.set(context, cached);
103
+ }
104
+ return cached;
105
+ }
106
+ /**
107
+ * Creates a description metadata action.
108
+ *
109
+ * @param description_ The description text.
110
+ *
111
+ * @returns A description action.
112
+ */
113
+ /* @__NO_SIDE_EFFECTS__ */
114
+ function description(description_) {
115
+ return {
116
+ kind: "metadata",
117
+ type: "description",
118
+ reference: description,
119
+ description: description_
120
+ };
121
+ }
122
+ /**
123
+ * Returns the fallback value of the schema.
124
+ *
125
+ * @param schema The schema to get it from.
126
+ * @param dataset The output dataset if available.
127
+ * @param config The config if available.
128
+ *
129
+ * @returns The fallback value.
130
+ */
131
+ /* @__NO_SIDE_EFFECTS__ */
132
+ function getFallback(schema, dataset, config$1) {
133
+ return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
134
+ }
135
+ /**
136
+ * Returns the default value of the schema.
137
+ *
138
+ * @param schema The schema to get it from.
139
+ * @param dataset The input dataset if available.
140
+ * @param config The config if available.
141
+ *
142
+ * @returns The default value.
143
+ */
144
+ /* @__NO_SIDE_EFFECTS__ */
145
+ function getDefault(schema, dataset, config$1) {
146
+ return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
147
+ }
148
+ /* @__NO_SIDE_EFFECTS__ */
149
+ function object(entries$1, message$1) {
150
+ return {
151
+ kind: "schema",
152
+ type: "object",
153
+ reference: object,
154
+ expects: "Object",
155
+ async: false,
156
+ entries: entries$1,
157
+ message: message$1,
158
+ get "~standard"() {
159
+ return /* @__PURE__ */ _getStandardProps(this);
160
+ },
161
+ "~run"(dataset, config$1) {
162
+ const input = dataset.value;
163
+ if (input && typeof input === "object") {
164
+ dataset.typed = true;
165
+ dataset.value = {};
166
+ for (const key in this.entries) {
167
+ const valueSchema = this.entries[key];
168
+ if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== void 0) {
169
+ const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
170
+ const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
171
+ if (valueDataset.issues) {
172
+ const pathItem = {
173
+ type: "object",
174
+ origin: "value",
175
+ input,
176
+ key,
177
+ value: value$1
178
+ };
179
+ for (const issue of valueDataset.issues) {
180
+ if (issue.path) issue.path.unshift(pathItem);
181
+ else issue.path = [pathItem];
182
+ dataset.issues?.push(issue);
183
+ }
184
+ if (!dataset.issues) dataset.issues = valueDataset.issues;
185
+ if (config$1.abortEarly) {
186
+ dataset.typed = false;
187
+ break;
188
+ }
189
+ }
190
+ if (!valueDataset.typed) dataset.typed = false;
191
+ dataset.value[key] = valueDataset.value;
192
+ } else if (valueSchema.fallback !== void 0) dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
193
+ else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
194
+ _addIssue(this, "key", dataset, config$1, {
195
+ input: void 0,
196
+ expected: `"${key}"`,
197
+ path: [{
198
+ type: "object",
199
+ origin: "key",
200
+ input,
201
+ key,
202
+ value: input[key]
203
+ }]
204
+ });
205
+ if (config$1.abortEarly) break;
206
+ }
207
+ }
208
+ } else _addIssue(this, "type", dataset, config$1);
209
+ return dataset;
210
+ }
211
+ };
212
+ }
213
+ /* @__NO_SIDE_EFFECTS__ */
214
+ function optional(wrapped, default_) {
215
+ return {
216
+ kind: "schema",
217
+ type: "optional",
218
+ reference: optional,
219
+ expects: `(${wrapped.expects} | undefined)`,
220
+ async: false,
221
+ wrapped,
222
+ default: default_,
223
+ get "~standard"() {
224
+ return /* @__PURE__ */ _getStandardProps(this);
225
+ },
226
+ "~run"(dataset, config$1) {
227
+ if (dataset.value === void 0) {
228
+ if (this.default !== void 0) dataset.value = /* @__PURE__ */ getDefault(this, dataset, config$1);
229
+ if (dataset.value === void 0) {
230
+ dataset.typed = true;
231
+ return dataset;
232
+ }
233
+ }
234
+ return this.wrapped["~run"](dataset, config$1);
235
+ }
236
+ };
237
+ }
238
+ /* @__NO_SIDE_EFFECTS__ */
239
+ function string(message$1) {
240
+ return {
241
+ kind: "schema",
242
+ type: "string",
243
+ reference: string,
244
+ expects: "string",
245
+ async: false,
246
+ message: message$1,
247
+ get "~standard"() {
248
+ return /* @__PURE__ */ _getStandardProps(this);
249
+ },
250
+ "~run"(dataset, config$1) {
251
+ if (typeof dataset.value === "string") dataset.typed = true;
252
+ else _addIssue(this, "type", dataset, config$1);
253
+ return dataset;
254
+ }
255
+ };
256
+ }
257
+ /* @__NO_SIDE_EFFECTS__ */
258
+ function pipe(...pipe$1) {
259
+ return {
260
+ ...pipe$1[0],
261
+ pipe: pipe$1,
262
+ get "~standard"() {
263
+ return /* @__PURE__ */ _getStandardProps(this);
264
+ },
265
+ "~run"(dataset, config$1) {
266
+ for (const item of pipe$1) if (item.kind !== "metadata") {
267
+ if (dataset.issues && (item.kind === "schema" || item.kind === "transformation")) {
268
+ dataset.typed = false;
269
+ break;
270
+ }
271
+ if (!dataset.issues || !config$1.abortEarly && !config$1.abortPipeEarly) dataset = item["~run"](dataset, config$1);
272
+ }
273
+ return dataset;
274
+ }
275
+ };
276
+ }
277
+ //#endregion
278
+ //#region src/types.ts
279
+ /** Database schema field definitions for better-auth plugin schema. */
280
+ const atprotoSchema = {
281
+ user: { fields: {
282
+ atprotoDid: {
283
+ type: "string",
284
+ unique: true,
285
+ required: false,
286
+ returned: true,
287
+ input: false
288
+ },
289
+ atprotoHandle: {
290
+ type: "string",
291
+ required: false,
292
+ returned: true,
293
+ input: false
294
+ }
295
+ } },
296
+ atprotoSession: { fields: {
297
+ did: {
298
+ type: "string",
299
+ unique: true,
300
+ required: true
301
+ },
302
+ sessionData: {
303
+ type: "string",
304
+ required: true
305
+ },
306
+ userId: {
307
+ type: "string",
308
+ required: true,
309
+ references: {
310
+ model: "user",
311
+ field: "id",
312
+ onDelete: "cascade"
313
+ }
314
+ },
315
+ handle: {
316
+ type: "string",
317
+ required: true
318
+ },
319
+ pdsUrl: {
320
+ type: "string",
321
+ required: true
322
+ },
323
+ updatedAt: {
324
+ type: "date",
325
+ required: true
326
+ }
327
+ } },
328
+ atprotoState: { fields: {
329
+ stateKey: {
330
+ type: "string",
331
+ unique: true,
332
+ required: true
333
+ },
334
+ stateData: {
335
+ type: "string",
336
+ required: true
337
+ },
338
+ expiresAt: {
339
+ type: "number",
340
+ required: true
341
+ }
342
+ } }
343
+ };
344
+ //#endregion
345
+ //#region src/stores.ts
346
+ /** Database-backed session store for @atcute/oauth-node-client. */
347
+ var DbSessionStore = class {
348
+ adapter;
349
+ constructor(adapter) {
350
+ this.adapter = adapter;
351
+ }
352
+ async get(did) {
353
+ const row = await this.adapter.findOne({
354
+ model: "atprotoSession",
355
+ where: [{
356
+ field: "did",
357
+ value: did
358
+ }]
359
+ });
360
+ if (!row) return void 0;
361
+ return JSON.parse(row.sessionData);
362
+ }
363
+ async set(did, session) {
364
+ const data = JSON.stringify(session);
365
+ if (await this.adapter.findOne({
366
+ model: "atprotoSession",
367
+ where: [{
368
+ field: "did",
369
+ value: did
370
+ }]
371
+ })) await this.adapter.update({
372
+ model: "atprotoSession",
373
+ where: [{
374
+ field: "did",
375
+ value: did
376
+ }],
377
+ update: {
378
+ sessionData: data,
379
+ updatedAt: /* @__PURE__ */ new Date()
380
+ }
381
+ });
382
+ else await this.adapter.create({
383
+ model: "atprotoSession",
384
+ data: {
385
+ did,
386
+ sessionData: data,
387
+ userId: "",
388
+ handle: "",
389
+ pdsUrl: "",
390
+ updatedAt: /* @__PURE__ */ new Date()
391
+ }
392
+ });
393
+ }
394
+ async delete(did) {
395
+ await this.adapter.delete({
396
+ model: "atprotoSession",
397
+ where: [{
398
+ field: "did",
399
+ value: did
400
+ }]
401
+ });
402
+ }
403
+ async clear() {
404
+ await this.adapter.deleteMany({
405
+ model: "atprotoSession",
406
+ where: [{
407
+ field: "did",
408
+ value: {
409
+ operator: "ne",
410
+ value: ""
411
+ }
412
+ }]
413
+ });
414
+ }
415
+ };
416
+ /** Database-backed state store for @atcute/oauth-node-client. */
417
+ var DbStateStore = class {
418
+ adapter;
419
+ constructor(adapter) {
420
+ this.adapter = adapter;
421
+ }
422
+ async get(stateKey) {
423
+ const row = await this.adapter.findOne({
424
+ model: "atprotoState",
425
+ where: [{
426
+ field: "stateKey",
427
+ value: stateKey
428
+ }]
429
+ });
430
+ if (!row) return void 0;
431
+ if (row.expiresAt < Date.now()) {
432
+ await this.delete(stateKey);
433
+ return;
434
+ }
435
+ return JSON.parse(row.stateData);
436
+ }
437
+ async set(stateKey, state) {
438
+ const data = JSON.stringify(state);
439
+ await this.adapter.create({
440
+ model: "atprotoState",
441
+ data: {
442
+ stateKey,
443
+ stateData: data,
444
+ expiresAt: state.expiresAt
445
+ }
446
+ });
447
+ }
448
+ async delete(stateKey) {
449
+ await this.adapter.delete({
450
+ model: "atprotoState",
451
+ where: [{
452
+ field: "stateKey",
453
+ value: stateKey
454
+ }]
455
+ });
456
+ }
457
+ async clear() {
458
+ await this.adapter.deleteMany({
459
+ model: "atprotoState",
460
+ where: [{
461
+ field: "stateKey",
462
+ value: {
463
+ operator: "ne",
464
+ value: ""
465
+ }
466
+ }]
467
+ });
468
+ }
469
+ };
470
+ //#endregion
471
+ //#region src/server.ts
472
+ /** Safely extract a string field from an unknown record. */
473
+ function getString(data, key) {
474
+ const val = data[key];
475
+ return typeof val === "string" ? val : void 0;
476
+ }
477
+ /** Parse a JSON response body into a plain record. */
478
+ async function parseJsonResponse(resp) {
479
+ const body = await resp.json();
480
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) return body;
481
+ return {};
482
+ }
483
+ /** Convert a string to a Did, validating at runtime. Returns undefined if invalid. */
484
+ function toDid(value) {
485
+ return isDid(value) ? value : void 0;
486
+ }
487
+ function isLoopbackUrl(url) {
488
+ try {
489
+ const { hostname } = new URL(url);
490
+ return hostname === "localhost" || hostname === "127.0.0.1";
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+ /** Normalize scope option to a single space-joined string. Always includes "atproto" as the base. */
496
+ function normalizeScope(scope) {
497
+ if (!scope) return "atproto";
498
+ const parts = (Array.isArray(scope) ? scope.join(" ") : scope).split(/\s+/).filter(Boolean);
499
+ if (!parts.includes("atproto")) parts.unshift("atproto");
500
+ return parts.join(" ");
501
+ }
502
+ function buildMetadata(baseURL, options) {
503
+ const isLoopback = isLoopbackUrl(baseURL);
504
+ const callbackPath = options.callbackPath ?? "/atproto/callback";
505
+ const scope = normalizeScope(options.scope);
506
+ const isConfidential = !!options.keyset?.length;
507
+ const redirectUri = isLoopback ? `http://127.0.0.1:${new URL(baseURL).port}${new URL(baseURL).pathname}${callbackPath}` : `${baseURL}${callbackPath}`;
508
+ if (isLoopback) {
509
+ if (isConfidential) console.warn("[atproto] keyset provided but baseURL is loopback — falling back to public client mode. Use an HTTPS baseURL for confidential client support.");
510
+ return {
511
+ redirect_uris: [redirectUri],
512
+ scope
513
+ };
514
+ }
515
+ if (isConfidential) return {
516
+ client_id: `${baseURL}${options.clientMetadataPath ?? "/oauth-client-metadata.json"}`,
517
+ client_name: options.clientName,
518
+ client_uri: options.clientUri,
519
+ logo_uri: options.logoUri,
520
+ tos_uri: options.tosUri,
521
+ policy_uri: options.policyUri,
522
+ redirect_uris: [redirectUri],
523
+ scope,
524
+ grant_types: ["authorization_code", "refresh_token"],
525
+ response_types: ["code"],
526
+ application_type: "web",
527
+ token_endpoint_auth_method: "private_key_jwt",
528
+ dpop_bound_access_tokens: true,
529
+ jwks_uri: `${baseURL}${options.jwksPath ?? "/.well-known/jwks.json"}`
530
+ };
531
+ return buildPublicClientMetadata({
532
+ client_id: `${baseURL}${options.clientMetadataPath ?? "/oauth-client-metadata.json"}`,
533
+ client_name: options.clientName,
534
+ client_uri: options.clientUri,
535
+ logo_uri: options.logoUri,
536
+ tos_uri: options.tosUri,
537
+ policy_uri: options.policyUri,
538
+ redirect_uris: [redirectUri],
539
+ scope
540
+ });
541
+ }
542
+ function createActorResolver() {
543
+ return new LocalActorResolver({
544
+ handleResolver: new CompositeHandleResolver({ methods: {
545
+ dns: new DohJsonHandleResolver({ dohUrl: "https://cloudflare-dns.com/dns-query" }),
546
+ http: new WellKnownHandleResolver()
547
+ } }),
548
+ didDocumentResolver: new CompositeDidDocumentResolver({ methods: {
549
+ plc: new PlcDidDocumentResolver(),
550
+ web: new WebDidDocumentResolver()
551
+ } })
552
+ });
553
+ }
554
+ /**
555
+ * Fetch an ATProto profile using the Bluesky public API.
556
+ * This is a fallback when an authenticated session is not available.
557
+ */
558
+ async function fetchAtprotoProfilePublic(did) {
559
+ try {
560
+ const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`;
561
+ const resp = await fetch(url);
562
+ if (!resp.ok) return null;
563
+ const data = await parseJsonResponse(resp);
564
+ return {
565
+ did: getString(data, "did") ?? did,
566
+ handle: getString(data, "handle") ?? did,
567
+ displayName: getString(data, "displayName"),
568
+ avatar: getString(data, "avatar"),
569
+ banner: getString(data, "banner"),
570
+ description: getString(data, "description")
571
+ };
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ /**
577
+ * Fetch an ATProto profile using an authenticated XRPC client.
578
+ * Falls back to the public API if the authenticated call fails.
579
+ */
580
+ async function fetchAtprotoProfile(oauthClient, did) {
581
+ try {
582
+ const validDid = toDid(did);
583
+ if (!validDid) throw new Error(`Invalid DID: ${did}`);
584
+ const resp = await (await oauthClient.restore(validDid)).handle(`/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`);
585
+ if (!resp.ok) throw new Error(`Profile fetch failed: ${resp.status}`);
586
+ const profile = await parseJsonResponse(resp);
587
+ return {
588
+ did: getString(profile, "did") ?? did,
589
+ handle: getString(profile, "handle") ?? did,
590
+ displayName: getString(profile, "displayName"),
591
+ avatar: getString(profile, "avatar"),
592
+ banner: getString(profile, "banner"),
593
+ description: getString(profile, "description")
594
+ };
595
+ } catch {}
596
+ const publicProfile = await fetchAtprotoProfilePublic(did);
597
+ if (publicProfile) return publicProfile;
598
+ return {
599
+ did,
600
+ handle: did
601
+ };
602
+ }
603
+ /**
604
+ * Generate a deterministic placeholder email for an ATProto DID.
605
+ * ATProto doesn't expose user emails, but better-auth requires one.
606
+ * Uses the RFC 2606 reserved `.invalid` TLD.
607
+ */
608
+ function atprotoPlaceholderEmail(did) {
609
+ return `${did.replaceAll(":", "_")}@atproto.invalid`;
610
+ }
611
+ const ATPROTO_ERROR_CODES = {
612
+ INVALID_HANDLE: {
613
+ code: "INVALID_HANDLE",
614
+ message: "Invalid ATProto handle or DID"
615
+ },
616
+ AUTHORIZATION_FAILED: {
617
+ code: "AUTHORIZATION_FAILED",
618
+ message: "Failed to start ATProto authorization"
619
+ },
620
+ CALLBACK_FAILED: {
621
+ code: "CALLBACK_FAILED",
622
+ message: "ATProto OAuth callback failed"
623
+ },
624
+ SESSION_NOT_FOUND: {
625
+ code: "SESSION_NOT_FOUND",
626
+ message: "No ATProto session found for the current user"
627
+ },
628
+ SIGNUP_DISABLED: {
629
+ code: "SIGNUP_DISABLED",
630
+ message: "New user registration via ATProto is disabled"
631
+ },
632
+ ACCOUNT_LINKING_DISABLED: {
633
+ code: "ACCOUNT_LINKING_DISABLED",
634
+ message: "Account linking is not enabled"
635
+ }
636
+ };
637
+ /**
638
+ * ATProto OAuth plugin for better-auth.
639
+ *
640
+ * Integrates ATProto OAuth 2.1 (DPoP + PAR + PKCE) via @atcute/oauth-node-client.
641
+ * Supports both confidential (with keyset) and public client modes.
642
+ */
643
+ const atproto = (options) => {
644
+ let oauthClient;
645
+ const signInPath = options.signInPath ?? "/sign-in/atproto";
646
+ const callbackPath = options.callbackPath ?? "/atproto/callback";
647
+ return {
648
+ id: "atproto",
649
+ schema: atprotoSchema,
650
+ rateLimit: [{
651
+ pathMatcher: (path) => path === signInPath,
652
+ window: 60,
653
+ max: 5
654
+ }, {
655
+ pathMatcher: (path) => path === callbackPath,
656
+ window: 60,
657
+ max: 10
658
+ }],
659
+ init(ctx) {
660
+ const baseURL = ctx.baseURL;
661
+ const adapter = ctx.adapter;
662
+ const sessionStore = new DbSessionStore(adapter);
663
+ const stateStore = new DbStateStore(adapter);
664
+ const metadata = buildMetadata(baseURL, options);
665
+ const actorResolver = createActorResolver();
666
+ if (!!options.keyset?.length && !isLoopbackUrl(baseURL)) oauthClient = new OAuthClient({
667
+ metadata,
668
+ keyset: options.keyset,
669
+ actorResolver,
670
+ stores: {
671
+ sessions: sessionStore,
672
+ states: stateStore
673
+ }
674
+ });
675
+ else oauthClient = new OAuthClient({
676
+ metadata,
677
+ actorResolver,
678
+ stores: {
679
+ sessions: sessionStore,
680
+ states: stateStore
681
+ }
682
+ });
683
+ },
684
+ endpoints: {
685
+ atprotoClientMetadata: createAuthEndpoint(options.clientMetadataPath ?? "/oauth-client-metadata.json", { method: "GET" }, async (ctx) => {
686
+ return ctx.json(oauthClient.metadata);
687
+ }),
688
+ atprotoJwks: createAuthEndpoint(options.jwksPath ?? "/.well-known/jwks.json", { method: "GET" }, async (ctx) => {
689
+ const jwks = oauthClient.jwks;
690
+ if (!jwks) throw APIError.fromStatus("NOT_FOUND", { message: "JWKS not available (public client mode)" });
691
+ return ctx.json(jwks);
692
+ }),
693
+ signInAtproto: createAuthEndpoint(signInPath, {
694
+ method: "POST",
695
+ body: /* @__PURE__ */ object({
696
+ handle: /* @__PURE__ */ pipe(/* @__PURE__ */ string(), /* @__PURE__ */ description("ATProto handle (e.g. user.bsky.social) or DID")),
697
+ callbackURL: /* @__PURE__ */ optional(/* @__PURE__ */ pipe(/* @__PURE__ */ string(), /* @__PURE__ */ description("URL to redirect to after sign-in")))
698
+ })
699
+ }, async (ctx) => {
700
+ const { handle, callbackURL } = ctx.body;
701
+ if (!handle || handle.length < 3) throw APIError.from("BAD_REQUEST", ATPROTO_ERROR_CODES.INVALID_HANDLE);
702
+ try {
703
+ const identifier = handle;
704
+ const result = await oauthClient.authorize({
705
+ target: {
706
+ type: "account",
707
+ identifier
708
+ },
709
+ state: callbackURL ? JSON.stringify({ callbackURL }) : void 0
710
+ });
711
+ return ctx.json({
712
+ url: result.url.toString(),
713
+ redirect: true
714
+ });
715
+ } catch (e) {
716
+ console.error("[atproto] authorize failed:", e);
717
+ throw APIError.from("INTERNAL_SERVER_ERROR", ATPROTO_ERROR_CODES.AUTHORIZATION_FAILED);
718
+ }
719
+ }),
720
+ atprotoCallback: createAuthEndpoint(callbackPath, {
721
+ method: "GET",
722
+ query: /* @__PURE__ */ object({
723
+ code: /* @__PURE__ */ optional(/* @__PURE__ */ string()),
724
+ state: /* @__PURE__ */ optional(/* @__PURE__ */ string()),
725
+ iss: /* @__PURE__ */ optional(/* @__PURE__ */ string()),
726
+ error: /* @__PURE__ */ optional(/* @__PURE__ */ string()),
727
+ error_description: /* @__PURE__ */ optional(/* @__PURE__ */ string())
728
+ })
729
+ }, async (ctx) => {
730
+ if (ctx.query.error) {
731
+ const errorUrl = `${ctx.context.baseURL}/error?error=${ctx.query.error}`;
732
+ throw ctx.redirect(errorUrl);
733
+ }
734
+ try {
735
+ const params = new URLSearchParams();
736
+ if (ctx.query.code) params.set("code", ctx.query.code);
737
+ if (ctx.query.state) params.set("state", ctx.query.state);
738
+ if (ctx.query.iss) params.set("iss", ctx.query.iss);
739
+ const { session: oauthSession, state: userState } = await oauthClient.callback(params);
740
+ const did = oauthSession.did;
741
+ const pdsUrl = (await oauthSession.getTokenInfo(false)).aud;
742
+ const profile = await fetchAtprotoProfile(oauthClient, did);
743
+ const mappedFields = options.mapProfileToUser ? options.mapProfileToUser(profile) : {
744
+ name: profile.displayName || profile.handle,
745
+ image: profile.avatar
746
+ };
747
+ const email = mappedFields.email || atprotoPlaceholderEmail(did);
748
+ const existingAccount = await ctx.context.internalAdapter.findAccountByProviderId(did, "atproto");
749
+ let userId;
750
+ if (existingAccount) {
751
+ userId = existingAccount.userId;
752
+ await ctx.context.internalAdapter.updateUser(userId, {
753
+ name: mappedFields.name,
754
+ image: mappedFields.image,
755
+ atprotoDid: did,
756
+ atprotoHandle: profile.handle
757
+ });
758
+ } else {
759
+ const { getSessionFromCtx } = await import("better-auth/api");
760
+ const currentSession = await getSessionFromCtx(ctx).catch(() => null);
761
+ if (currentSession) {
762
+ if (!(ctx.context.options.account?.accountLinking?.enabled !== false)) throw APIError.from("FORBIDDEN", ATPROTO_ERROR_CODES.ACCOUNT_LINKING_DISABLED);
763
+ await ctx.context.internalAdapter.linkAccount({
764
+ userId: currentSession.user.id,
765
+ providerId: "atproto",
766
+ accountId: did,
767
+ accessToken: "atproto-session",
768
+ refreshToken: "atproto-session",
769
+ scope: normalizeScope(options.scope)
770
+ });
771
+ userId = currentSession.user.id;
772
+ await ctx.context.internalAdapter.updateUser(userId, {
773
+ atprotoDid: did,
774
+ atprotoHandle: profile.handle,
775
+ ...mappedFields.image ? { image: mappedFields.image } : {}
776
+ });
777
+ } else {
778
+ if (options.disableSignUp) throw APIError.from("FORBIDDEN", ATPROTO_ERROR_CODES.SIGNUP_DISABLED);
779
+ const newUser = await ctx.context.internalAdapter.createUser({
780
+ name: mappedFields.name || profile.handle,
781
+ email,
782
+ emailVerified: false,
783
+ image: mappedFields.image || null,
784
+ atprotoDid: did,
785
+ atprotoHandle: profile.handle,
786
+ createdAt: /* @__PURE__ */ new Date(),
787
+ updatedAt: /* @__PURE__ */ new Date()
788
+ });
789
+ await ctx.context.internalAdapter.createAccount({
790
+ userId: newUser.id,
791
+ providerId: "atproto",
792
+ accountId: did,
793
+ accessToken: "atproto-session",
794
+ refreshToken: "atproto-session",
795
+ scope: normalizeScope(options.scope)
796
+ });
797
+ userId = newUser.id;
798
+ }
799
+ }
800
+ if (await ctx.context.adapter.findOne({
801
+ model: "atprotoSession",
802
+ where: [{
803
+ field: "did",
804
+ value: did
805
+ }]
806
+ })) await ctx.context.adapter.update({
807
+ model: "atprotoSession",
808
+ where: [{
809
+ field: "did",
810
+ value: did
811
+ }],
812
+ update: {
813
+ userId,
814
+ handle: profile.handle,
815
+ pdsUrl,
816
+ updatedAt: /* @__PURE__ */ new Date()
817
+ }
818
+ });
819
+ else await ctx.context.adapter.create({
820
+ model: "atprotoSession",
821
+ data: {
822
+ did,
823
+ sessionData: "{}",
824
+ userId,
825
+ handle: profile.handle,
826
+ pdsUrl,
827
+ updatedAt: /* @__PURE__ */ new Date()
828
+ }
829
+ });
830
+ const foundUser = await ctx.context.internalAdapter.findUserById(userId);
831
+ if (!foundUser) throw APIError.from("INTERNAL_SERVER_ERROR", ATPROTO_ERROR_CODES.CALLBACK_FAILED);
832
+ await setSessionCookie(ctx, {
833
+ session: await ctx.context.internalAdapter.createSession(userId),
834
+ user: foundUser
835
+ });
836
+ let callbackURL = "/";
837
+ if (userState) try {
838
+ const parsed = typeof userState === "string" ? JSON.parse(userState) : userState;
839
+ if (parsed.callbackURL && typeof parsed.callbackURL === "string") callbackURL = parsed.callbackURL;
840
+ } catch {}
841
+ if (!callbackURL.startsWith("/") || callbackURL.startsWith("//")) callbackURL = "/";
842
+ throw ctx.redirect(callbackURL);
843
+ } catch (e) {
844
+ if (e && typeof e === "object" && ("statusCode" in e || "status" in e)) throw e;
845
+ if (e instanceof OAuthCallbackError) {
846
+ const errorUrl = `${ctx.context.baseURL}/error?error=${e.error}`;
847
+ throw ctx.redirect(errorUrl);
848
+ }
849
+ throw APIError.from("INTERNAL_SERVER_ERROR", ATPROTO_ERROR_CODES.CALLBACK_FAILED);
850
+ }
851
+ }),
852
+ atprotoGetSession: createAuthEndpoint("/atproto/session", { method: "GET" }, async (ctx) => {
853
+ const { getSessionFromCtx } = await import("better-auth/api");
854
+ const currentSession = await getSessionFromCtx(ctx);
855
+ if (!currentSession) throw APIError.fromStatus("UNAUTHORIZED", { message: "Not authenticated" });
856
+ const atprotoSession = await ctx.context.adapter.findOne({
857
+ model: "atprotoSession",
858
+ where: [{
859
+ field: "userId",
860
+ value: currentSession.user.id
861
+ }]
862
+ });
863
+ if (!atprotoSession) throw APIError.from("NOT_FOUND", ATPROTO_ERROR_CODES.SESSION_NOT_FOUND);
864
+ const user = currentSession.user;
865
+ return ctx.json({
866
+ did: atprotoSession.did,
867
+ handle: atprotoSession.handle,
868
+ pdsUrl: atprotoSession.pdsUrl,
869
+ atprotoDid: getString(user, "atprotoDid") ?? null,
870
+ atprotoHandle: getString(user, "atprotoHandle") ?? null
871
+ });
872
+ }),
873
+ atprotoRestore: createAuthEndpoint("/atproto/restore", { method: "POST" }, async (ctx) => {
874
+ const { getSessionFromCtx } = await import("better-auth/api");
875
+ const currentSession = await getSessionFromCtx(ctx);
876
+ if (!currentSession) throw APIError.fromStatus("UNAUTHORIZED", { message: "Not authenticated" });
877
+ const atprotoSession = await ctx.context.adapter.findOne({
878
+ model: "atprotoSession",
879
+ where: [{
880
+ field: "userId",
881
+ value: currentSession.user.id
882
+ }]
883
+ });
884
+ if (!atprotoSession) return ctx.json({ active: false });
885
+ try {
886
+ const validDid = toDid(atprotoSession.did);
887
+ if (!validDid) return ctx.json({ active: false });
888
+ await oauthClient.restore(validDid);
889
+ return ctx.json({
890
+ active: true,
891
+ did: atprotoSession.did,
892
+ handle: atprotoSession.handle
893
+ });
894
+ } catch {
895
+ return ctx.json({ active: false });
896
+ }
897
+ }),
898
+ atprotoSignOut: createAuthEndpoint("/atproto/sign-out", { method: "POST" }, async (ctx) => {
899
+ const { getSessionFromCtx } = await import("better-auth/api");
900
+ const currentSession = await getSessionFromCtx(ctx);
901
+ if (!currentSession) throw APIError.fromStatus("UNAUTHORIZED", { message: "Not authenticated" });
902
+ const atprotoSession = await ctx.context.adapter.findOne({
903
+ model: "atprotoSession",
904
+ where: [{
905
+ field: "userId",
906
+ value: currentSession.user.id
907
+ }]
908
+ });
909
+ if (atprotoSession) try {
910
+ const validDid = toDid(atprotoSession.did);
911
+ if (validDid) await oauthClient.revoke(validDid);
912
+ } catch {}
913
+ return ctx.json({ success: true });
914
+ }),
915
+ getAtprotoClient: createAuthEndpoint({
916
+ method: "POST",
917
+ body: /* @__PURE__ */ object({
918
+ did: /* @__PURE__ */ optional(/* @__PURE__ */ string()),
919
+ userId: /* @__PURE__ */ optional(/* @__PURE__ */ string())
920
+ }),
921
+ metadata: { SERVER_ONLY: true }
922
+ }, async (ctx) => {
923
+ const { did: inputDid, userId } = ctx.body;
924
+ let did = inputDid;
925
+ if (!did && userId) {
926
+ const atprotoSession = await ctx.context.adapter.findOne({
927
+ model: "atprotoSession",
928
+ where: [{
929
+ field: "userId",
930
+ value: userId
931
+ }]
932
+ });
933
+ if (atprotoSession) did = atprotoSession.did;
934
+ }
935
+ if (!did) throw APIError.from("BAD_REQUEST", ATPROTO_ERROR_CODES.SESSION_NOT_FOUND);
936
+ const validDid = toDid(did);
937
+ if (!validDid) throw APIError.from("BAD_REQUEST", ATPROTO_ERROR_CODES.INVALID_HANDLE);
938
+ const oauthSession = await oauthClient.restore(validDid);
939
+ return {
940
+ client: new Client({ handler: oauthSession }),
941
+ session: oauthSession
942
+ };
943
+ })
944
+ },
945
+ $ERROR_CODES: ATPROTO_ERROR_CODES
946
+ };
947
+ };
948
+ //#endregion
949
+ export { DbStateStore as a, DbSessionStore as i, atprotoPlaceholderEmail as n, atprotoSchema as o, fetchAtprotoProfilePublic as r, atproto as t };
950
+
951
+ //# sourceMappingURL=server-DS4UMolW.js.map