@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,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
|