@miguelmorales13/nestkit 0.5.0 → 0.6.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/README.md +81 -2
- package/dist/auth/index.cjs +12 -70
- package/dist/auth/index.js +11 -69
- package/dist/auth/oauth/http.d.ts +38 -0
- package/dist/auth/oauth/index.cjs +610 -0
- package/dist/auth/oauth/index.d.ts +12 -0
- package/dist/auth/oauth/index.js +610 -0
- package/dist/auth/oauth/oauth-auth.module.d.ts +28 -0
- package/dist/auth/oauth/oauth-auth.service.d.ts +101 -0
- package/dist/auth/oauth/oauth-callback.filter.d.ts +20 -0
- package/dist/auth/oauth/oauth-provider.d.ts +73 -0
- package/dist/auth/oauth/oauth.controller.d.ts +38 -0
- package/dist/auth/oauth/oauth.options.d.ts +89 -0
- package/dist/auth/oauth/oauth.ports.d.ts +86 -0
- package/dist/chunk-A3B2EY4V.js +75 -0
- package/dist/chunk-NVCI3CQI.cjs +75 -0
- package/dist/index.cjs +10 -10
- package/dist/index.js +11 -11
- package/dist/umami/index.cjs +109 -0
- package/dist/umami/index.d.ts +4 -0
- package/dist/umami/index.js +109 -0
- package/dist/umami/umami.module.d.ts +6 -0
- package/dist/umami/umami.options.d.ts +20 -0
- package/dist/umami/umami.service.d.ts +40 -0
- package/dist/umami/umami.types.d.ts +24 -0
- package/package.json +15 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type OAuthProfile, type OAuthProviderConfig } from './oauth-provider.js';
|
|
2
|
+
import { type OAuthAuthOptions, type ResolvedOAuthOptions } from './oauth.options.js';
|
|
3
|
+
import type { OAuthStorePort, OAuthUser, RefreshTokenStorePort } from './oauth.ports.js';
|
|
4
|
+
export interface TokenPair {
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
refreshTokenExpiresAt: Date;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* OAuth sign-in, account linking and rotating refresh sessions.
|
|
11
|
+
*
|
|
12
|
+
* Knows nothing about a database: everything it stores goes through
|
|
13
|
+
* `OAuthStorePort` and `RefreshTokenStorePort`, so the same service runs on
|
|
14
|
+
* Prisma, Drizzle or a hand-written repository.
|
|
15
|
+
*/
|
|
16
|
+
export declare class OAuthAuthService<U extends OAuthUser = OAuthUser> {
|
|
17
|
+
private readonly store;
|
|
18
|
+
private readonly sessions;
|
|
19
|
+
private readonly options;
|
|
20
|
+
constructor(options: OAuthAuthOptions<U>, store: OAuthStorePort<U>, sessions: RefreshTokenStorePort);
|
|
21
|
+
get config(): ResolvedOAuthOptions<U>;
|
|
22
|
+
/**
|
|
23
|
+
* Looks up a configured provider, or explains that it is unavailable.
|
|
24
|
+
*
|
|
25
|
+
* Missing credentials make one provider stop existing rather than stopping
|
|
26
|
+
* the boot — the same fail-late rule the rest of nestkit's optional
|
|
27
|
+
* integrations follow. An unknown name and an unconfigured one answer
|
|
28
|
+
* identically on purpose: neither is something the caller can act on
|
|
29
|
+
* differently, and distinguishing them only tells a prober what exists.
|
|
30
|
+
*/
|
|
31
|
+
provider(name: string): OAuthProviderConfig;
|
|
32
|
+
authorizeUrl(name: string): string;
|
|
33
|
+
/** Exchanges the callback's `code` for a normalised profile. */
|
|
34
|
+
profileFromCode(name: string, code: string): Promise<OAuthProfile>;
|
|
35
|
+
/**
|
|
36
|
+
* Signs someone in, creating the account the first time.
|
|
37
|
+
*
|
|
38
|
+
* The provider is a parameter because the flow is identical for all of
|
|
39
|
+
* them: find the linked account, and failing that link by email to whoever
|
|
40
|
+
* is already registered. That email link is what makes signing in with
|
|
41
|
+
* Facebook today and Google tomorrow land on one account instead of two.
|
|
42
|
+
*/
|
|
43
|
+
login(providerName: string, profile: OAuthProfile): Promise<{
|
|
44
|
+
user: unknown;
|
|
45
|
+
} & TokenPair>;
|
|
46
|
+
/**
|
|
47
|
+
* Rotates a refresh token: the old one is spent, a new pair comes back.
|
|
48
|
+
*
|
|
49
|
+
* Revoking before issuing, and only continuing when the revoke actually hit
|
|
50
|
+
* a live row, is the whole point — replaying a token that has already been
|
|
51
|
+
* used finds nothing to revoke and is rejected.
|
|
52
|
+
*/
|
|
53
|
+
refresh(rawRefreshToken: string): Promise<TokenPair>;
|
|
54
|
+
logout(rawRefreshToken: string): Promise<void>;
|
|
55
|
+
currentUser(userId: string): Promise<unknown>;
|
|
56
|
+
/**
|
|
57
|
+
* Issues the short-lived permission to link another provider.
|
|
58
|
+
*
|
|
59
|
+
* Needed because the OAuth leg is a whole-browser navigation, not a request
|
|
60
|
+
* with headers: the access token does not travel there. So a permission is
|
|
61
|
+
* minted, parked in an httpOnly cookie, and read back by the callback to
|
|
62
|
+
* tell "link this to me" from "sign in". It carries its own `typ` so it can
|
|
63
|
+
* never pass as an access token, or the other way round, despite sharing a
|
|
64
|
+
* secret.
|
|
65
|
+
*/
|
|
66
|
+
issueLinkToken(userId: string): {
|
|
67
|
+
token: string;
|
|
68
|
+
expiresAt: Date;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The user id behind a link permission, or null.
|
|
72
|
+
*
|
|
73
|
+
* Null rather than an exception: with no valid permission the callback just
|
|
74
|
+
* performs an ordinary sign-in, which is the correct outcome.
|
|
75
|
+
*/
|
|
76
|
+
readLinkToken(token: string | undefined): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Ties a provider account to the already-signed-in user.
|
|
79
|
+
*
|
|
80
|
+
* This is the way out when the emails do not match: if someone's Facebook
|
|
81
|
+
* is registered under a different address, linking by email can never join
|
|
82
|
+
* them, and without this they keep two profiles forever.
|
|
83
|
+
*/
|
|
84
|
+
linkAccount(userId: string, providerName: string, profile: OAuthProfile): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Which providers are linked, and what photo each one offers.
|
|
87
|
+
*
|
|
88
|
+
* Enough to build both a "connect another account" screen and an avatar
|
|
89
|
+
* picker; what the app does with the choice is the app's decision.
|
|
90
|
+
*/
|
|
91
|
+
linkedAccounts(userId: string): Promise<{
|
|
92
|
+
connected: string[];
|
|
93
|
+
avatars: {
|
|
94
|
+
provider: string;
|
|
95
|
+
avatarUrl: string;
|
|
96
|
+
}[];
|
|
97
|
+
}>;
|
|
98
|
+
private present;
|
|
99
|
+
private issueTokenPair;
|
|
100
|
+
private verifyRefresh;
|
|
101
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ArgumentsHost, type ExceptionFilter } from '@nestjs/common';
|
|
2
|
+
import { type OAuthAuthOptions } from './oauth.options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sends the browser back to the sign-in screen when an OAuth callback fails.
|
|
5
|
+
*
|
|
6
|
+
* Without it any failure falls through to the global filter, which answers
|
|
7
|
+
* JSON — and mid-navigation that leaves a person staring at
|
|
8
|
+
* `{"statusCode":500,…}` on a blank page with no way back: they did not
|
|
9
|
+
* arrive by an API call, they were redirected there by the provider.
|
|
10
|
+
*
|
|
11
|
+
* The real cause is always logged. Only the text of deliberate HTTP
|
|
12
|
+
* exceptions is passed on; an internal error gets a generic message, on the
|
|
13
|
+
* same principle the global exception filter follows.
|
|
14
|
+
*/
|
|
15
|
+
export declare class OAuthCallbackFilter implements ExceptionFilter {
|
|
16
|
+
private readonly logger;
|
|
17
|
+
private readonly options;
|
|
18
|
+
constructor(options: OAuthAuthOptions);
|
|
19
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What nestkit needs to know to run an OAuth 2.0 authorization-code flow
|
|
3
|
+
* against one provider.
|
|
4
|
+
*
|
|
5
|
+
* A provider is a plain object, not a class to subclass or a Passport strategy
|
|
6
|
+
* to install. Adding Apple, GitHub or Microsoft is declaring four URLs and a
|
|
7
|
+
* function that maps their JSON onto `OAuthProfile` — no new dependency, no
|
|
8
|
+
* new file in this library.
|
|
9
|
+
*
|
|
10
|
+
* Passport is deliberately absent. Its redirect step calls `res.setHeader()`,
|
|
11
|
+
* which does not exist on a Fastify reply, so the sign-in leg has to be
|
|
12
|
+
* hand-built anyway; and its per-provider profile mapping is an undocumented
|
|
13
|
+
* translation table that silently forwards field names it does not recognise
|
|
14
|
+
* (`photos.type(large)` reaching Facebook's Graph API as a request for the
|
|
15
|
+
* user's uploaded photos, which needs a permission the app does not have, is
|
|
16
|
+
* how that bug presents in production). The whole flow below is roughly forty
|
|
17
|
+
* lines of `fetch`, and every field it sends is visible in this file.
|
|
18
|
+
*/
|
|
19
|
+
export interface OAuthProfile {
|
|
20
|
+
/** The provider's own id for this person. Stable; the email is not. */
|
|
21
|
+
providerAccountId: string;
|
|
22
|
+
email: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
avatarUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface OAuthProviderConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Route segment and stored provider key: `/auth/google`. Whatever goes in
|
|
29
|
+
* here is what `OAuthStorePort` will receive as `provider`, so if the
|
|
30
|
+
* consumer persists an enum, these names have to match its members.
|
|
31
|
+
*/
|
|
32
|
+
name: string;
|
|
33
|
+
clientId?: string;
|
|
34
|
+
clientSecret?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Must match the redirect URI registered with the provider **exactly**,
|
|
37
|
+
* including the path prefix the app is mounted under. A mismatch is
|
|
38
|
+
* rejected by the provider, not by us.
|
|
39
|
+
*/
|
|
40
|
+
callbackUrl?: string;
|
|
41
|
+
/** Consent screen. */
|
|
42
|
+
authorizeUrl: string;
|
|
43
|
+
/** Where the `code` is exchanged for an access token. */
|
|
44
|
+
tokenUrl: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
/** Extra query parameters for the consent screen (`prompt`, `access_type`…). */
|
|
47
|
+
authorizeParams?: Record<string, string>;
|
|
48
|
+
/** Fetches the profile with the provider's access token and normalises it. */
|
|
49
|
+
fetchProfile(accessToken: string): Promise<OAuthProfile>;
|
|
50
|
+
}
|
|
51
|
+
export interface ProviderCredentials {
|
|
52
|
+
clientId?: string;
|
|
53
|
+
clientSecret?: string;
|
|
54
|
+
callbackUrl?: string;
|
|
55
|
+
/** Overrides the default scope. */
|
|
56
|
+
scope?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Google. Returns the email verified by Google itself, so it is safe to link
|
|
60
|
+
* accounts on.
|
|
61
|
+
*/
|
|
62
|
+
export declare function googleProvider(credentials?: ProviderCredentials): OAuthProviderConfig;
|
|
63
|
+
/**
|
|
64
|
+
* Facebook. Note that unlike Google it may return no email at all — the
|
|
65
|
+
* account can be registered with a phone number, or the person can withhold
|
|
66
|
+
* the permission on the consent screen. `OAuthAuthService` turns that into a
|
|
67
|
+
* message they can act on rather than an internal failure.
|
|
68
|
+
*/
|
|
69
|
+
export declare function facebookProvider(credentials?: ProviderCredentials): OAuthProviderConfig;
|
|
70
|
+
/** The consent-screen URL to send the browser to. */
|
|
71
|
+
export declare function buildAuthorizeUrl(provider: OAuthProviderConfig, state?: string): string;
|
|
72
|
+
/** Trades the `code` from the callback for the provider's access token. */
|
|
73
|
+
export declare function exchangeCodeForToken(provider: OAuthProviderConfig, code: string): Promise<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type CanActivate, type Type } from '@nestjs/common';
|
|
2
|
+
export interface CreateOAuthControllerOptions {
|
|
3
|
+
/** Route prefix. Default `auth`. */
|
|
4
|
+
path?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Guard for the routes that need a session. Defaults to nestkit's
|
|
7
|
+
* `JwtAuthGuard`.
|
|
8
|
+
*
|
|
9
|
+
* An app that already has its own guard should pass it, rather than end up
|
|
10
|
+
* with two of them verifying the same token in the same codebase — and with
|
|
11
|
+
* `request.user` shaped one way on these routes and another way everywhere
|
|
12
|
+
* else.
|
|
13
|
+
*/
|
|
14
|
+
guard?: Type<CanActivate>;
|
|
15
|
+
/**
|
|
16
|
+
* Reads the user id out of whatever the guard attached. Defaults to the
|
|
17
|
+
* `sub` claim, which is what nestkit's guard puts there; a guard that maps
|
|
18
|
+
* it to `id` needs `(user) => user.id`.
|
|
19
|
+
*/
|
|
20
|
+
userId?(user: unknown): string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generates the routes for OAuth sign-in, session refresh and account
|
|
24
|
+
* linking, wired to `OAuthAuthService`.
|
|
25
|
+
*
|
|
26
|
+
* A factory rather than a fixed class so the prefix can move, and so an app
|
|
27
|
+
* can skip it entirely and call the service from its own controller — the
|
|
28
|
+
* same composition-over-inheritance shape as `createCrudController`.
|
|
29
|
+
*
|
|
30
|
+
* `@Module({ controllers: [class AuthController extends createOAuthController() {}] })`
|
|
31
|
+
*
|
|
32
|
+
* The provider is a route parameter instead of one pair of routes per
|
|
33
|
+
* provider, so adding Apple later touches configuration only. The static
|
|
34
|
+
* routes are declared first on purpose: Fastify's router always prefers a
|
|
35
|
+
* static segment over a parametric one, but Express matches in declaration
|
|
36
|
+
* order, and `me` would otherwise be read as a provider name.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createOAuthController(options?: CreateOAuthControllerOptions): Type<object>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { OAuthProviderConfig } from './oauth-provider.js';
|
|
2
|
+
import type { OAuthUser, ProfileSyncPolicy } from './oauth.ports.js';
|
|
3
|
+
export declare const OAUTH_AUTH_OPTIONS: unique symbol;
|
|
4
|
+
/** Bind to an implementation of `OAuthStorePort`. */
|
|
5
|
+
export declare const OAUTH_STORE: unique symbol;
|
|
6
|
+
/** Bind to an implementation of `RefreshTokenStorePort`. */
|
|
7
|
+
export declare const REFRESH_TOKEN_STORE: unique symbol;
|
|
8
|
+
export interface OAuthCookieOptions {
|
|
9
|
+
/** Default `refresh_token`. */
|
|
10
|
+
name?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Default `/auth`. Scope it as narrowly as the routes allow: the cookie is
|
|
13
|
+
* then never sent to the rest of the API, which is the cheapest CSRF
|
|
14
|
+
* mitigation available. Apps mounted under a prefix want `/api/auth`.
|
|
15
|
+
*/
|
|
16
|
+
path?: string;
|
|
17
|
+
/** Default `lax` — has to survive the redirect back from the provider. */
|
|
18
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
19
|
+
/** Default: on outside development. */
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface OAuthAuthOptions<U extends OAuthUser = OAuthUser> {
|
|
23
|
+
/** Every provider the app offers. Order is irrelevant; `name` is the key. */
|
|
24
|
+
providers: OAuthProviderConfig[];
|
|
25
|
+
/** Front-end origin. Used to build the default redirects below. */
|
|
26
|
+
webUrl: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where the browser lands after signing in. The access token cannot go in a
|
|
29
|
+
* cookie — the client has to read it — and it must not go in the query
|
|
30
|
+
* string, which is logged by proxies and kept in browser history. The
|
|
31
|
+
* default puts it in the fragment, which never leaves the browser.
|
|
32
|
+
*/
|
|
33
|
+
onLoginRedirect?(accessToken: string): string;
|
|
34
|
+
/** Where a failed callback lands. Default `${webUrl}/login?error=…`. */
|
|
35
|
+
onErrorRedirect?(message: string): string;
|
|
36
|
+
/** Where a finished account link lands. Default `${webUrl}/profile?…`. */
|
|
37
|
+
onLinkRedirect?(provider: string, error?: string): string;
|
|
38
|
+
cookie?: OAuthCookieOptions;
|
|
39
|
+
/**
|
|
40
|
+
* Cookie carrying the short-lived permission to link another provider to an
|
|
41
|
+
* already signed-in account. Default `link_intent`.
|
|
42
|
+
*/
|
|
43
|
+
linkCookieName?: string;
|
|
44
|
+
/** Defaults: 15 min access, 30 days refresh, 10 min link permission. */
|
|
45
|
+
accessTtlSeconds?: number;
|
|
46
|
+
refreshTtlSeconds?: number;
|
|
47
|
+
linkTtlSeconds?: number;
|
|
48
|
+
/** Read from `JWT_ACCESS_SECRET` / `JWT_REFRESH_SECRET` when omitted. */
|
|
49
|
+
accessSecret?: string;
|
|
50
|
+
refreshSecret?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether signing in closes every other session. Default `true`.
|
|
53
|
+
*
|
|
54
|
+
* Right for a phone-and-laptop consumer app, wrong for anything people use
|
|
55
|
+
* in several browsers at once — there it logs them out of the other one on
|
|
56
|
+
* every sign-in, which reads as a bug.
|
|
57
|
+
*/
|
|
58
|
+
singleSession?: boolean;
|
|
59
|
+
/** See `ProfileSyncPolicy`. Default resyncs name and photo every login. */
|
|
60
|
+
syncProfile?: ProfileSyncPolicy<U>;
|
|
61
|
+
/**
|
|
62
|
+
* Shapes the user in API responses. Without it the stored row goes out
|
|
63
|
+
* whole, password hashes and internal columns included — so anything with
|
|
64
|
+
* fields not meant to be public should pass one.
|
|
65
|
+
*/
|
|
66
|
+
mapUser?(user: U): unknown;
|
|
67
|
+
/**
|
|
68
|
+
* Message when a provider is offered but not configured. Reaches the user,
|
|
69
|
+
* so it belongs in the app's language.
|
|
70
|
+
*/
|
|
71
|
+
unavailableMessage?(provider: string): string;
|
|
72
|
+
/** Message when the provider hands back no email. */
|
|
73
|
+
missingEmailMessage?(provider: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Shown when the callback fails for any reason the user cannot be told
|
|
76
|
+
* about — an internal error, or a consent screen they cancelled. Reaches
|
|
77
|
+
* the user, so it belongs in the app's language.
|
|
78
|
+
*/
|
|
79
|
+
genericErrorMessage?(): string;
|
|
80
|
+
}
|
|
81
|
+
/** Everything above, with the defaults filled in. Internal. */
|
|
82
|
+
export interface ResolvedOAuthOptions<U extends OAuthUser = OAuthUser> extends Required<Omit<OAuthAuthOptions<U>, 'accessSecret' | 'refreshSecret' | 'cookie' | 'mapUser' | 'syncProfile'>> {
|
|
83
|
+
accessSecret?: string;
|
|
84
|
+
refreshSecret?: string;
|
|
85
|
+
cookie: Required<OAuthCookieOptions>;
|
|
86
|
+
mapUser?(user: U): unknown;
|
|
87
|
+
syncProfile: ProfileSyncPolicy<U>;
|
|
88
|
+
}
|
|
89
|
+
export declare function resolveOAuthOptions<U extends OAuthUser>(options: OAuthAuthOptions<U>): ResolvedOAuthOptions<U>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OAuthProfile } from './oauth-provider.js';
|
|
2
|
+
/**
|
|
3
|
+
* The minimum nestkit needs a user to have. Consumers keep their own columns
|
|
4
|
+
* — the service passes whatever the port returns straight back out, so the
|
|
5
|
+
* app's `subscriptionTier`, `roles` or `tenantId` survive untouched.
|
|
6
|
+
*/
|
|
7
|
+
export interface OAuthUser {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name?: string | null;
|
|
11
|
+
avatarUrl?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface OAuthAccount {
|
|
14
|
+
userId: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
providerAccountId: string;
|
|
17
|
+
avatarUrl: string | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Persistence for identities: who someone is, and which provider accounts
|
|
21
|
+
* point at them.
|
|
22
|
+
*
|
|
23
|
+
* Kept apart from `RefreshTokenStorePort` because they answer different
|
|
24
|
+
* questions — identity outlives any session, and a project may well want the
|
|
25
|
+
* sessions in Redis and the users in Postgres.
|
|
26
|
+
*/
|
|
27
|
+
export interface OAuthStorePort<U extends OAuthUser = OAuthUser> {
|
|
28
|
+
/** The linked account, with its user — the fast path of every login. */
|
|
29
|
+
findAccount(provider: string, providerAccountId: string): Promise<(OAuthAccount & {
|
|
30
|
+
user: U;
|
|
31
|
+
}) | null>;
|
|
32
|
+
/** Used to refuse linking a second account of a provider already linked. */
|
|
33
|
+
findAccountByUserAndProvider(userId: string, provider: string): Promise<OAuthAccount | null>;
|
|
34
|
+
/** Everything linked to this user — what the avatar picker is built from. */
|
|
35
|
+
listAccountsByUser(userId: string): Promise<OAuthAccount[]>;
|
|
36
|
+
createAccount(account: OAuthAccount): Promise<void>;
|
|
37
|
+
updateAccountAvatar(provider: string, providerAccountId: string, avatarUrl: string | null): Promise<void>;
|
|
38
|
+
/** Must compare on the normalised (trimmed, lower-cased) email. */
|
|
39
|
+
findUserByEmail(email: string): Promise<U | null>;
|
|
40
|
+
findUserById(id: string): Promise<U | null>;
|
|
41
|
+
createUser(data: {
|
|
42
|
+
email: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
avatarUrl?: string;
|
|
45
|
+
}): Promise<U>;
|
|
46
|
+
updateUser(id: string, data: {
|
|
47
|
+
name?: string | null;
|
|
48
|
+
avatarUrl?: string | null;
|
|
49
|
+
}): Promise<U>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Persistence for sessions.
|
|
53
|
+
*
|
|
54
|
+
* Only ever sees a SHA-256 of the token, never the token: a leaked database
|
|
55
|
+
* then yields nothing that can be replayed. Rotation is what makes that
|
|
56
|
+
* worth doing — `revokeIfActive` has to be atomic (a conditional UPDATE, not
|
|
57
|
+
* read-then-write), because that single row is what stops a stolen refresh
|
|
58
|
+
* token from being used twice.
|
|
59
|
+
*/
|
|
60
|
+
export interface RefreshTokenStorePort {
|
|
61
|
+
create(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Revokes the token if it is still active, and reports whether it was.
|
|
64
|
+
* A count of 0 means already used, revoked or unknown — all of which are
|
|
65
|
+
* a rejected refresh.
|
|
66
|
+
*/
|
|
67
|
+
revokeIfActive(tokenHash: string): Promise<{
|
|
68
|
+
count: number;
|
|
69
|
+
}>;
|
|
70
|
+
/** Closes every other session. Called on login when `singleSession`. */
|
|
71
|
+
revokeAllForUser(userId: string): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decides what a fresh sign-in is allowed to overwrite on the stored user.
|
|
75
|
+
*
|
|
76
|
+
* The default resyncs name and photo from the provider on every login, which
|
|
77
|
+
* is right until the app lets people choose their own — at which point the
|
|
78
|
+
* default silently undoes their choice the next time they sign in. Apps with
|
|
79
|
+
* a picker return the values they want kept instead.
|
|
80
|
+
*
|
|
81
|
+
* Returning `null` means "change nothing".
|
|
82
|
+
*/
|
|
83
|
+
export type ProfileSyncPolicy<U extends OAuthUser = OAuthUser> = (user: U, profile: OAuthProfile) => {
|
|
84
|
+
name?: string | null;
|
|
85
|
+
avatarUrl?: string | null;
|
|
86
|
+
} | null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UnauthorizedAppException
|
|
3
|
+
} from "./chunk-ORWJ7LES.js";
|
|
4
|
+
import {
|
|
5
|
+
__decorateClass
|
|
6
|
+
} from "./chunk-4MGIQFAJ.js";
|
|
7
|
+
|
|
8
|
+
// src/auth/token.service.ts
|
|
9
|
+
import jwt from "jsonwebtoken";
|
|
10
|
+
function requireEnv(name) {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (!value) {
|
|
13
|
+
throw new Error(`TokenService: ${name} environment variable is not set`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
var TokenService = class {
|
|
18
|
+
signAccessToken(payload) {
|
|
19
|
+
return jwt.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
|
|
20
|
+
expiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? "15m"
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
signRefreshToken(payload) {
|
|
24
|
+
return jwt.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
|
|
25
|
+
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? "30d"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
verifyAccessToken(token) {
|
|
29
|
+
try {
|
|
30
|
+
return jwt.verify(token, requireEnv("JWT_ACCESS_SECRET"));
|
|
31
|
+
} catch {
|
|
32
|
+
throw new UnauthorizedAppException("Invalid or expired access token");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
verifyRefreshToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
return jwt.verify(token, requireEnv("JWT_REFRESH_SECRET"));
|
|
38
|
+
} catch {
|
|
39
|
+
throw new UnauthorizedAppException("Invalid or expired refresh token");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/auth/jwt-auth.guard.ts
|
|
45
|
+
import { Injectable } from "@nestjs/common";
|
|
46
|
+
var JwtAuthGuard = class {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.tokens = new TokenService();
|
|
49
|
+
}
|
|
50
|
+
canActivate(context) {
|
|
51
|
+
const req = context.switchToHttp().getRequest();
|
|
52
|
+
const header = req.headers.authorization;
|
|
53
|
+
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : void 0;
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new UnauthorizedAppException("Missing Authorization bearer token");
|
|
56
|
+
}
|
|
57
|
+
req.user = this.tokens.verifyAccessToken(token);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
JwtAuthGuard = __decorateClass([
|
|
62
|
+
Injectable()
|
|
63
|
+
], JwtAuthGuard);
|
|
64
|
+
|
|
65
|
+
// src/auth/current-user.decorator.ts
|
|
66
|
+
import { createParamDecorator } from "@nestjs/common";
|
|
67
|
+
var CurrentUser = createParamDecorator(
|
|
68
|
+
(_, ctx) => ctx.switchToHttp().getRequest().user
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
TokenService,
|
|
73
|
+
JwtAuthGuard,
|
|
74
|
+
CurrentUser
|
|
75
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkFDNGAYTZcjs = require('./chunk-FDNGAYTZ.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunk2REOCMUDcjs = require('./chunk-2REOCMUD.cjs');
|
|
7
|
+
|
|
8
|
+
// src/auth/token.service.ts
|
|
9
|
+
var _jsonwebtoken = require('jsonwebtoken'); var _jsonwebtoken2 = _interopRequireDefault(_jsonwebtoken);
|
|
10
|
+
function requireEnv(name) {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (!value) {
|
|
13
|
+
throw new Error(`TokenService: ${name} environment variable is not set`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
var TokenService = class {
|
|
18
|
+
signAccessToken(payload) {
|
|
19
|
+
return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_ACCESS_SECRET"), {
|
|
20
|
+
expiresIn: _nullishCoalesce(process.env.JWT_ACCESS_EXPIRES_IN, () => ( "15m"))
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
signRefreshToken(payload) {
|
|
24
|
+
return _jsonwebtoken2.default.sign(payload, requireEnv("JWT_REFRESH_SECRET"), {
|
|
25
|
+
expiresIn: _nullishCoalesce(process.env.JWT_REFRESH_EXPIRES_IN, () => ( "30d"))
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
verifyAccessToken(token) {
|
|
29
|
+
try {
|
|
30
|
+
return _jsonwebtoken2.default.verify(token, requireEnv("JWT_ACCESS_SECRET"));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired access token");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
verifyRefreshToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
return _jsonwebtoken2.default.verify(token, requireEnv("JWT_REFRESH_SECRET"));
|
|
38
|
+
} catch (e2) {
|
|
39
|
+
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Invalid or expired refresh token");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/auth/jwt-auth.guard.ts
|
|
45
|
+
var _common = require('@nestjs/common');
|
|
46
|
+
var JwtAuthGuard = class {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.tokens = new TokenService();
|
|
49
|
+
}
|
|
50
|
+
canActivate(context) {
|
|
51
|
+
const req = context.switchToHttp().getRequest();
|
|
52
|
+
const header = req.headers.authorization;
|
|
53
|
+
const token = _optionalChain([header, 'optionalAccess', _2 => _2.startsWith, 'call', _3 => _3("Bearer ")]) ? header.slice("Bearer ".length) : void 0;
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new (0, _chunkFDNGAYTZcjs.UnauthorizedAppException)("Missing Authorization bearer token");
|
|
56
|
+
}
|
|
57
|
+
req.user = this.tokens.verifyAccessToken(token);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
JwtAuthGuard = exports.JwtAuthGuard = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
62
|
+
_common.Injectable.call(void 0, )
|
|
63
|
+
], JwtAuthGuard);
|
|
64
|
+
|
|
65
|
+
// src/auth/current-user.decorator.ts
|
|
66
|
+
|
|
67
|
+
var CurrentUser = _common.createParamDecorator.call(void 0,
|
|
68
|
+
(_, ctx) => ctx.switchToHttp().getRequest().user
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
exports.TokenService = TokenService; exports.JwtAuthGuard = JwtAuthGuard; exports.CurrentUser = CurrentUser;
|
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});require('./chunk-KVBQBT3D.cjs');
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
|
|
6
|
-
require('./chunk-KVBQBT3D.cjs');
|
|
7
5
|
|
|
6
|
+
var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
|
|
11
|
+
var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
|
|
15
12
|
require('./chunk-MR2IFCZE.cjs');
|
|
16
13
|
|
|
17
14
|
|
|
@@ -28,6 +25,13 @@ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
|
|
|
28
25
|
|
|
29
26
|
|
|
30
27
|
var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
|
|
31
35
|
require('./chunk-7SOM7EZP.cjs');
|
|
32
36
|
|
|
33
37
|
|
|
@@ -45,10 +49,6 @@ var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
|
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
|
|
52
52
|
require('./chunk-2REOCMUD.cjs');
|
|
53
53
|
|
|
54
54
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PG_POOL,
|
|
3
|
-
PgModule,
|
|
4
|
-
withTenantScope
|
|
5
|
-
} from "./chunk-VKOPDDCC.js";
|
|
6
1
|
import "./chunk-EPVKCBPT.js";
|
|
7
2
|
import {
|
|
8
3
|
SUPABASE_ANON_CLIENT,
|
|
@@ -10,8 +5,10 @@ import {
|
|
|
10
5
|
SupabaseModule
|
|
11
6
|
} from "./chunk-PA24P76K.js";
|
|
12
7
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
PG_POOL,
|
|
9
|
+
PgModule,
|
|
10
|
+
withTenantScope
|
|
11
|
+
} from "./chunk-VKOPDDCC.js";
|
|
15
12
|
import "./chunk-NAK4WDKS.js";
|
|
16
13
|
import {
|
|
17
14
|
REQUEST_ID_HEADER,
|
|
@@ -28,6 +25,13 @@ import {
|
|
|
28
25
|
BaseCrudService,
|
|
29
26
|
createCrudController
|
|
30
27
|
} from "./chunk-JOVBJDJ2.js";
|
|
28
|
+
import {
|
|
29
|
+
BaseResponseDto
|
|
30
|
+
} from "./chunk-XX2HPTRU.js";
|
|
31
|
+
import {
|
|
32
|
+
I18nModule,
|
|
33
|
+
translateOr
|
|
34
|
+
} from "./chunk-IYUUYCP5.js";
|
|
31
35
|
import "./chunk-DQYAIQQ5.js";
|
|
32
36
|
import {
|
|
33
37
|
ConflictAppException,
|
|
@@ -45,10 +49,6 @@ import {
|
|
|
45
49
|
import {
|
|
46
50
|
AppException
|
|
47
51
|
} from "./chunk-YFYHLYHN.js";
|
|
48
|
-
import {
|
|
49
|
-
I18nModule,
|
|
50
|
-
translateOr
|
|
51
|
-
} from "./chunk-IYUUYCP5.js";
|
|
52
52
|
import "./chunk-4MGIQFAJ.js";
|
|
53
53
|
export {
|
|
54
54
|
AppException,
|