@draftlab/auth 0.0.1
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/dist/adapters/node.d.ts +18 -0
- package/dist/adapters/node.js +71 -0
- package/dist/allow-CixonwTW.d.ts +59 -0
- package/dist/allow-DX5cehSc.js +63 -0
- package/dist/allow.d.ts +2 -0
- package/dist/allow.js +4 -0
- package/dist/base-DRutbxgL.js +422 -0
- package/dist/client.d.ts +413 -0
- package/dist/client.js +209 -0
- package/dist/code-l_uvMR1j.d.ts +212 -0
- package/dist/core-8WTqfnb4.d.ts +129 -0
- package/dist/core-CncE5rPg.js +498 -0
- package/dist/core.d.ts +9 -0
- package/dist/core.js +14 -0
- package/dist/error-CWAdNAzm.d.ts +243 -0
- package/dist/error-DgAKK7b2.js +237 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.js +3 -0
- package/dist/form-6XKM_cOk.js +61 -0
- package/dist/icon-Ci5uqGB_.js +192 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/keys-EEfxEGfO.js +140 -0
- package/dist/keys.d.ts +67 -0
- package/dist/keys.js +5 -0
- package/dist/oauth2-B7-6Z7Lc.js +155 -0
- package/dist/oauth2-DtKwtl8p.d.ts +176 -0
- package/dist/password-Cm0dRMwa.d.ts +385 -0
- package/dist/pkce-276Za_rZ.js +162 -0
- package/dist/pkce.d.ts +72 -0
- package/dist/pkce.js +3 -0
- package/dist/provider/code.d.ts +4 -0
- package/dist/provider/code.js +145 -0
- package/dist/provider/facebook.d.ts +137 -0
- package/dist/provider/facebook.js +85 -0
- package/dist/provider/github.d.ts +141 -0
- package/dist/provider/github.js +88 -0
- package/dist/provider/google.d.ts +113 -0
- package/dist/provider/google.js +62 -0
- package/dist/provider/oauth2.d.ts +4 -0
- package/dist/provider/oauth2.js +7 -0
- package/dist/provider/password.d.ts +4 -0
- package/dist/provider/password.js +366 -0
- package/dist/provider/provider.d.ts +3 -0
- package/dist/provider/provider.js +44 -0
- package/dist/provider-CwWMG-1l.d.ts +227 -0
- package/dist/random-SXMYlaVr.js +87 -0
- package/dist/random.d.ts +66 -0
- package/dist/random.js +3 -0
- package/dist/select-BjySLL8I.js +280 -0
- package/dist/storage/memory.d.ts +82 -0
- package/dist/storage/memory.js +127 -0
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +3 -0
- package/dist/storage/turso.d.ts +31 -0
- package/dist/storage/turso.js +117 -0
- package/dist/storage/unstorage.d.ts +38 -0
- package/dist/storage/unstorage.js +97 -0
- package/dist/storage-BEaqEPNQ.js +62 -0
- package/dist/storage-CxKerLlc.d.ts +162 -0
- package/dist/subject-DiQdRWGt.d.ts +62 -0
- package/dist/subject.d.ts +3 -0
- package/dist/subject.js +36 -0
- package/dist/theme-C9by7VXf.d.ts +209 -0
- package/dist/theme-CswaLtbW.js +120 -0
- package/dist/themes/theme.d.ts +2 -0
- package/dist/themes/theme.js +3 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +0 -0
- package/dist/ui/base.d.ts +43 -0
- package/dist/ui/base.js +4 -0
- package/dist/ui/code.d.ts +158 -0
- package/dist/ui/code.js +197 -0
- package/dist/ui/form.d.ts +31 -0
- package/dist/ui/form.js +3 -0
- package/dist/ui/icon.d.ts +98 -0
- package/dist/ui/icon.js +3 -0
- package/dist/ui/password.d.ts +54 -0
- package/dist/ui/password.js +300 -0
- package/dist/ui/select.d.ts +233 -0
- package/dist/ui/select.js +6 -0
- package/dist/util-CSdHUFOo.js +108 -0
- package/dist/util-ChlgVqPN.d.ts +72 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +3 -0
- package/package.json +63 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { getRelativeUrl } from "../util-CSdHUFOo.js";
|
|
2
|
+
import { UnknownStateError } from "../error-DgAKK7b2.js";
|
|
3
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random-SXMYlaVr.js";
|
|
4
|
+
import { Storage } from "../storage-BEaqEPNQ.js";
|
|
5
|
+
import * as jose from "jose";
|
|
6
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
7
|
+
import { TextEncoder } from "node:util";
|
|
8
|
+
|
|
9
|
+
//#region src/provider/password.ts
|
|
10
|
+
/**
|
|
11
|
+
* Creates a password authentication provider with email verification.
|
|
12
|
+
* Implements secure registration, login, and password change flows.
|
|
13
|
+
*
|
|
14
|
+
* @param config - Provider configuration including UI handlers and email service
|
|
15
|
+
* @returns Provider instance implementing password authentication
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const provider = PasswordProvider({
|
|
20
|
+
* login: async (req, form, error) => {
|
|
21
|
+
* return new Response(renderLogin(form, error))
|
|
22
|
+
* },
|
|
23
|
+
* register: async (req, state, form, error) => {
|
|
24
|
+
* return new Response(renderRegister(state, form, error))
|
|
25
|
+
* },
|
|
26
|
+
* change: async (req, state, form, error) => {
|
|
27
|
+
* return new Response(renderChange(state, form, error))
|
|
28
|
+
* },
|
|
29
|
+
* sendCode: async (email, code) => {
|
|
30
|
+
* await emailService.send(email, `Code: ${code}`)
|
|
31
|
+
* },
|
|
32
|
+
* validatePassword: (pwd) => {
|
|
33
|
+
* return pwd.length >= 8 ? undefined : "Too short"
|
|
34
|
+
* }
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
const PasswordProvider = (config) => {
|
|
39
|
+
const hasher = config.hasher ?? ScryptHasher();
|
|
40
|
+
/**
|
|
41
|
+
* Generates a cryptographically secure verification code.
|
|
42
|
+
*/
|
|
43
|
+
const generateCode = () => {
|
|
44
|
+
return generateUnbiasedDigits(config.length ?? 6);
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
type: "password",
|
|
48
|
+
init(routes, ctx) {
|
|
49
|
+
/**
|
|
50
|
+
* GET /authorize - Display login form
|
|
51
|
+
*/
|
|
52
|
+
routes.get("/authorize", async (c) => ctx.forward(c, await config.login(c.request)));
|
|
53
|
+
/**
|
|
54
|
+
* POST /authorize - Process login attempt
|
|
55
|
+
*/
|
|
56
|
+
routes.post("/authorize", async (c) => {
|
|
57
|
+
const formData = await c.formData();
|
|
58
|
+
const error = async (err) => {
|
|
59
|
+
return ctx.forward(c, await config.login(c.request, formData, err));
|
|
60
|
+
};
|
|
61
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
62
|
+
if (!email) return error({ type: "invalid_email" });
|
|
63
|
+
const storedHash = await Storage.get(ctx.storage, [
|
|
64
|
+
"email",
|
|
65
|
+
email,
|
|
66
|
+
"password"
|
|
67
|
+
]);
|
|
68
|
+
const password = formData.get("password")?.toString();
|
|
69
|
+
if (!(password && storedHash && await hasher.verify(password, storedHash))) return error({ type: "invalid_password" });
|
|
70
|
+
return ctx.success(c, { email }, { invalidate: async (subject) => {
|
|
71
|
+
await Storage.set(ctx.storage, [
|
|
72
|
+
"email",
|
|
73
|
+
email,
|
|
74
|
+
"subject"
|
|
75
|
+
], subject);
|
|
76
|
+
} });
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* GET /register - Display registration form
|
|
80
|
+
*/
|
|
81
|
+
routes.get("/register", async (c) => {
|
|
82
|
+
const state = { type: "start" };
|
|
83
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
84
|
+
return ctx.forward(c, await config.register(c.request, state));
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* POST /register - Process registration steps
|
|
88
|
+
*/
|
|
89
|
+
routes.post("/register", async (c) => {
|
|
90
|
+
const formData = await c.formData();
|
|
91
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
92
|
+
const action = formData.get("action")?.toString();
|
|
93
|
+
let provider = await ctx.get(c, "provider");
|
|
94
|
+
if (!provider) {
|
|
95
|
+
const state = { type: "start" };
|
|
96
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
97
|
+
if (action === "register") provider = state;
|
|
98
|
+
else return ctx.forward(c, await config.register(c.request, state));
|
|
99
|
+
}
|
|
100
|
+
const transition = async (next, err) => {
|
|
101
|
+
await ctx.set(c, "provider", 60 * 60 * 24, next);
|
|
102
|
+
return ctx.forward(c, await config.register(c.request, next, formData, err));
|
|
103
|
+
};
|
|
104
|
+
if (action === "register" && provider.type === "start") {
|
|
105
|
+
const password = formData.get("password")?.toString();
|
|
106
|
+
const repeat = formData.get("repeat")?.toString();
|
|
107
|
+
if (!email) return transition(provider, { type: "invalid_email" });
|
|
108
|
+
if (!password) return transition(provider, { type: "invalid_password" });
|
|
109
|
+
if (password !== repeat) return transition(provider, { type: "password_mismatch" });
|
|
110
|
+
if (config.validatePassword) {
|
|
111
|
+
let validationError;
|
|
112
|
+
try {
|
|
113
|
+
if (typeof config.validatePassword === "function") validationError = await config.validatePassword(password);
|
|
114
|
+
else {
|
|
115
|
+
const result = await config.validatePassword["~standard"].validate(password);
|
|
116
|
+
if (result.issues?.length) throw new Error(result.issues.map((issue) => issue.message).join(", "));
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
validationError = error instanceof Error ? error.message : void 0;
|
|
120
|
+
}
|
|
121
|
+
if (validationError) return transition(provider, {
|
|
122
|
+
type: "validation_error",
|
|
123
|
+
message: validationError
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const existingUser = await Storage.get(ctx.storage, [
|
|
127
|
+
"email",
|
|
128
|
+
email,
|
|
129
|
+
"password"
|
|
130
|
+
]);
|
|
131
|
+
if (existingUser) return transition(provider, { type: "email_taken" });
|
|
132
|
+
const code = generateCode();
|
|
133
|
+
await config.sendCode(email, code);
|
|
134
|
+
return transition({
|
|
135
|
+
type: "code",
|
|
136
|
+
code,
|
|
137
|
+
password: await hasher.hash(password),
|
|
138
|
+
email
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (action === "register" && provider.type === "code") {
|
|
142
|
+
const code = generateCode();
|
|
143
|
+
await config.sendCode(provider.email, code);
|
|
144
|
+
return transition({
|
|
145
|
+
type: "code",
|
|
146
|
+
code,
|
|
147
|
+
password: provider.password,
|
|
148
|
+
email: provider.email
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (action === "verify" && provider.type === "code") {
|
|
152
|
+
const code = formData.get("code")?.toString();
|
|
153
|
+
if (!(code && timingSafeCompare(code, provider.code))) return transition(provider, { type: "invalid_code" });
|
|
154
|
+
const existingUser = await Storage.get(ctx.storage, [
|
|
155
|
+
"email",
|
|
156
|
+
provider.email,
|
|
157
|
+
"password"
|
|
158
|
+
]);
|
|
159
|
+
if (existingUser) return transition({ type: "start" }, { type: "email_taken" });
|
|
160
|
+
await Storage.set(ctx.storage, [
|
|
161
|
+
"email",
|
|
162
|
+
provider.email,
|
|
163
|
+
"password"
|
|
164
|
+
], provider.password);
|
|
165
|
+
return ctx.success(c, { email: provider.email });
|
|
166
|
+
}
|
|
167
|
+
return transition({ type: "start" });
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* GET /change - Display password change form
|
|
171
|
+
*/
|
|
172
|
+
routes.get("/change", async (c) => {
|
|
173
|
+
const redirect = c.query("redirect_uri") || getRelativeUrl(c, "/authorize");
|
|
174
|
+
const state = {
|
|
175
|
+
type: "start",
|
|
176
|
+
redirect
|
|
177
|
+
};
|
|
178
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
179
|
+
return ctx.forward(c, await config.change(c.request, state));
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* POST /change - Process password change steps
|
|
183
|
+
*/
|
|
184
|
+
routes.post("/change", async (c) => {
|
|
185
|
+
const formData = await c.formData();
|
|
186
|
+
const action = formData.get("action")?.toString();
|
|
187
|
+
const provider = await ctx.get(c, "provider");
|
|
188
|
+
if (!provider) throw new UnknownStateError();
|
|
189
|
+
const transition = async (next, err) => {
|
|
190
|
+
await ctx.set(c, "provider", 60 * 60 * 24, next);
|
|
191
|
+
return ctx.forward(c, await config.change(c.request, next, formData, err));
|
|
192
|
+
};
|
|
193
|
+
if (action === "code") {
|
|
194
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
195
|
+
if (!email) return transition({
|
|
196
|
+
type: "start",
|
|
197
|
+
redirect: provider.redirect
|
|
198
|
+
}, { type: "invalid_email" });
|
|
199
|
+
const code = generateCode();
|
|
200
|
+
await config.sendCode(email, code);
|
|
201
|
+
return transition({
|
|
202
|
+
type: "code",
|
|
203
|
+
code,
|
|
204
|
+
email,
|
|
205
|
+
redirect: provider.redirect
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (action === "verify" && provider.type === "code") {
|
|
209
|
+
const code = formData.get("code")?.toString();
|
|
210
|
+
if (!(code && timingSafeCompare(code, provider.code))) return transition(provider, { type: "invalid_code" });
|
|
211
|
+
return transition({
|
|
212
|
+
type: "update",
|
|
213
|
+
email: provider.email,
|
|
214
|
+
redirect: provider.redirect
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (action === "update" && provider.type === "update") {
|
|
218
|
+
const existingPassword = await Storage.get(ctx.storage, [
|
|
219
|
+
"email",
|
|
220
|
+
provider.email,
|
|
221
|
+
"password"
|
|
222
|
+
]);
|
|
223
|
+
if (!existingPassword) return c.redirect(provider.redirect, 302);
|
|
224
|
+
const password = formData.get("password")?.toString();
|
|
225
|
+
const repeat = formData.get("repeat")?.toString();
|
|
226
|
+
if (!password) return transition(provider, { type: "invalid_password" });
|
|
227
|
+
if (password !== repeat) return transition(provider, { type: "password_mismatch" });
|
|
228
|
+
if (config.validatePassword) {
|
|
229
|
+
let validationError;
|
|
230
|
+
try {
|
|
231
|
+
if (typeof config.validatePassword === "function") validationError = await config.validatePassword(password);
|
|
232
|
+
else {
|
|
233
|
+
const result = await config.validatePassword["~standard"].validate(password);
|
|
234
|
+
if (result.issues?.length) throw new Error(result.issues.map((issue) => issue.message).join(", "));
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
validationError = error instanceof Error ? error.message : void 0;
|
|
238
|
+
}
|
|
239
|
+
if (validationError) return transition(provider, {
|
|
240
|
+
type: "validation_error",
|
|
241
|
+
message: validationError
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
await Storage.set(ctx.storage, [
|
|
245
|
+
"email",
|
|
246
|
+
provider.email,
|
|
247
|
+
"password"
|
|
248
|
+
], await hasher.hash(password));
|
|
249
|
+
const subject = await Storage.get(ctx.storage, [
|
|
250
|
+
"email",
|
|
251
|
+
provider.email,
|
|
252
|
+
"subject"
|
|
253
|
+
]);
|
|
254
|
+
if (subject) await ctx.invalidate(subject);
|
|
255
|
+
return c.redirect(provider.redirect, 302);
|
|
256
|
+
}
|
|
257
|
+
return transition({
|
|
258
|
+
type: "start",
|
|
259
|
+
redirect: provider.redirect
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* PBKDF2 password hasher with configurable iterations.
|
|
267
|
+
* Good choice for compatibility but slower than Scrypt.
|
|
268
|
+
*
|
|
269
|
+
* @param opts - Configuration options
|
|
270
|
+
* @returns Password hasher using PBKDF2 algorithm
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
const PBKDF2Hasher = (opts) => {
|
|
274
|
+
const iterations = opts?.iterations ?? 6e5;
|
|
275
|
+
return {
|
|
276
|
+
async hash(password) {
|
|
277
|
+
const encoder = new TextEncoder();
|
|
278
|
+
const passwordBytes = encoder.encode(password);
|
|
279
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
280
|
+
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
281
|
+
const hashBuffer = await crypto.subtle.deriveBits({
|
|
282
|
+
name: "PBKDF2",
|
|
283
|
+
hash: "SHA-256",
|
|
284
|
+
salt,
|
|
285
|
+
iterations
|
|
286
|
+
}, keyMaterial, 256);
|
|
287
|
+
const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
|
|
288
|
+
const saltBase64 = jose.base64url.encode(salt);
|
|
289
|
+
return {
|
|
290
|
+
hash: hashBase64,
|
|
291
|
+
salt: saltBase64,
|
|
292
|
+
iterations
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
async verify(password, compare) {
|
|
296
|
+
const encoder = new TextEncoder();
|
|
297
|
+
const passwordBytes = encoder.encode(password);
|
|
298
|
+
const salt = jose.base64url.decode(compare.salt);
|
|
299
|
+
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
300
|
+
const hashBuffer = await crypto.subtle.deriveBits({
|
|
301
|
+
name: "PBKDF2",
|
|
302
|
+
hash: "SHA-256",
|
|
303
|
+
salt,
|
|
304
|
+
iterations: compare.iterations
|
|
305
|
+
}, keyMaterial, 256);
|
|
306
|
+
const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
|
|
307
|
+
return timingSafeCompare(hashBase64, compare.hash);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Scrypt password hasher with secure defaults.
|
|
313
|
+
* Recommended choice for new applications due to memory-hard properties.
|
|
314
|
+
*
|
|
315
|
+
* @param opts - Scrypt parameters (N, r, p)
|
|
316
|
+
* @returns Password hasher using Scrypt algorithm
|
|
317
|
+
* @internal
|
|
318
|
+
*/
|
|
319
|
+
const ScryptHasher = (opts) => {
|
|
320
|
+
const N = opts?.N ?? 16384;
|
|
321
|
+
const r = opts?.r ?? 8;
|
|
322
|
+
const p = opts?.p ?? 1;
|
|
323
|
+
return {
|
|
324
|
+
async hash(password) {
|
|
325
|
+
const salt = randomBytes(16);
|
|
326
|
+
const keyLength = 32;
|
|
327
|
+
const derivedKey = await new Promise((resolve, reject) => {
|
|
328
|
+
scrypt(password, salt, keyLength, {
|
|
329
|
+
N,
|
|
330
|
+
r,
|
|
331
|
+
p
|
|
332
|
+
}, (err, derivedKey$1) => {
|
|
333
|
+
if (err) reject(err);
|
|
334
|
+
else resolve(derivedKey$1);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
const hashBase64 = derivedKey.toString("base64");
|
|
338
|
+
const saltBase64 = salt.toString("base64");
|
|
339
|
+
return {
|
|
340
|
+
hash: hashBase64,
|
|
341
|
+
salt: saltBase64,
|
|
342
|
+
N,
|
|
343
|
+
r,
|
|
344
|
+
p
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
async verify(password, compare) {
|
|
348
|
+
const salt = Buffer.from(compare.salt, "base64");
|
|
349
|
+
const keyLength = 32;
|
|
350
|
+
const derivedKey = await new Promise((resolve, reject) => {
|
|
351
|
+
scrypt(password, salt, keyLength, {
|
|
352
|
+
N: compare.N,
|
|
353
|
+
r: compare.r,
|
|
354
|
+
p: compare.p
|
|
355
|
+
}, (err, derivedKey$1) => {
|
|
356
|
+
if (err) reject(err);
|
|
357
|
+
else resolve(derivedKey$1);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
export { PBKDF2Hasher, PasswordProvider, ScryptHasher };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/provider/provider.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base error class for provider-related errors.
|
|
4
|
+
* Extend this class to create specific provider error types.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* export class GitHubApiError extends ProviderError {
|
|
9
|
+
* constructor(message: string, public readonly statusCode: number) {
|
|
10
|
+
* super(message)
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
var ProviderError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ProviderError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when a provider encounters an unknown or unexpected error.
|
|
23
|
+
* Used as a fallback for unhandled error conditions.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* catch (error) {
|
|
28
|
+
* if (error instanceof SomeSpecificError) {
|
|
29
|
+
* // Handle specific error
|
|
30
|
+
* } else {
|
|
31
|
+
* throw new ProviderUnknownError(`Unexpected error: ${error}`)
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
var ProviderUnknownError = class extends ProviderError {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message || "An unknown provider error occurred");
|
|
39
|
+
this.name = "ProviderUnknownError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ProviderError, ProviderUnknownError };
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { StorageAdapter } from "./storage-CxKerLlc.js";
|
|
2
|
+
import { Router } from "@draftlab/router";
|
|
3
|
+
import { RouterContext } from "@draftlab/router/types";
|
|
4
|
+
|
|
5
|
+
//#region src/provider/provider.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OAuth provider system for Draft Auth.
|
|
9
|
+
* Defines the interfaces and utilities for implementing authentication providers
|
|
10
|
+
* that integrate with various OAuth 2.0 services.
|
|
11
|
+
*
|
|
12
|
+
* ## Creating a Provider
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export const MyProvider = (config: MyConfig): Provider<MyUserData> => ({
|
|
16
|
+
* type: "my-provider",
|
|
17
|
+
*
|
|
18
|
+
* init(routes, ctx) {
|
|
19
|
+
* routes.get("/authorize", async (c) => {
|
|
20
|
+
* // Redirect to provider's auth URL
|
|
21
|
+
* return c.redirect(authUrl)
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* routes.get("/callback", async (c) => {
|
|
25
|
+
* // Handle callback and extract user data
|
|
26
|
+
* const userData = await processCallback(c)
|
|
27
|
+
* return await ctx.success(c, userData)
|
|
28
|
+
* })
|
|
29
|
+
* }
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* ## Using Providers
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* export default issuer({
|
|
37
|
+
* providers: {
|
|
38
|
+
* github: GithubProvider({ ... }),
|
|
39
|
+
* google: GoogleProvider({ ... })
|
|
40
|
+
* }
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Router instance used for provider route definitions.
|
|
46
|
+
* Providers use this to register their authorization and callback endpoints.
|
|
47
|
+
*/
|
|
48
|
+
type ProviderRoute = Router;
|
|
49
|
+
/**
|
|
50
|
+
* Authentication provider interface that handles OAuth flows.
|
|
51
|
+
* Each provider implements authentication with a specific service (GitHub, Google, etc.).
|
|
52
|
+
*
|
|
53
|
+
* @template Properties - Type of user data returned by successful authentication
|
|
54
|
+
*/
|
|
55
|
+
interface Provider<Properties = Record<string, unknown>> {
|
|
56
|
+
/**
|
|
57
|
+
* Unique identifier for this provider type.
|
|
58
|
+
* Used in URLs and provider selection UI.
|
|
59
|
+
*
|
|
60
|
+
* @example "github", "google", "steam"
|
|
61
|
+
*/
|
|
62
|
+
readonly type: string;
|
|
63
|
+
/**
|
|
64
|
+
* Initializes the provider by registering required routes.
|
|
65
|
+
* Called during issuer setup to configure authorization and callback endpoints.
|
|
66
|
+
*
|
|
67
|
+
* @param route - Router instance for registering provider endpoints
|
|
68
|
+
* @param options - Provider utilities and configuration
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* init(routes, ctx) {
|
|
73
|
+
* routes.get("/authorize", async (c) => {
|
|
74
|
+
* // Redirect to OAuth provider
|
|
75
|
+
* return c.redirect(buildAuthUrl())
|
|
76
|
+
* })
|
|
77
|
+
*
|
|
78
|
+
* routes.get("/callback", async (c) => {
|
|
79
|
+
* // Process callback and return user data
|
|
80
|
+
* const userData = await handleCallback(c)
|
|
81
|
+
* return await ctx.success(c, userData)
|
|
82
|
+
* })
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
init: (route: ProviderRoute, options: ProviderOptions<Properties>) => void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Utilities and callbacks provided to providers during initialization.
|
|
90
|
+
* Contains methods for state management, user flow completion, and storage access.
|
|
91
|
+
*
|
|
92
|
+
* @template Properties - Type of user data handled by the provider
|
|
93
|
+
*/
|
|
94
|
+
interface ProviderOptions<Properties> {
|
|
95
|
+
/**
|
|
96
|
+
* Name of the provider instance as configured in the issuer.
|
|
97
|
+
* Corresponds to the key used in the providers object.
|
|
98
|
+
*/
|
|
99
|
+
readonly name: string;
|
|
100
|
+
/**
|
|
101
|
+
* Completes the authentication flow with user data.
|
|
102
|
+
* Called when the provider successfully authenticates a user.
|
|
103
|
+
*
|
|
104
|
+
* @param ctx - Router request context
|
|
105
|
+
* @param properties - User data extracted from the provider
|
|
106
|
+
* @param opts - Optional utilities for session management
|
|
107
|
+
* @returns Response that completes the OAuth flow
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const userData = { userId: "123", email: "user@example.com" }
|
|
112
|
+
* return await ctx.success(c, userData)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
success: (ctx: RouterContext, properties: Properties, opts?: {
|
|
116
|
+
/** Function to invalidate existing user sessions */
|
|
117
|
+
readonly invalidate?: (subject: string) => Promise<void>;
|
|
118
|
+
}) => Promise<Response>;
|
|
119
|
+
/**
|
|
120
|
+
* Forwards a response through the provider context.
|
|
121
|
+
* Used for redirects and custom responses within the OAuth flow.
|
|
122
|
+
*
|
|
123
|
+
* @param ctx - Router request context
|
|
124
|
+
* @param response - Response to forward
|
|
125
|
+
* @returns Forwarded response
|
|
126
|
+
*/
|
|
127
|
+
forward: (ctx: RouterContext, response: Response) => Response;
|
|
128
|
+
/**
|
|
129
|
+
* Stores a temporary value with expiration for the current session.
|
|
130
|
+
* Useful for storing OAuth state, PKCE verifiers, and other temporary data.
|
|
131
|
+
*
|
|
132
|
+
* @param ctx - Router request context
|
|
133
|
+
* @param key - Storage key identifier
|
|
134
|
+
* @param maxAge - TTL in seconds
|
|
135
|
+
* @param value - Value to store
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* // Store OAuth state for 10 minutes
|
|
140
|
+
* await ctx.set(c, "oauth_state", 600, { state, redirectUri })
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
set: <T>(ctx: RouterContext, key: string, maxAge: number, value: T) => Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Retrieves a previously stored temporary value.
|
|
146
|
+
*
|
|
147
|
+
* @param ctx - Router request context
|
|
148
|
+
* @param key - Storage key identifier
|
|
149
|
+
* @returns Promise resolving to the stored value or undefined if not found/expired
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const oauthState = await ctx.get<OAuthState>(c, "oauth_state")
|
|
154
|
+
* if (!oauthState) {
|
|
155
|
+
* throw new Error("OAuth state expired")
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
get: <T>(ctx: RouterContext, key: string) => Promise<T | undefined>;
|
|
160
|
+
/**
|
|
161
|
+
* Removes a stored temporary value.
|
|
162
|
+
*
|
|
163
|
+
* @param ctx - Router request context
|
|
164
|
+
* @param key - Storage key identifier
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* // Clean up OAuth state after use
|
|
169
|
+
* await ctx.unset(c, "oauth_state")
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
unset: (ctx: RouterContext, key: string) => Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Invalidates all sessions for a given subject (user).
|
|
175
|
+
* Forces logout across all devices and applications.
|
|
176
|
+
*
|
|
177
|
+
* @param subject - Subject identifier to invalidate
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* // Force logout on password change
|
|
182
|
+
* await ctx.invalidate(userId)
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
invalidate: (subject: string) => Promise<void>;
|
|
186
|
+
/**
|
|
187
|
+
* Storage adapter for persistent data operations.
|
|
188
|
+
* Provides access to the configured storage backend.
|
|
189
|
+
*/
|
|
190
|
+
readonly storage: StorageAdapter;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Base error class for provider-related errors.
|
|
194
|
+
* Extend this class to create specific provider error types.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* export class GitHubApiError extends ProviderError {
|
|
199
|
+
* constructor(message: string, public readonly statusCode: number) {
|
|
200
|
+
* super(message)
|
|
201
|
+
* }
|
|
202
|
+
* }
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
declare class ProviderError extends Error {
|
|
206
|
+
constructor(message: string);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Error thrown when a provider encounters an unknown or unexpected error.
|
|
210
|
+
* Used as a fallback for unhandled error conditions.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* catch (error) {
|
|
215
|
+
* if (error instanceof SomeSpecificError) {
|
|
216
|
+
* // Handle specific error
|
|
217
|
+
* } else {
|
|
218
|
+
* throw new ProviderUnknownError(`Unexpected error: ${error}`)
|
|
219
|
+
* }
|
|
220
|
+
* }
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
declare class ProviderUnknownError extends ProviderError {
|
|
224
|
+
constructor(message?: string);
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { Provider, ProviderError, ProviderOptions, ProviderRoute, ProviderUnknownError };
|