@server/next 0.29.0 → 0.30.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.
Files changed (3) hide show
  1. package/index.d.ts +6 -2
  2. package/index.js +479 -195
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -33,9 +33,13 @@ type RouteOptions = {
33
33
  description?: string;
34
34
  };
35
35
  type Cookie = {
36
- value?: string;
36
+ value?: string | null;
37
37
  path?: string;
38
38
  expires?: number | string | Date;
39
+ maxAge?: number;
40
+ httpOnly?: boolean;
41
+ secure?: boolean;
42
+ sameSite?: "Strict" | "Lax" | "None";
39
43
  };
40
44
  type RouterMethod = "*" | Method;
41
45
  type Bucket = {
@@ -77,7 +81,7 @@ type KVStore = {
77
81
  del: (key: string) => Promise<void | string>;
78
82
  keys: () => Promise<string[]>;
79
83
  };
80
- type Provider = "email" | "github";
84
+ type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
81
85
  type Strategy = "cookie" | "jwt" | "token";
82
86
  type AuthSession = {
83
87
  id: string;
package/index.js CHANGED
@@ -43,12 +43,22 @@ ServerError_default.extend({
43
43
  NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
44
44
  NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
45
45
  AUTH_ARGON_NEEDED: "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
46
- AUTH_INVALID_TOKEN: "Invalid Authorization token",
47
- AUTH_INVALID_COOKIE: "Invalid Authorization cookie",
48
- AUTH_INVALID_HEADER: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)",
49
- AUTH_INVALID_STRATEGY: "Invalid Authorization type '{strategy}', valid one is '{valid}'",
46
+ AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
47
+ AUTH_INVALID_COOKIE: { status: 401, message: "Invalid Authorization cookie" },
48
+ AUTH_INVALID_HEADER: {
49
+ status: 401,
50
+ message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
51
+ },
52
+ AUTH_INVALID_STRATEGY: {
53
+ status: 401,
54
+ message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
55
+ },
56
+ AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
50
57
  AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
51
- AUTH_INVALID_PROVIDER: "Invalid provider '{provider}', valid ones are: '{valid}'",
58
+ AUTH_INVALID_PROVIDER: {
59
+ status: 401,
60
+ message: "Invalid provider '{provider}', valid ones are: '{valid}'"
61
+ },
52
62
  AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
53
63
  AUTH_NO_USER: {
54
64
  status: 401,
@@ -97,98 +107,6 @@ function clientIp(headers2, opts = {}) {
97
107
  return normalize(remoteAddress);
98
108
  }
99
109
 
100
- // src/auth/updateUser.ts
101
- async function updateUser(user, auth2, store) {
102
- if (auth2.provider === "email") {
103
- return await store.set(auth2.email, user);
104
- }
105
- }
106
-
107
- // src/auth/providers/email.ts
108
- var createSession = async (user, ctx) => {
109
- const { strategy, session: session2, cleanUser, redirect: redirect2 = "/user" } = ctx.options.auth;
110
- user = await cleanUser(user);
111
- const id = createId();
112
- const provider = "email";
113
- ctx.user = {
114
- id,
115
- strategy,
116
- provider,
117
- email: user.email
118
- };
119
- await session2.set(
120
- id,
121
- { id, strategy, provider, user: user.email },
122
- { expires: "1w" }
123
- );
124
- if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
125
- if (strategy.includes("token")) {
126
- return status(201).json({ ...user, token: id });
127
- }
128
- if (strategy.includes("cookie")) {
129
- return status(302).cookies({ authentication: id }).redirect(redirect2);
130
- }
131
- if (strategy.includes("jwt")) {
132
- throw new Error("JWT auth not supported yet");
133
- }
134
- if (strategy.includes("key")) {
135
- throw new Error("Key auth not supported yet");
136
- }
137
- throw new Error("Unknown auth type");
138
- };
139
- async function emailLogin(ctx) {
140
- const { email, password } = ctx.body;
141
- if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
142
- if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
143
- if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
144
- if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
145
- const store = ctx.options.auth.store;
146
- if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
147
- const user = await store.get(email);
148
- const isValid = await verify(password, user.password);
149
- if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
150
- return createSession(user, ctx);
151
- }
152
- async function emailRegister(ctx) {
153
- const { email, password, ...data } = ctx.body;
154
- if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
155
- if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
156
- if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
157
- if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
158
- const store = ctx.options.auth.store;
159
- if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
160
- const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
161
- const user = {
162
- id: createId(email),
163
- strategy: ctx.options.auth.strategy,
164
- provider: "email",
165
- email,
166
- password: await hash(password),
167
- time,
168
- ...data
169
- };
170
- await store.set(email, user);
171
- return createSession(user, ctx);
172
- }
173
- async function emailResetPassword() {
174
- }
175
- async function emailUpdatePassword(ctx) {
176
- const passwords = ctx.body;
177
- const fullUser = await ctx.options.auth.store.get(ctx.user.email);
178
- if (!fullUser) throw ServerError_default.AUTH_NO_USER();
179
- const isValid = await verify(passwords.previous, fullUser.password);
180
- if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
181
- fullUser.password = await hash(passwords.updated);
182
- await updateUser(fullUser, ctx.user, ctx.options.auth.store);
183
- return 200;
184
- }
185
- var email_default = {
186
- login: emailLogin,
187
- register: emailRegister,
188
- reset: emailResetPassword,
189
- password: emailUpdatePassword
190
- };
191
-
192
110
  // src/helpers/isReadableStream.ts
193
111
  function isReadableStream(obj) {
194
112
  return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
@@ -314,6 +232,379 @@ var json = (...args) => r().json(...args);
314
232
  var file = (...args) => r().file(...args);
315
233
  var redirect = (...args) => r().redirect(...args);
316
234
 
235
+ // src/auth/finishLogin.ts
236
+ async function finishLogin(ctx, input) {
237
+ const settings = ctx.options.auth;
238
+ const { strategy, cleanUser } = settings;
239
+ const key = String(input.key);
240
+ const auth2 = {
241
+ id: createId(),
242
+ strategy,
243
+ provider: input.provider,
244
+ user: key,
245
+ email: input.email,
246
+ time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
247
+ };
248
+ let user = input.user;
249
+ if (input.store !== false) {
250
+ const existing = await settings.store.get(key);
251
+ user = { ...existing ?? {}, ...input.user };
252
+ }
253
+ user = await cleanUser(user);
254
+ if (input.store !== false) await settings.store.set(key, user);
255
+ await settings.session.set(auth2.id, auth2, { expires: "1w" });
256
+ if (strategy.includes("token")) {
257
+ return status(201).json({ ...user, token: auth2.id });
258
+ }
259
+ if (strategy.includes("cookie")) {
260
+ return cookies("authentication", {
261
+ value: auth2.id,
262
+ path: "/",
263
+ httpOnly: true,
264
+ secure: ctx.platform.production,
265
+ sameSite: "Lax"
266
+ }).redirect(settings.redirect);
267
+ }
268
+ if (strategy.includes("jwt")) throw new Error("JWT auth not supported yet");
269
+ if (strategy.includes("key")) throw new Error("Key auth not supported yet");
270
+ throw new Error("Unknown auth type");
271
+ }
272
+
273
+ // src/helpers/createCookies.ts
274
+ var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
275
+ var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
276
+ parse.millisecond = parse.ms = 1e-3;
277
+ parse.second = parse.sec = parse.s = parse[""] = 1;
278
+ parse.minute = parse.min = parse.m = parse.s * 60;
279
+ parse.hour = parse.hr = parse.h = parse.m * 60;
280
+ parse.day = parse.d = parse.h * 24;
281
+ parse.week = parse.wk = parse.w = parse.d * 7;
282
+ parse.year = parse.yr = parse.y = parse.d * 365.25;
283
+ parse.month = parse.b = parse.y / 12;
284
+ function parse(str) {
285
+ if (str === null || str === void 0) return null;
286
+ if (typeof str === "number") return str;
287
+ if (typeof str !== "string") {
288
+ throw new Error(`Not a string: ${str} (${typeof str})`);
289
+ }
290
+ str = str.toLowerCase().replace(/[,_]/g, "");
291
+ const [_, value, units] = times.exec(str) || [];
292
+ if (!units) return null;
293
+ const unitValue = parse[units] || parse[units.replace(/s$/, "")];
294
+ if (!unitValue) return null;
295
+ const result = unitValue * parseFloat(value);
296
+ return Math.abs(Math.round(result * 1e3));
297
+ }
298
+ function normalizeExpires(expires) {
299
+ if (expires === null || expires === void 0) return void 0;
300
+ if (expires === 0) return EXPIRED2;
301
+ if (typeof expires === "string") {
302
+ if (/^[\d._]+\w+$/.test(expires)) {
303
+ return new Date(Date.now() + parse(expires)).toUTCString();
304
+ } else {
305
+ return expires;
306
+ }
307
+ }
308
+ if (typeof expires === "number") {
309
+ return new Date(Date.now() + expires).toUTCString();
310
+ }
311
+ if (expires instanceof Date) {
312
+ return expires.toUTCString();
313
+ }
314
+ return void 0;
315
+ }
316
+ function createCookies(key, val) {
317
+ if (val.value === null) val.expires = EXPIRED2;
318
+ const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
319
+ let str = `${key}=${value || ""};Path=${path2 || "/"}`;
320
+ if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
321
+ if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
322
+ if (httpOnly) str += ";HttpOnly";
323
+ if (secure) str += ";Secure";
324
+ if (sameSite) str += `;SameSite=${sameSite}`;
325
+ return str;
326
+ }
327
+
328
+ // src/auth/state.ts
329
+ var NAME = "oauth_state";
330
+ function startState(ctx, crossSite = false) {
331
+ const state = createId();
332
+ return {
333
+ state,
334
+ cookie: {
335
+ value: state,
336
+ path: "/",
337
+ expires: "10m",
338
+ httpOnly: true,
339
+ secure: crossSite || ctx.platform.production,
340
+ sameSite: crossSite ? "None" : "Lax"
341
+ }
342
+ };
343
+ }
344
+ function checkState(ctx, received) {
345
+ const expected = ctx.cookies[NAME];
346
+ if (!expected || !received || expected !== received) {
347
+ throw ServerError_default.AUTH_INVALID_STATE();
348
+ }
349
+ }
350
+ function clearState() {
351
+ return createCookies(NAME, { value: null });
352
+ }
353
+
354
+ // src/auth/providers/apple.ts
355
+ var AUTHORIZE = "https://appleid.apple.com/auth/authorize";
356
+ var TOKEN = "https://appleid.apple.com/auth/token";
357
+ var b64url = (data) => {
358
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
359
+ let bin = "";
360
+ for (const byte of bytes) bin += String.fromCharCode(byte);
361
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
362
+ };
363
+ var b64urlJson = (segment) => {
364
+ let b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
365
+ b64 += "=".repeat((4 - b64.length % 4) % 4);
366
+ const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
367
+ return JSON.parse(new TextDecoder().decode(bytes));
368
+ };
369
+ var clientSecret = async () => {
370
+ const now = Math.floor(Date.now() / 1e3);
371
+ const header = { alg: "ES256", kid: env.APPLE_KEY_ID, typ: "JWT" };
372
+ const payload = {
373
+ iss: env.APPLE_TEAM_ID,
374
+ iat: now,
375
+ exp: now + 3600,
376
+ aud: "https://appleid.apple.com",
377
+ sub: env.APPLE_ID
378
+ };
379
+ const data = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
380
+ const pem = String(env.APPLE_PRIVATE_KEY).replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
381
+ const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
382
+ const key = await crypto.subtle.importKey(
383
+ "pkcs8",
384
+ der,
385
+ { name: "ECDSA", namedCurve: "P-256" },
386
+ false,
387
+ ["sign"]
388
+ );
389
+ const sig = await crypto.subtle.sign(
390
+ { name: "ECDSA", hash: "SHA-256" },
391
+ key,
392
+ new TextEncoder().encode(data)
393
+ );
394
+ return `${data}.${b64url(new Uint8Array(sig))}`;
395
+ };
396
+ var login = (ctx) => {
397
+ const { state, cookie } = startState(ctx, true);
398
+ const params = new URLSearchParams({
399
+ client_id: env.APPLE_ID,
400
+ redirect_uri: `${ctx.url.origin}/auth/callback/apple`,
401
+ response_type: "code",
402
+ scope: "name email",
403
+ // Requesting scopes forces Apple to POST the result back (form_post)
404
+ response_mode: "form_post",
405
+ state
406
+ });
407
+ return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
408
+ };
409
+ var callback = async (ctx) => {
410
+ const body = ctx.body || {};
411
+ checkState(ctx, body.state);
412
+ const tokenRes = await fetch(TOKEN, {
413
+ method: "POST",
414
+ headers: {
415
+ accept: "application/json",
416
+ "content-type": "application/x-www-form-urlencoded"
417
+ },
418
+ body: new URLSearchParams({
419
+ client_id: env.APPLE_ID,
420
+ client_secret: await clientSecret(),
421
+ code: body.code,
422
+ grant_type: "authorization_code",
423
+ redirect_uri: `${ctx.url.origin}/auth/callback/apple`
424
+ })
425
+ });
426
+ if (!tokenRes.ok) throw new Error("apple: token exchange failed");
427
+ const token = await tokenRes.json();
428
+ const claims = b64urlJson(token.id_token.split(".")[1]);
429
+ let name;
430
+ if (body.user) {
431
+ const parsed = JSON.parse(body.user).name;
432
+ if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
433
+ }
434
+ const res = await finishLogin(ctx, {
435
+ provider: "apple",
436
+ key: claims.sub,
437
+ email: claims.email,
438
+ user: { id: claims.sub, name, email: claims.email }
439
+ });
440
+ res.headers.append("set-cookie", clearState());
441
+ return res;
442
+ };
443
+ var apple_default = { login, callback };
444
+
445
+ // src/auth/providers/oauth.ts
446
+ function oauthProvider(config2) {
447
+ const KEY = config2.name.toUpperCase();
448
+ const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
449
+ const login3 = (ctx) => {
450
+ const { state, cookie } = startState(ctx);
451
+ const params = new URLSearchParams({
452
+ client_id: env[`${KEY}_ID`],
453
+ redirect_uri: callbackUrl(ctx),
454
+ response_type: "code",
455
+ scope: config2.scope,
456
+ state
457
+ });
458
+ return cookies("oauth_state", cookie).redirect(
459
+ `${config2.authorizeUrl}?${params}`
460
+ );
461
+ };
462
+ const callback3 = async (ctx) => {
463
+ checkState(ctx, ctx.url.query.state);
464
+ const tokenRes = await fetch(config2.tokenUrl, {
465
+ method: "POST",
466
+ headers: {
467
+ accept: "application/json",
468
+ "content-type": "application/x-www-form-urlencoded"
469
+ },
470
+ body: new URLSearchParams({
471
+ client_id: env[`${KEY}_ID`],
472
+ client_secret: env[`${KEY}_SECRET`],
473
+ code: ctx.url.query.code,
474
+ grant_type: "authorization_code",
475
+ redirect_uri: callbackUrl(ctx)
476
+ })
477
+ });
478
+ if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
479
+ const token = await tokenRes.json();
480
+ const profileRes = await fetch(config2.profileUrl, {
481
+ headers: {
482
+ accept: "application/json",
483
+ authorization: `Bearer ${token.access_token}`
484
+ }
485
+ });
486
+ if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
487
+ const profile = config2.profile(await profileRes.json());
488
+ const res = await finishLogin(ctx, {
489
+ provider: config2.name,
490
+ key: profile.id,
491
+ email: profile.email,
492
+ user: {
493
+ id: profile.id,
494
+ name: profile.name,
495
+ email: profile.email,
496
+ picture: profile.picture
497
+ }
498
+ });
499
+ res.headers.append("set-cookie", clearState());
500
+ return res;
501
+ };
502
+ return { login: login3, callback: callback3 };
503
+ }
504
+
505
+ // src/auth/providers/discord.ts
506
+ var discord_default = oauthProvider({
507
+ name: "discord",
508
+ authorizeUrl: "https://discord.com/oauth2/authorize",
509
+ tokenUrl: "https://discord.com/api/oauth2/token",
510
+ profileUrl: "https://discord.com/api/users/@me",
511
+ scope: "identify email",
512
+ profile: (p) => ({
513
+ id: p.id,
514
+ email: p.email,
515
+ name: p.global_name || p.username,
516
+ picture: p.avatar ? `https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png` : void 0
517
+ })
518
+ });
519
+
520
+ // src/auth/updateUser.ts
521
+ async function updateUser(user, auth2, store) {
522
+ if (auth2.provider === "email") {
523
+ return await store.set(auth2.email, user);
524
+ }
525
+ }
526
+
527
+ // src/auth/providers/email.ts
528
+ async function emailLogin(ctx) {
529
+ const { email, password } = ctx.body;
530
+ if (!email) throw ServerError_default.LOGIN_NO_EMAIL();
531
+ if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
532
+ if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
533
+ if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
534
+ const store = ctx.options.auth.store;
535
+ if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
536
+ const user = await store.get(email);
537
+ const isValid = await verify(password, user.password);
538
+ if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
539
+ return finishLogin(ctx, {
540
+ provider: "email",
541
+ key: user.email,
542
+ email: user.email,
543
+ user,
544
+ store: false
545
+ });
546
+ }
547
+ async function emailRegister(ctx) {
548
+ const { email, password, ...data } = ctx.body;
549
+ if (!email) throw ServerError_default.REGISTER_NO_EMAIL();
550
+ if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
551
+ if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
552
+ if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
553
+ const store = ctx.options.auth.store;
554
+ if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
555
+ const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
556
+ const user = {
557
+ id: createId(email),
558
+ strategy: ctx.options.auth.strategy,
559
+ provider: "email",
560
+ email,
561
+ password: await hash(password),
562
+ time,
563
+ ...data
564
+ };
565
+ await store.set(email, user);
566
+ return finishLogin(ctx, {
567
+ provider: "email",
568
+ key: email,
569
+ email,
570
+ user,
571
+ store: false
572
+ });
573
+ }
574
+ async function emailResetPassword() {
575
+ }
576
+ async function emailUpdatePassword(ctx) {
577
+ const passwords = ctx.body;
578
+ const fullUser = await ctx.options.auth.store.get(ctx.user.email);
579
+ if (!fullUser) throw ServerError_default.AUTH_NO_USER();
580
+ const isValid = await verify(passwords.previous, fullUser.password);
581
+ if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
582
+ fullUser.password = await hash(passwords.updated);
583
+ await updateUser(fullUser, ctx.user, ctx.options.auth.store);
584
+ return 200;
585
+ }
586
+ var email_default = {
587
+ login: emailLogin,
588
+ register: emailRegister,
589
+ reset: emailResetPassword,
590
+ password: emailUpdatePassword
591
+ };
592
+
593
+ // src/auth/providers/facebook.ts
594
+ var facebook_default = oauthProvider({
595
+ name: "facebook",
596
+ authorizeUrl: "https://www.facebook.com/v18.0/dialog/oauth",
597
+ tokenUrl: "https://graph.facebook.com/v18.0/oauth/access_token",
598
+ profileUrl: "https://graph.facebook.com/me?fields=id,name,email,picture",
599
+ scope: "email public_profile",
600
+ profile: (p) => ({
601
+ id: p.id,
602
+ email: p.email,
603
+ name: p.name,
604
+ picture: p.picture?.data?.url
605
+ })
606
+ });
607
+
317
608
  // src/auth/providers/github.ts
318
609
  var oauth = async (code) => {
319
610
  const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
@@ -337,9 +628,15 @@ var oauth = async (code) => {
337
628
  });
338
629
  };
339
630
  };
340
- var login = function githubLogin() {
341
- return redirect(
342
- `https://github.com/login/oauth/authorize?client_id=${env.GITHUB_ID}&scope=user:email`
631
+ var login2 = (ctx) => {
632
+ const { state, cookie } = startState(ctx);
633
+ const params = new URLSearchParams({
634
+ client_id: env.GITHUB_ID,
635
+ scope: "user:email",
636
+ state
637
+ });
638
+ return cookies("oauth_state", cookie).redirect(
639
+ `https://github.com/login/oauth/authorize?${params}`
343
640
  );
344
641
  };
345
642
  var getUserProfile = async (code) => {
@@ -351,47 +648,67 @@ var getUserProfile = async (code) => {
351
648
  const email = emails.sort((a) => a.primary ? -1 : 1)[0]?.email;
352
649
  return { ...profile, email };
353
650
  };
354
- var callback = async (ctx) => {
355
- const { strategy, cleanUser, store, session: session2, redirect: redirect2 } = ctx.options.auth;
651
+ var callback2 = async (ctx) => {
652
+ checkState(ctx, ctx.url.query.state);
356
653
  const profile = await getUserProfile(ctx.url.query.code);
357
- const auth2 = {
358
- id: createId(),
359
- strategy,
654
+ const res = await finishLogin(ctx, {
360
655
  provider: "github",
361
- user: String(profile.id),
656
+ key: profile.id,
362
657
  email: profile.email,
363
- time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
364
- };
365
- const existing = await store.get(String(profile.id));
366
- const user = cleanUser({
367
- ...existing ?? {},
368
- id: profile.id,
369
- name: profile.name,
370
- email: profile.email,
371
- picture: profile.avatar_url,
372
- location: profile.location,
373
- created: profile.created_at
658
+ user: {
659
+ id: profile.id,
660
+ name: profile.name,
661
+ email: profile.email,
662
+ picture: profile.avatar_url,
663
+ location: profile.location,
664
+ created: profile.created_at
665
+ }
374
666
  });
375
- await store.set(auth2.user, user);
376
- await session2.set(auth2.id, auth2, { expires: "1w" });
377
- if (auth2.strategy.includes("token")) {
378
- return status(201).json({ ...user, token: auth2.id });
379
- }
380
- if (auth2.strategy.includes("cookie")) {
381
- return status(302).cookies({ authentication: auth2.id }).redirect(redirect2);
382
- }
383
- if (auth2.strategy.includes("jwt")) {
384
- throw new Error("JWT auth not supported yet");
385
- }
386
- if (auth2.strategy.includes("key")) {
387
- throw new Error("Key auth not supported yet");
388
- }
389
- throw new Error("Unknown auth type");
667
+ res.headers.append("set-cookie", clearState());
668
+ return res;
390
669
  };
391
- var github_default = { login, callback };
670
+ var github_default = { login: login2, callback: callback2 };
671
+
672
+ // src/auth/providers/google.ts
673
+ var google_default = oauthProvider({
674
+ name: "google",
675
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
676
+ tokenUrl: "https://oauth2.googleapis.com/token",
677
+ profileUrl: "https://openidconnect.googleapis.com/v1/userinfo",
678
+ scope: "openid email profile",
679
+ profile: (p) => ({
680
+ id: p.sub,
681
+ email: p.email,
682
+ name: p.name,
683
+ picture: p.picture
684
+ })
685
+ });
686
+
687
+ // src/auth/providers/microsoft.ts
688
+ var microsoft_default = oauthProvider({
689
+ name: "microsoft",
690
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
691
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
692
+ profileUrl: "https://graph.microsoft.com/v1.0/me",
693
+ scope: "openid email profile User.Read",
694
+ profile: (p) => ({
695
+ // Personal accounts expose `userPrincipalName` rather than `mail`
696
+ id: p.id,
697
+ email: p.mail || p.userPrincipalName,
698
+ name: p.displayName
699
+ })
700
+ });
392
701
 
393
702
  // src/auth/providers/index.ts
394
- var providers_default = { email: email_default, github: github_default };
703
+ var providers_default = {
704
+ apple: apple_default,
705
+ discord: discord_default,
706
+ email: email_default,
707
+ facebook: facebook_default,
708
+ github: github_default,
709
+ google: google_default,
710
+ microsoft: microsoft_default
711
+ };
395
712
 
396
713
  // src/auth/parseAuthOptions.ts
397
714
  var defaultRedirect = "/user";
@@ -884,58 +1201,6 @@ function applyCors(res, ctx) {
884
1201
  }
885
1202
  }
886
1203
 
887
- // src/helpers/createCookies.ts
888
- var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
889
- var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
890
- parse.millisecond = parse.ms = 1e-3;
891
- parse.second = parse.sec = parse.s = parse[""] = 1;
892
- parse.minute = parse.min = parse.m = parse.s * 60;
893
- parse.hour = parse.hr = parse.h = parse.m * 60;
894
- parse.day = parse.d = parse.h * 24;
895
- parse.week = parse.wk = parse.w = parse.d * 7;
896
- parse.year = parse.yr = parse.y = parse.d * 365.25;
897
- parse.month = parse.b = parse.y / 12;
898
- function parse(str) {
899
- if (str === null || str === void 0) return null;
900
- if (typeof str === "number") return str;
901
- if (typeof str !== "string") {
902
- throw new Error(`Not a string: ${str} (${typeof str})`);
903
- }
904
- str = str.toLowerCase().replace(/[,_]/g, "");
905
- const [_, value, units] = times.exec(str) || [];
906
- if (!units) return null;
907
- const unitValue = parse[units] || parse[units.replace(/s$/, "")];
908
- if (!unitValue) return null;
909
- const result = unitValue * parseFloat(value);
910
- return Math.abs(Math.round(result * 1e3));
911
- }
912
- function normalizeExpires(expires) {
913
- if (expires === null || expires === void 0) return void 0;
914
- if (expires === 0) return EXPIRED2;
915
- if (typeof expires === "string") {
916
- if (/^[\d._]+\w+$/.test(expires)) {
917
- return new Date(Date.now() + parse(expires)).toUTCString();
918
- } else {
919
- return expires;
920
- }
921
- }
922
- if (typeof expires === "number") {
923
- return new Date(Date.now() + expires).toUTCString();
924
- }
925
- if (expires instanceof Date) {
926
- return expires.toUTCString();
927
- }
928
- return void 0;
929
- }
930
- function createCookies(key, val) {
931
- if (val.value === null) val.expires = EXPIRED2;
932
- const { value, path: path2, expires } = val;
933
- const pathPart = `;Path=${path2 || "/"}`;
934
- const expiresStr = normalizeExpires(expires);
935
- const expiresPart = typeof expires !== "undefined" ? `;Expires=${expiresStr}` : "";
936
- return `${key}=${value || ""}${pathPart}${expiresPart}`;
937
- }
938
-
939
1204
  // src/helpers/createWebsocket.ts
940
1205
  function createWebsocket(sockets, handlers) {
941
1206
  return {
@@ -1594,7 +1859,7 @@ async function logout(ctx) {
1594
1859
  return { token: null };
1595
1860
  }
1596
1861
  if (strategy.includes("cookie")) {
1597
- return cookies({ authorization: null }).redirect("/");
1862
+ return cookies({ authentication: null }).redirect("/");
1598
1863
  }
1599
1864
  if (strategy.includes("jwt")) {
1600
1865
  throw new Error("JWT auth not supported yet");
@@ -1606,18 +1871,37 @@ async function logout(ctx) {
1606
1871
  }
1607
1872
 
1608
1873
  // src/auth/index.ts
1874
+ var oauth2 = [
1875
+ "github",
1876
+ "google",
1877
+ "microsoft",
1878
+ "discord",
1879
+ "facebook"
1880
+ ];
1609
1881
  function auth(app) {
1610
1882
  app.use(async function middle(ctx) {
1611
1883
  ctx.user = await getUser(ctx);
1612
1884
  });
1613
- if (app.settings.auth.provider.includes("github")) {
1614
- if (!env.GITHUB_ID) throw new Error("GITHUB_ID not defined");
1615
- if (!env.GITHUB_SECRET) throw new Error("GITHUB_SECRET not defined");
1885
+ const enabled = app.settings.auth.provider;
1886
+ for (const name of oauth2) {
1887
+ if (!enabled.includes(name)) continue;
1888
+ const key = name.toUpperCase();
1889
+ if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
1890
+ if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
1891
+ app.get("/auth/logout", logout);
1892
+ app.get(`/auth/login/${name}`, providers_default[name].login);
1893
+ app.get(`/auth/callback/${name}`, providers_default[name].callback);
1894
+ }
1895
+ if (enabled.includes("apple")) {
1896
+ const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
1897
+ for (const key of keys) {
1898
+ if (!env[key]) throw new Error(`${key} not defined`);
1899
+ }
1616
1900
  app.get("/auth/logout", logout);
1617
- app.get("/auth/login/github", providers_default.github.login);
1618
- app.get("/auth/callback/github", providers_default.github.callback);
1901
+ app.get("/auth/login/apple", providers_default.apple.login);
1902
+ app.post("/auth/callback/apple", providers_default.apple.callback);
1619
1903
  }
1620
- if (app.settings.auth.provider.includes("email")) {
1904
+ if (enabled.includes("email")) {
1621
1905
  app.post("/auth/logout", logout);
1622
1906
  app.post("/auth/register/email", providers_default.email.register);
1623
1907
  app.post("/auth/login/email", providers_default.email.login);
@@ -1885,9 +2169,9 @@ import { TLSSocket } from "tls";
1885
2169
  // src/context/createEvents.ts
1886
2170
  function createEvents() {
1887
2171
  const events = {};
1888
- events.on = (name, callback2) => {
2172
+ events.on = (name, callback3) => {
1889
2173
  events[name] = events[name] || [];
1890
- events[name].push(callback2);
2174
+ events[name].push(callback3);
1891
2175
  };
1892
2176
  events.trigger = (name, data) => {
1893
2177
  if (!events[name]) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "github:franciscop/server-next",