@miguelmorales13/nestkit 0.5.1 → 0.7.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 (35) hide show
  1. package/README.md +250 -6
  2. package/dist/auth/index.cjs +12 -70
  3. package/dist/auth/index.js +11 -69
  4. package/dist/auth/oauth/http.d.ts +38 -0
  5. package/dist/auth/oauth/index.cjs +610 -0
  6. package/dist/auth/oauth/index.d.ts +12 -0
  7. package/dist/auth/oauth/index.js +610 -0
  8. package/dist/auth/oauth/oauth-auth.module.d.ts +28 -0
  9. package/dist/auth/oauth/oauth-auth.service.d.ts +101 -0
  10. package/dist/auth/oauth/oauth-callback.filter.d.ts +20 -0
  11. package/dist/auth/oauth/oauth-provider.d.ts +73 -0
  12. package/dist/auth/oauth/oauth.controller.d.ts +38 -0
  13. package/dist/auth/oauth/oauth.options.d.ts +89 -0
  14. package/dist/auth/oauth/oauth.ports.d.ts +86 -0
  15. package/dist/bootstrap/index.cjs +3 -3
  16. package/dist/bootstrap/index.js +2 -2
  17. package/dist/chunk-A3B2EY4V.js +75 -0
  18. package/dist/chunk-NVCI3CQI.cjs +75 -0
  19. package/dist/database/typeorm/index.cjs +79 -0
  20. package/dist/database/typeorm/index.d.ts +3 -0
  21. package/dist/database/typeorm/index.js +79 -0
  22. package/dist/database/typeorm/tenant-scope.d.ts +18 -0
  23. package/dist/database/typeorm/typeorm.module.d.ts +35 -0
  24. package/dist/index.cjs +17 -17
  25. package/dist/index.js +24 -24
  26. package/dist/umami/index.cjs +109 -0
  27. package/dist/umami/index.d.ts +4 -0
  28. package/dist/umami/index.js +109 -0
  29. package/dist/umami/umami.module.d.ts +6 -0
  30. package/dist/umami/umami.options.d.ts +20 -0
  31. package/dist/umami/umami.service.d.ts +40 -0
  32. package/dist/umami/umami.types.d.ts +24 -0
  33. package/package.json +22 -2
  34. package/dist/{chunk-AOCF5QCZ.js → chunk-ANQ3YPDI.js} +3 -3
  35. package/dist/{chunk-54ZXIB5T.cjs → chunk-YARLPYG5.cjs} +2 -2
@@ -0,0 +1,610 @@
1
+ import {
2
+ CurrentUser,
3
+ JwtAuthGuard
4
+ } from "../../chunk-A3B2EY4V.js";
5
+ import {
6
+ UnauthorizedAppException,
7
+ ValidationAppException
8
+ } from "../../chunk-ORWJ7LES.js";
9
+ import "../../chunk-YFYHLYHN.js";
10
+ import {
11
+ __decorateClass,
12
+ __decorateParam
13
+ } from "../../chunk-4MGIQFAJ.js";
14
+
15
+ // src/auth/oauth/oauth-provider.ts
16
+ async function readJson(response, what) {
17
+ const text = await response.text();
18
+ let body;
19
+ try {
20
+ body = JSON.parse(text);
21
+ } catch {
22
+ throw new Error(`${what}: expected JSON, got ${response.status} ${text.slice(0, 200)}`);
23
+ }
24
+ if (!response.ok) {
25
+ throw new Error(`${what}: HTTP ${response.status} ${text.slice(0, 200)}`);
26
+ }
27
+ return body;
28
+ }
29
+ function googleProvider(credentials = {}) {
30
+ return {
31
+ name: "google",
32
+ clientId: credentials.clientId ?? process.env.GOOGLE_CLIENT_ID,
33
+ clientSecret: credentials.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET,
34
+ callbackUrl: credentials.callbackUrl ?? process.env.GOOGLE_CALLBACK_URL,
35
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
36
+ tokenUrl: "https://oauth2.googleapis.com/token",
37
+ scope: credentials.scope ?? "email profile",
38
+ authorizeParams: {
39
+ // Without this, someone signed into several Google accounts is put
40
+ // straight back into the one the browser last used, with no way to pick
41
+ // — which reads as the app ignoring who they wanted to be.
42
+ prompt: "select_account",
43
+ access_type: "offline"
44
+ },
45
+ async fetchProfile(accessToken) {
46
+ const data = await readJson(
47
+ await fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
48
+ headers: { Authorization: `Bearer ${accessToken}` }
49
+ }),
50
+ "Google userinfo"
51
+ );
52
+ const email = data.email;
53
+ if (!email) throw new Error("Google did not return an email.");
54
+ return {
55
+ providerAccountId: String(data.sub),
56
+ email,
57
+ name: data.name,
58
+ avatarUrl: data.picture
59
+ };
60
+ }
61
+ };
62
+ }
63
+ function facebookProvider(credentials = {}) {
64
+ const version = process.env.FACEBOOK_GRAPH_VERSION ?? "v21.0";
65
+ return {
66
+ name: "facebook",
67
+ clientId: credentials.clientId ?? process.env.FACEBOOK_APP_ID,
68
+ clientSecret: credentials.clientSecret ?? process.env.FACEBOOK_APP_SECRET,
69
+ callbackUrl: credentials.callbackUrl ?? process.env.FACEBOOK_CALLBACK_URL,
70
+ authorizeUrl: `https://www.facebook.com/${version}/dialog/oauth`,
71
+ tokenUrl: `https://graph.facebook.com/${version}/oauth/access_token`,
72
+ scope: credentials.scope ?? "email",
73
+ async fetchProfile(accessToken) {
74
+ const url = new URL(`https://graph.facebook.com/${version}/me`);
75
+ url.searchParams.set("fields", "id,name,email,picture.type(large)");
76
+ const data = await readJson(
77
+ await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }),
78
+ "Facebook me"
79
+ );
80
+ const picture = data.picture;
81
+ return {
82
+ providerAccountId: String(data.id),
83
+ email: data.email ?? "",
84
+ name: data.name,
85
+ avatarUrl: picture?.data?.url
86
+ };
87
+ }
88
+ };
89
+ }
90
+ function buildAuthorizeUrl(provider, state) {
91
+ const params = new URLSearchParams({
92
+ client_id: provider.clientId ?? "",
93
+ redirect_uri: provider.callbackUrl ?? "",
94
+ response_type: "code",
95
+ scope: provider.scope,
96
+ ...provider.authorizeParams,
97
+ ...state ? { state } : {}
98
+ });
99
+ return `${provider.authorizeUrl}?${params.toString()}`;
100
+ }
101
+ async function exchangeCodeForToken(provider, code) {
102
+ const body = new URLSearchParams({
103
+ client_id: provider.clientId ?? "",
104
+ client_secret: provider.clientSecret ?? "",
105
+ redirect_uri: provider.callbackUrl ?? "",
106
+ grant_type: "authorization_code",
107
+ code
108
+ });
109
+ const data = await readJson(
110
+ await fetch(provider.tokenUrl, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/x-www-form-urlencoded",
114
+ Accept: "application/json"
115
+ },
116
+ body
117
+ }),
118
+ `${provider.name} token exchange`
119
+ );
120
+ const token = data.access_token;
121
+ if (!token) throw new Error(`${provider.name} token exchange returned no access_token.`);
122
+ return token;
123
+ }
124
+
125
+ // src/auth/oauth/oauth.options.ts
126
+ var OAUTH_AUTH_OPTIONS = /* @__PURE__ */ Symbol("OAUTH_AUTH_OPTIONS");
127
+ var OAUTH_STORE = /* @__PURE__ */ Symbol("OAUTH_STORE");
128
+ var REFRESH_TOKEN_STORE = /* @__PURE__ */ Symbol("REFRESH_TOKEN_STORE");
129
+ function resolveOAuthOptions(options) {
130
+ const webUrl = options.webUrl.replace(/\/$/, "");
131
+ return {
132
+ providers: options.providers,
133
+ webUrl,
134
+ onLoginRedirect: options.onLoginRedirect ?? ((accessToken) => `${webUrl}/oauth-callback#accessToken=${accessToken}`),
135
+ onErrorRedirect: options.onErrorRedirect ?? ((message) => `${webUrl}/login?error=${encodeURIComponent(message)}`),
136
+ onLinkRedirect: options.onLinkRedirect ?? ((provider, error) => error ? `${webUrl}/profile?error=${encodeURIComponent(error)}` : `${webUrl}/profile?linked=${provider}`),
137
+ cookie: {
138
+ name: options.cookie?.name ?? "refresh_token",
139
+ path: options.cookie?.path ?? "/auth",
140
+ sameSite: options.cookie?.sameSite ?? "lax",
141
+ secure: options.cookie?.secure ?? process.env.NODE_ENV === "production"
142
+ },
143
+ linkCookieName: options.linkCookieName ?? "link_intent",
144
+ accessTtlSeconds: options.accessTtlSeconds ?? 15 * 60,
145
+ refreshTtlSeconds: options.refreshTtlSeconds ?? 30 * 24 * 60 * 60,
146
+ linkTtlSeconds: options.linkTtlSeconds ?? 10 * 60,
147
+ accessSecret: options.accessSecret,
148
+ refreshSecret: options.refreshSecret,
149
+ singleSession: options.singleSession ?? true,
150
+ syncProfile: options.syncProfile ?? ((_user, profile) => ({ name: profile.name, avatarUrl: profile.avatarUrl ?? null })),
151
+ mapUser: options.mapUser,
152
+ unavailableMessage: options.unavailableMessage ?? ((provider) => `Signing in with ${provider} is unavailable.`),
153
+ genericErrorMessage: options.genericErrorMessage ?? (() => "Sign-in could not be completed. Please try again."),
154
+ missingEmailMessage: options.missingEmailMessage ?? ((provider) => `${provider} did not share your email address, so we cannot create your account.`)
155
+ };
156
+ }
157
+
158
+ // src/auth/oauth/oauth-auth.service.ts
159
+ import { Inject, Injectable, ServiceUnavailableException } from "@nestjs/common";
160
+ import { createHash, randomUUID } from "crypto";
161
+ import jwt from "jsonwebtoken";
162
+ function hashToken(token) {
163
+ return createHash("sha256").update(token).digest("hex");
164
+ }
165
+ function requireSecret(explicit, envName) {
166
+ const value = explicit ?? process.env[envName];
167
+ if (!value) throw new Error(`OAuthAuthService: ${envName} is not set`);
168
+ return value;
169
+ }
170
+ var OAuthAuthService = class {
171
+ constructor(options, store, sessions) {
172
+ this.store = store;
173
+ this.sessions = sessions;
174
+ this.options = resolveOAuthOptions(options);
175
+ }
176
+ get config() {
177
+ return this.options;
178
+ }
179
+ /**
180
+ * Looks up a configured provider, or explains that it is unavailable.
181
+ *
182
+ * Missing credentials make one provider stop existing rather than stopping
183
+ * the boot — the same fail-late rule the rest of nestkit's optional
184
+ * integrations follow. An unknown name and an unconfigured one answer
185
+ * identically on purpose: neither is something the caller can act on
186
+ * differently, and distinguishing them only tells a prober what exists.
187
+ */
188
+ provider(name) {
189
+ const found = this.options.providers.find((p) => p.name === name);
190
+ if (!found?.clientId || !found.clientSecret) {
191
+ throw new ServiceUnavailableException(this.options.unavailableMessage(name));
192
+ }
193
+ return found;
194
+ }
195
+ authorizeUrl(name) {
196
+ return buildAuthorizeUrl(this.provider(name));
197
+ }
198
+ /** Exchanges the callback's `code` for a normalised profile. */
199
+ async profileFromCode(name, code) {
200
+ const provider = this.provider(name);
201
+ const accessToken = await exchangeCodeForToken(provider, code);
202
+ const profile = await provider.fetchProfile(accessToken);
203
+ if (!profile.email) {
204
+ throw new UnauthorizedAppException(this.options.missingEmailMessage(name));
205
+ }
206
+ return { ...profile, email: normaliseEmail(profile.email) };
207
+ }
208
+ /**
209
+ * Signs someone in, creating the account the first time.
210
+ *
211
+ * The provider is a parameter because the flow is identical for all of
212
+ * them: find the linked account, and failing that link by email to whoever
213
+ * is already registered. That email link is what makes signing in with
214
+ * Facebook today and Google tomorrow land on one account instead of two.
215
+ */
216
+ async login(providerName, profile) {
217
+ const provider = this.provider(providerName).name;
218
+ const existing = await this.store.findAccount(provider, profile.providerAccountId);
219
+ let user = existing?.user ?? null;
220
+ if (!user) {
221
+ const email = normaliseEmail(profile.email);
222
+ user = await this.store.findUserByEmail(email) ?? await this.store.createUser({
223
+ email,
224
+ name: profile.name,
225
+ avatarUrl: profile.avatarUrl
226
+ });
227
+ await this.store.createAccount({
228
+ userId: user.id,
229
+ provider,
230
+ providerAccountId: profile.providerAccountId,
231
+ avatarUrl: profile.avatarUrl ?? null
232
+ });
233
+ } else {
234
+ await this.store.updateAccountAvatar(
235
+ provider,
236
+ profile.providerAccountId,
237
+ profile.avatarUrl ?? null
238
+ );
239
+ }
240
+ const changes = this.options.syncProfile(user, profile);
241
+ if (changes && (changes.name !== user.name || changes.avatarUrl !== user.avatarUrl)) {
242
+ user = await this.store.updateUser(user.id, changes);
243
+ }
244
+ if (this.options.singleSession) {
245
+ await this.sessions.revokeAllForUser(user.id);
246
+ }
247
+ const tokens = await this.issueTokenPair(user);
248
+ return { user: this.present(user), ...tokens };
249
+ }
250
+ /**
251
+ * Rotates a refresh token: the old one is spent, a new pair comes back.
252
+ *
253
+ * Revoking before issuing, and only continuing when the revoke actually hit
254
+ * a live row, is the whole point — replaying a token that has already been
255
+ * used finds nothing to revoke and is rejected.
256
+ */
257
+ async refresh(rawRefreshToken) {
258
+ const payload = this.verifyRefresh(rawRefreshToken);
259
+ const { count } = await this.sessions.revokeIfActive(hashToken(rawRefreshToken));
260
+ if (count === 0) throw new UnauthorizedAppException("Invalid refresh token.");
261
+ const user = await this.store.findUserById(payload.sub);
262
+ if (!user) throw new UnauthorizedAppException("Invalid refresh token.");
263
+ return this.issueTokenPair(user);
264
+ }
265
+ async logout(rawRefreshToken) {
266
+ await this.sessions.revokeIfActive(hashToken(rawRefreshToken));
267
+ }
268
+ async currentUser(userId) {
269
+ const user = await this.store.findUserById(userId);
270
+ if (!user) throw new UnauthorizedAppException("User not found.");
271
+ return this.present(user);
272
+ }
273
+ /**
274
+ * Issues the short-lived permission to link another provider.
275
+ *
276
+ * Needed because the OAuth leg is a whole-browser navigation, not a request
277
+ * with headers: the access token does not travel there. So a permission is
278
+ * minted, parked in an httpOnly cookie, and read back by the callback to
279
+ * tell "link this to me" from "sign in". It carries its own `typ` so it can
280
+ * never pass as an access token, or the other way round, despite sharing a
281
+ * secret.
282
+ */
283
+ issueLinkToken(userId) {
284
+ const ttl = this.options.linkTtlSeconds;
285
+ const token = jwt.sign(
286
+ { sub: userId, typ: "link" },
287
+ requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET"),
288
+ { expiresIn: ttl }
289
+ );
290
+ return { token, expiresAt: new Date(Date.now() + ttl * 1e3) };
291
+ }
292
+ /**
293
+ * The user id behind a link permission, or null.
294
+ *
295
+ * Null rather than an exception: with no valid permission the callback just
296
+ * performs an ordinary sign-in, which is the correct outcome.
297
+ */
298
+ readLinkToken(token) {
299
+ if (!token) return null;
300
+ try {
301
+ const payload = jwt.verify(
302
+ token,
303
+ requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET")
304
+ );
305
+ return payload.typ === "link" && payload.sub ? payload.sub : null;
306
+ } catch {
307
+ return null;
308
+ }
309
+ }
310
+ /**
311
+ * Ties a provider account to the already-signed-in user.
312
+ *
313
+ * This is the way out when the emails do not match: if someone's Facebook
314
+ * is registered under a different address, linking by email can never join
315
+ * them, and without this they keep two profiles forever.
316
+ */
317
+ async linkAccount(userId, providerName, profile) {
318
+ const provider = this.provider(providerName).name;
319
+ const existing = await this.store.findAccount(provider, profile.providerAccountId);
320
+ if (existing && existing.userId !== userId) {
321
+ throw new ValidationAppException(
322
+ "That account is already connected to another profile.",
323
+ { provider },
324
+ "OAUTH_ACCOUNT_TAKEN"
325
+ );
326
+ }
327
+ const alreadyLinked = await this.store.findAccountByUserAndProvider(userId, provider);
328
+ if (alreadyLinked && alreadyLinked.providerAccountId !== profile.providerAccountId) {
329
+ throw new ValidationAppException(
330
+ "You already have an account from that provider connected.",
331
+ { provider },
332
+ "OAUTH_PROVIDER_ALREADY_LINKED"
333
+ );
334
+ }
335
+ if (existing) {
336
+ await this.store.updateAccountAvatar(
337
+ provider,
338
+ profile.providerAccountId,
339
+ profile.avatarUrl ?? null
340
+ );
341
+ return;
342
+ }
343
+ await this.store.createAccount({
344
+ userId,
345
+ provider,
346
+ providerAccountId: profile.providerAccountId,
347
+ avatarUrl: profile.avatarUrl ?? null
348
+ });
349
+ }
350
+ /**
351
+ * Which providers are linked, and what photo each one offers.
352
+ *
353
+ * Enough to build both a "connect another account" screen and an avatar
354
+ * picker; what the app does with the choice is the app's decision.
355
+ */
356
+ async linkedAccounts(userId) {
357
+ const accounts = await this.store.listAccountsByUser(userId);
358
+ return {
359
+ connected: accounts.map((a) => a.provider),
360
+ avatars: accounts.filter((a) => Boolean(a.avatarUrl)).map((a) => ({ provider: a.provider, avatarUrl: a.avatarUrl }))
361
+ };
362
+ }
363
+ present(user) {
364
+ return this.options.mapUser ? this.options.mapUser(user) : user;
365
+ }
366
+ async issueTokenPair(user) {
367
+ const accessToken = jwt.sign(
368
+ { sub: user.id, email: user.email },
369
+ requireSecret(this.options.accessSecret, "JWT_ACCESS_SECRET"),
370
+ { expiresIn: this.options.accessTtlSeconds }
371
+ );
372
+ const refreshToken = jwt.sign(
373
+ // The jti keeps two tokens minted for the same user in the same second
374
+ // from coming out byte-identical — a JWT is deterministic given the
375
+ // same payload and secret, and identical tokens collide on the unique
376
+ // index over the stored hash.
377
+ { sub: user.id, jti: randomUUID() },
378
+ requireSecret(this.options.refreshSecret, "JWT_REFRESH_SECRET"),
379
+ { expiresIn: this.options.refreshTtlSeconds }
380
+ );
381
+ const expiresAt = new Date(Date.now() + this.options.refreshTtlSeconds * 1e3);
382
+ await this.sessions.create(user.id, hashToken(refreshToken), expiresAt);
383
+ return { accessToken, refreshToken, refreshTokenExpiresAt: expiresAt };
384
+ }
385
+ verifyRefresh(token) {
386
+ try {
387
+ return jwt.verify(
388
+ token,
389
+ requireSecret(this.options.refreshSecret, "JWT_REFRESH_SECRET")
390
+ );
391
+ } catch {
392
+ throw new UnauthorizedAppException("Invalid refresh token.");
393
+ }
394
+ }
395
+ };
396
+ OAuthAuthService = __decorateClass([
397
+ Injectable(),
398
+ __decorateParam(0, Inject(OAUTH_AUTH_OPTIONS)),
399
+ __decorateParam(1, Inject(OAUTH_STORE)),
400
+ __decorateParam(2, Inject(REFRESH_TOKEN_STORE))
401
+ ], OAuthAuthService);
402
+ function normaliseEmail(email) {
403
+ return email.trim().toLowerCase();
404
+ }
405
+
406
+ // src/auth/oauth/oauth-callback.filter.ts
407
+ import {
408
+ Catch,
409
+ HttpException,
410
+ Inject as Inject2,
411
+ Logger
412
+ } from "@nestjs/common";
413
+
414
+ // src/auth/oauth/http.ts
415
+ function setCookie(reply, name, value, options) {
416
+ if (typeof reply.setCookie === "function") reply.setCookie(name, value, options);
417
+ else if (typeof reply.cookie === "function") reply.cookie(name, value, options);
418
+ else throw new Error("No cookie support on the reply: register @fastify/cookie or cookie-parser.");
419
+ }
420
+ function clearCookie(reply, name, options) {
421
+ reply.clearCookie?.(name, options);
422
+ }
423
+ function redirect(reply, url) {
424
+ reply.redirect(url, 302);
425
+ }
426
+
427
+ // src/auth/oauth/oauth-callback.filter.ts
428
+ var OAuthCallbackFilter = class {
429
+ constructor(options) {
430
+ this.logger = new Logger(OAuthCallbackFilter.name);
431
+ this.options = resolveOAuthOptions(options);
432
+ }
433
+ catch(exception, host) {
434
+ const reply = host.switchToHttp().getResponse();
435
+ this.logger.error(
436
+ `OAuth callback failed: ${exception instanceof Error ? exception.stack : String(exception)}`
437
+ );
438
+ const message = exception instanceof HttpException ? exception.message : this.options.genericErrorMessage();
439
+ redirect(reply, this.options.onErrorRedirect(message));
440
+ }
441
+ };
442
+ OAuthCallbackFilter = __decorateClass([
443
+ Catch(),
444
+ __decorateParam(0, Inject2(OAUTH_AUTH_OPTIONS))
445
+ ], OAuthCallbackFilter);
446
+
447
+ // src/auth/oauth/oauth.controller.ts
448
+ import {
449
+ Controller,
450
+ Get,
451
+ Param,
452
+ Post,
453
+ Query,
454
+ Req,
455
+ Res,
456
+ UseFilters,
457
+ UseGuards
458
+ } from "@nestjs/common";
459
+ function createOAuthController(options = {}) {
460
+ const Guard = options.guard ?? JwtAuthGuard;
461
+ const userId = options.userId ?? ((user) => user.sub);
462
+ let OAuthControllerHost = class {
463
+ constructor(auth) {
464
+ this.auth = auth;
465
+ }
466
+ async refresh(request, reply) {
467
+ const raw = request.cookies?.[this.auth.config.cookie.name];
468
+ if (!raw) throw new UnauthorizedAppException("No refresh token.");
469
+ const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.auth.refresh(raw);
470
+ this.writeRefreshCookie(reply, refreshToken, refreshTokenExpiresAt);
471
+ return { accessToken };
472
+ }
473
+ async logout(request, reply) {
474
+ const raw = request.cookies?.[this.auth.config.cookie.name];
475
+ if (raw) await this.auth.logout(raw);
476
+ clearCookie(reply, this.auth.config.cookie.name, { path: this.auth.config.cookie.path });
477
+ return { success: true };
478
+ }
479
+ me(user) {
480
+ return this.auth.currentUser(userId(user));
481
+ }
482
+ linkedAccounts(user) {
483
+ return this.auth.linkedAccounts(userId(user));
484
+ }
485
+ linkIntent(user, reply) {
486
+ const { token, expiresAt } = this.auth.issueLinkToken(userId(user));
487
+ setCookie(reply, this.auth.config.linkCookieName, token, {
488
+ ...this.cookieBase(),
489
+ expires: expiresAt
490
+ });
491
+ return { ok: true };
492
+ }
493
+ start(provider, reply) {
494
+ redirect(reply, this.auth.authorizeUrl(provider));
495
+ }
496
+ async callback(provider, code, request, reply) {
497
+ if (!code) throw new UnauthorizedAppException(this.auth.config.genericErrorMessage());
498
+ const profile = await this.auth.profileFromCode(provider, code);
499
+ const linkUserId = this.auth.readLinkToken(request.cookies?.[this.auth.config.linkCookieName]);
500
+ if (linkUserId) {
501
+ clearCookie(reply, this.auth.config.linkCookieName, { path: this.auth.config.cookie.path });
502
+ try {
503
+ await this.auth.linkAccount(linkUserId, provider, profile);
504
+ redirect(reply, this.auth.config.onLinkRedirect(provider));
505
+ } catch (error) {
506
+ const message = error instanceof Error ? error.message : "Could not connect the account.";
507
+ redirect(reply, this.auth.config.onLinkRedirect(provider, message));
508
+ }
509
+ return;
510
+ }
511
+ const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.auth.login(
512
+ provider,
513
+ profile
514
+ );
515
+ this.writeRefreshCookie(reply, refreshToken, refreshTokenExpiresAt);
516
+ redirect(reply, this.auth.config.onLoginRedirect(accessToken));
517
+ }
518
+ cookieBase() {
519
+ const { secure, sameSite, path } = this.auth.config.cookie;
520
+ return { httpOnly: true, secure, sameSite, path };
521
+ }
522
+ writeRefreshCookie(reply, token, expiresAt) {
523
+ setCookie(reply, this.auth.config.cookie.name, token, {
524
+ ...this.cookieBase(),
525
+ expires: expiresAt
526
+ });
527
+ }
528
+ };
529
+ __decorateClass([
530
+ Post("refresh"),
531
+ __decorateParam(0, Req()),
532
+ __decorateParam(1, Res({ passthrough: true }))
533
+ ], OAuthControllerHost.prototype, "refresh", 1);
534
+ __decorateClass([
535
+ Post("logout"),
536
+ __decorateParam(0, Req()),
537
+ __decorateParam(1, Res({ passthrough: true }))
538
+ ], OAuthControllerHost.prototype, "logout", 1);
539
+ __decorateClass([
540
+ UseGuards(Guard),
541
+ Get("me"),
542
+ __decorateParam(0, CurrentUser())
543
+ ], OAuthControllerHost.prototype, "me", 1);
544
+ __decorateClass([
545
+ UseGuards(Guard),
546
+ Get("linked-accounts"),
547
+ __decorateParam(0, CurrentUser())
548
+ ], OAuthControllerHost.prototype, "linkedAccounts", 1);
549
+ __decorateClass([
550
+ UseGuards(Guard),
551
+ Post("link-intent"),
552
+ __decorateParam(0, CurrentUser()),
553
+ __decorateParam(1, Res({ passthrough: true }))
554
+ ], OAuthControllerHost.prototype, "linkIntent", 1);
555
+ __decorateClass([
556
+ Get(":provider"),
557
+ __decorateParam(0, Param("provider")),
558
+ __decorateParam(1, Res())
559
+ ], OAuthControllerHost.prototype, "start", 1);
560
+ __decorateClass([
561
+ Get(":provider/callback"),
562
+ UseFilters(OAuthCallbackFilter),
563
+ __decorateParam(0, Param("provider")),
564
+ __decorateParam(1, Query("code")),
565
+ __decorateParam(2, Req()),
566
+ __decorateParam(3, Res())
567
+ ], OAuthControllerHost.prototype, "callback", 1);
568
+ OAuthControllerHost = __decorateClass([
569
+ Controller(options.path ?? "auth")
570
+ ], OAuthControllerHost);
571
+ return OAuthControllerHost;
572
+ }
573
+
574
+ // src/auth/oauth/oauth-auth.module.ts
575
+ import { Module } from "@nestjs/common";
576
+ var OAuthAuthModule = class {
577
+ static forRoot(config) {
578
+ return {
579
+ module: OAuthAuthModule,
580
+ imports: config.imports ?? [],
581
+ providers: [
582
+ { provide: OAUTH_AUTH_OPTIONS, useValue: config.options },
583
+ ...config.providers,
584
+ OAuthAuthService,
585
+ OAuthCallbackFilter
586
+ ],
587
+ // The options travel out too: a consumer's own controller needs the
588
+ // cookie name and the redirect builders, and re-deriving them there is
589
+ // how the two copies drift apart.
590
+ exports: [OAuthAuthService, OAuthCallbackFilter, OAUTH_AUTH_OPTIONS]
591
+ };
592
+ }
593
+ };
594
+ OAuthAuthModule = __decorateClass([
595
+ Module({})
596
+ ], OAuthAuthModule);
597
+ export {
598
+ OAUTH_AUTH_OPTIONS,
599
+ OAUTH_STORE,
600
+ OAuthAuthModule,
601
+ OAuthAuthService,
602
+ OAuthCallbackFilter,
603
+ REFRESH_TOKEN_STORE,
604
+ buildAuthorizeUrl,
605
+ createOAuthController,
606
+ exchangeCodeForToken,
607
+ facebookProvider,
608
+ googleProvider,
609
+ resolveOAuthOptions
610
+ };
@@ -0,0 +1,28 @@
1
+ import { type DynamicModule, type ModuleMetadata, type Provider } from '@nestjs/common';
2
+ import { type OAuthAuthOptions } from './oauth.options.js';
3
+ import type { OAuthUser } from './oauth.ports.js';
4
+ export interface OAuthAuthModuleOptions<U extends OAuthUser = OAuthUser> {
5
+ options: OAuthAuthOptions<U>;
6
+ /**
7
+ * Whatever provides `OAUTH_STORE` and `REFRESH_TOKEN_STORE` — usually one
8
+ * repository class bound to both tokens.
9
+ *
10
+ * Plain Nest providers rather than a bespoke `useClass`/`useExisting`
11
+ * wrapper: there is no shape this module needs that Nest's own DI does not
12
+ * already express, and a second vocabulary for it would only be one more
13
+ * thing to look up.
14
+ */
15
+ providers: Provider[];
16
+ /** Modules exporting whatever those providers inject (a PrismaModule, say). */
17
+ imports?: ModuleMetadata['imports'];
18
+ }
19
+ /**
20
+ * OAuth sign-in with rotating refresh sessions.
21
+ *
22
+ * Brings no controller of its own — call `createOAuthController()` from the
23
+ * consuming module, so the route prefix and any extra endpoints stay the
24
+ * app's decision. See `OAuthAuthService`.
25
+ */
26
+ export declare class OAuthAuthModule {
27
+ static forRoot<U extends OAuthUser>(config: OAuthAuthModuleOptions<U>): DynamicModule;
28
+ }