@warlock.js/auth 5.11.0 → 5.13.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/CHANGELOG.md +65 -0
- package/README.md +88 -86
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/contracts/index.d.mts +4 -2
- package/esm/contracts/providers.d.mts +101 -0
- package/esm/contracts/providers.d.mts.map +1 -0
- package/esm/contracts/request-locals.d.mts +43 -0
- package/esm/contracts/request-locals.d.mts.map +1 -0
- package/esm/contracts/types.d.mts +159 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
- package/esm/errors/email-not-verified.error.d.mts +16 -0
- package/esm/errors/email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/email-not-verified.error.mjs +20 -0
- package/esm/errors/email-not-verified.error.mjs.map +1 -0
- package/esm/errors/index.d.mts +7 -0
- package/esm/errors/index.mjs +9 -0
- package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
- package/esm/errors/invalid-one-time-token.error.mjs +21 -0
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
- package/esm/errors/invalid-passkey.error.d.mts +18 -0
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
- package/esm/errors/invalid-passkey.error.mjs +23 -0
- package/esm/errors/invalid-passkey.error.mjs.map +1 -0
- package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
- package/esm/errors/invalid-provider-callback.error.mjs +23 -0
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
- package/esm/errors/notifications-unavailable.error.d.mts +15 -0
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
- package/esm/errors/notifications-unavailable.error.mjs +20 -0
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
- package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/provider-email-not-verified.error.mjs +20 -0
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
- package/esm/index.d.mts +31 -3
- package/esm/index.mjs +29 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +13 -2
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +40 -0
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
- package/esm/middleware/csrf-origin-check.mjs +105 -0
- package/esm/middleware/csrf-origin-check.mjs.map +1 -0
- package/esm/middleware/index.d.mts +5 -1
- package/esm/middleware/index.mjs +4 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/otp-throttle.middleware.mjs +45 -0
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
- package/esm/middleware/require-verified-email.middleware.mjs +24 -0
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
- package/esm/models/access-token/access-token.model.d.mts +8 -0
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +3 -0
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/index.d.mts +3 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +16 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/index.d.mts +1 -0
- package/esm/models/one-time-token/index.mjs +3 -0
- package/esm/models/one-time-token/migration.mjs +31 -0
- package/esm/models/one-time-token/migration.mjs.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
- package/esm/models/passkey-credential/index.d.mts +1 -0
- package/esm/models/passkey-credential/index.mjs +3 -0
- package/esm/models/passkey-credential/migration.mjs +25 -0
- package/esm/models/passkey-credential/migration.mjs.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
- package/esm/models/provider-account/index.d.mts +1 -0
- package/esm/models/provider-account/index.mjs +3 -0
- package/esm/models/provider-account/migration.mjs +25 -0
- package/esm/models/provider-account/migration.mjs.map +1 -0
- package/esm/models/provider-account/provider-account.model.d.mts +75 -0
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
- package/esm/models/provider-account/provider-account.model.mjs +55 -0
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/otp/index.d.mts +1 -0
- package/esm/otp/index.mjs +3 -0
- package/esm/otp/otp.d.mts +38 -0
- package/esm/otp/otp.d.mts.map +1 -0
- package/esm/otp/otp.mjs +105 -0
- package/esm/otp/otp.mjs.map +1 -0
- package/esm/passkeys/index.d.mts +3 -0
- package/esm/passkeys/index.mjs +4 -0
- package/esm/passkeys/passkey-authentication.d.mts +33 -0
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
- package/esm/passkeys/passkey-authentication.mjs +93 -0
- package/esm/passkeys/passkey-authentication.mjs.map +1 -0
- package/esm/passkeys/passkey-challenges.mjs +53 -0
- package/esm/passkeys/passkey-challenges.mjs.map +1 -0
- package/esm/passkeys/passkey-registration.d.mts +29 -0
- package/esm/passkeys/passkey-registration.d.mts.map +1 -0
- package/esm/passkeys/passkey-registration.mjs +88 -0
- package/esm/passkeys/passkey-registration.mjs.map +1 -0
- package/esm/passkeys/simplewebauthn.d.mts +21 -0
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
- package/esm/passkeys/simplewebauthn.mjs +13 -0
- package/esm/passkeys/simplewebauthn.mjs.map +1 -0
- package/esm/providers/google-provider.d.mts +40 -0
- package/esm/providers/google-provider.d.mts.map +1 -0
- package/esm/providers/google-provider.mjs +105 -0
- package/esm/providers/google-provider.mjs.map +1 -0
- package/esm/providers/index.d.mts +2 -0
- package/esm/providers/index.mjs +4 -0
- package/esm/providers/provider-login.d.mts +43 -0
- package/esm/providers/provider-login.d.mts.map +1 -0
- package/esm/providers/provider-login.mjs +86 -0
- package/esm/providers/provider-login.mjs.map +1 -0
- package/esm/providers/provider-state-cookie.mjs +63 -0
- package/esm/providers/provider-state-cookie.mjs.map +1 -0
- package/esm/providers/resolve-provider-user.mjs +45 -0
- package/esm/providers/resolve-provider-user.mjs.map +1 -0
- package/esm/services/auth-config.mjs +84 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +59 -2
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +86 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +21 -0
- package/esm/services/current-user.d.mts.map +1 -0
- package/esm/services/current-user.mjs +24 -0
- package/esm/services/current-user.mjs.map +1 -0
- package/esm/services/email-verification.d.mts +35 -0
- package/esm/services/email-verification.d.mts.map +1 -0
- package/esm/services/email-verification.mjs +55 -0
- package/esm/services/email-verification.mjs.map +1 -0
- package/esm/services/index.d.mts +5 -1
- package/esm/services/index.mjs +4 -0
- package/esm/services/one-time-tokens.d.mts +32 -0
- package/esm/services/one-time-tokens.d.mts.map +1 -0
- package/esm/services/one-time-tokens.mjs +56 -0
- package/esm/services/one-time-tokens.mjs.map +1 -0
- package/esm/services/optional-peer.mjs +37 -0
- package/esm/services/optional-peer.mjs.map +1 -0
- package/esm/services/password-reset.d.mts +34 -0
- package/esm/services/password-reset.d.mts.map +1 -0
- package/esm/services/password-reset.mjs +81 -0
- package/esm/services/password-reset.mjs.map +1 -0
- package/esm/services/token-notifications.mjs +87 -0
- package/esm/services/token-notifications.mjs.map +1 -0
- package/esm/utils/auth-error-codes.d.mts +48 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +41 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/index.mjs +1 -0
- package/esm/utils/safe-equal.d.mts +6 -0
- package/esm/utils/safe-equal.d.mts.map +1 -0
- package/esm/utils/safe-equal.mjs +13 -0
- package/esm/utils/safe-equal.mjs.map +1 -0
- package/llms-full.txt +402 -29
- package/llms.txt +2 -0
- package/package.json +21 -7
- package/skills/handle-login-and-logout/SKILL.md +1 -1
- package/skills/login-with-providers/SKILL.md +154 -0
- package/skills/manage-tokens/SKILL.md +7 -1
- package/skills/overview/SKILL.md +1 -1
- package/skills/protect-routes/SKILL.md +71 -13
- package/skills/register-user/SKILL.md +6 -8
- package/skills/run-auth-commands/SKILL.md +1 -1
- package/skills/verify-email-and-reset-password/SKILL.md +137 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { authConfig } from "../services/auth-config.mjs";
|
|
2
|
+
import { safeEqual } from "../utils/safe-equal.mjs";
|
|
3
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region ../auth/src/providers/provider-state-cookie.ts
|
|
6
|
+
/** Cookie carrying a redirect login's state from the start to the callback. */
|
|
7
|
+
const PROVIDER_STATE_COOKIE = "auth_provider_state";
|
|
8
|
+
/** How long a started provider login stays completable. */
|
|
9
|
+
const PROVIDER_STATE_TTL_SECONDS = 600;
|
|
10
|
+
/** 32 random bytes, base64url — used for `state`, `nonce` and the PKCE verifier. */
|
|
11
|
+
function randomUrlSafe() {
|
|
12
|
+
return randomBytes(32).toString("base64url");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* HMAC over the payload with a key DERIVED from the access-token secret, so
|
|
16
|
+
* the state cookie never shares a MAC key with the JWTs themselves.
|
|
17
|
+
*/
|
|
18
|
+
function sign(payload) {
|
|
19
|
+
return createHmac("sha256", createHmac("sha256", authConfig.accessToken.secret()).update("@warlock.js/auth:provider-state").digest()).update(payload).digest("base64url");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Write the signed, 10-minute state cookie for a provider login. Attribute
|
|
23
|
+
* flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come from the
|
|
24
|
+
* `response.cookie()` secure defaults; `Lax` still rides the provider's
|
|
25
|
+
* top-level GET redirect back to the app.
|
|
26
|
+
*/
|
|
27
|
+
function writeProviderState(response, provider, state) {
|
|
28
|
+
const stored = {
|
|
29
|
+
...state,
|
|
30
|
+
provider,
|
|
31
|
+
expiresAt: Date.now() + PROVIDER_STATE_TTL_SECONDS * 1e3
|
|
32
|
+
};
|
|
33
|
+
const payload = Buffer.from(JSON.stringify(stored)).toString("base64url");
|
|
34
|
+
response.cookie(PROVIDER_STATE_COOKIE, `${payload}.${sign(payload)}`, {
|
|
35
|
+
raw: true,
|
|
36
|
+
path: "/",
|
|
37
|
+
maxAge: PROVIDER_STATE_TTL_SECONDS
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read AND clear the state cookie, so a started login completes at most once.
|
|
42
|
+
* Resolves `undefined` for an absent, forged (bad MAC), malformed or expired
|
|
43
|
+
* cookie.
|
|
44
|
+
*/
|
|
45
|
+
function takeProviderState(request, response) {
|
|
46
|
+
const raw = request.cookie(PROVIDER_STATE_COOKIE);
|
|
47
|
+
response.clearCookie(PROVIDER_STATE_COOKIE, { path: "/" });
|
|
48
|
+
if (typeof raw !== "string") return void 0;
|
|
49
|
+
const [payload, signature, ...rest] = raw.split(".");
|
|
50
|
+
if (!payload || !signature || rest.length > 0 || !safeEqual(signature, sign(payload))) return;
|
|
51
|
+
let stored;
|
|
52
|
+
try {
|
|
53
|
+
stored = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (typeof stored?.state !== "string" || typeof stored.nonce !== "string" || typeof stored.codeVerifier !== "string" || typeof stored.provider !== "string" || typeof stored.expiresAt !== "number" || stored.expiresAt <= Date.now()) return;
|
|
58
|
+
return stored;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { randomUrlSafe, takeProviderState, writeProviderState };
|
|
63
|
+
//# sourceMappingURL=provider-state-cookie.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-state-cookie.mjs","names":[],"sources":["../../../../../../../auth/src/providers/provider-state-cookie.ts"],"sourcesContent":["import type { Request, Response } from \"@warlock.js/core\";\nimport { createHmac, randomBytes } from \"node:crypto\";\nimport type { ProviderAuthorizationState } from \"../contracts/providers\";\nimport { authConfig } from \"../services/auth-config\";\nimport { safeEqual } from \"../utils/safe-equal\";\n\n/** Cookie carrying a redirect login's state from the start to the callback. */\nexport const PROVIDER_STATE_COOKIE = \"auth_provider_state\";\n\n/** How long a started provider login stays completable. */\nconst PROVIDER_STATE_TTL_SECONDS = 600;\n\n/** What the signed cookie holds. */\nexport type StoredProviderState = ProviderAuthorizationState & {\n provider: string;\n expiresAt: number;\n};\n\n/** 32 random bytes, base64url — used for `state`, `nonce` and the PKCE verifier. */\nexport function randomUrlSafe(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/**\n * HMAC over the payload with a key DERIVED from the access-token secret, so\n * the state cookie never shares a MAC key with the JWTs themselves.\n */\nfunction sign(payload: string): string {\n const key = createHmac(\"sha256\", authConfig.accessToken.secret())\n .update(\"@warlock.js/auth:provider-state\")\n .digest();\n\n return createHmac(\"sha256\", key).update(payload).digest(\"base64url\");\n}\n\n/**\n * Write the signed, 10-minute state cookie for a provider login. Attribute\n * flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come from the\n * `response.cookie()` secure defaults; `Lax` still rides the provider's\n * top-level GET redirect back to the app.\n */\nexport function writeProviderState(\n response: Response,\n provider: string,\n state: ProviderAuthorizationState,\n): void {\n const stored: StoredProviderState = {\n ...state,\n provider,\n expiresAt: Date.now() + PROVIDER_STATE_TTL_SECONDS * 1000,\n };\n const payload = Buffer.from(JSON.stringify(stored)).toString(\"base64url\");\n\n response.cookie(PROVIDER_STATE_COOKIE, `${payload}.${sign(payload)}`, {\n raw: true,\n path: \"/\",\n maxAge: PROVIDER_STATE_TTL_SECONDS,\n });\n}\n\n/**\n * Read AND clear the state cookie, so a started login completes at most once.\n * Resolves `undefined` for an absent, forged (bad MAC), malformed or expired\n * cookie.\n */\nexport function takeProviderState(\n request: Request,\n response: Response,\n): StoredProviderState | undefined {\n const raw: unknown = request.cookie(PROVIDER_STATE_COOKIE);\n\n response.clearCookie(PROVIDER_STATE_COOKIE, { path: \"/\" });\n\n if (typeof raw !== \"string\") return undefined;\n\n const [payload, signature, ...rest] = raw.split(\".\");\n\n if (!payload || !signature || rest.length > 0 || !safeEqual(signature, sign(payload))) {\n return undefined;\n }\n\n let stored: Partial<StoredProviderState> | undefined;\n\n try {\n stored = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\"));\n } catch {\n return undefined;\n }\n\n if (\n typeof stored?.state !== \"string\" ||\n typeof stored.nonce !== \"string\" ||\n typeof stored.codeVerifier !== \"string\" ||\n typeof stored.provider !== \"string\" ||\n typeof stored.expiresAt !== \"number\" ||\n stored.expiresAt <= Date.now()\n ) {\n return undefined;\n }\n\n return stored as StoredProviderState;\n}\n"],"mappings":";;;;;;AAOA,MAAa,wBAAwB;;AAGrC,MAAM,6BAA6B;;AASnC,SAAgB,gBAAwB;CACtC,OAAO,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;AAC7C;;;;;AAMA,SAAS,KAAK,SAAyB;CAKrC,OAAO,WAAW,UAJN,WAAW,UAAU,WAAW,YAAY,OAAO,CAAC,CAAC,CAC9D,OAAO,iCAAiC,CAAC,CACzC,OAE2B,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,OAAO,WAAW;AACrE;;;;;;;AAQA,SAAgB,mBACd,UACA,UACA,OACM;CACN,MAAM,SAA8B;EAClC,GAAG;EACH;EACA,WAAW,KAAK,IAAI,IAAI,6BAA6B;CACvD;CACA,MAAM,UAAU,OAAO,KAAK,KAAK,UAAU,MAAM,CAAC,CAAC,CAAC,SAAS,WAAW;CAExE,SAAS,OAAO,uBAAuB,GAAG,QAAQ,GAAG,KAAK,OAAO,KAAK;EACpE,KAAK;EACL,MAAM;EACN,QAAQ;CACV,CAAC;AACH;;;;;;AAOA,SAAgB,kBACd,SACA,UACiC;CACjC,MAAM,MAAe,QAAQ,OAAO,qBAAqB;CAEzD,SAAS,YAAY,uBAAuB,EAAE,MAAM,IAAI,CAAC;CAEzD,IAAI,OAAO,QAAQ,UAAU,OAAO;CAEpC,MAAM,CAAC,SAAS,WAAW,GAAG,QAAQ,IAAI,MAAM,GAAG;CAEnD,IAAI,CAAC,WAAW,CAAC,aAAa,KAAK,SAAS,KAAK,CAAC,UAAU,WAAW,KAAK,OAAO,CAAC,GAClF;CAGF,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;CACxE,QAAQ;EACN;CACF;CAEA,IACE,OAAO,QAAQ,UAAU,YACzB,OAAO,OAAO,UAAU,YACxB,OAAO,OAAO,iBAAiB,YAC/B,OAAO,OAAO,aAAa,YAC3B,OAAO,OAAO,cAAc,YAC5B,OAAO,aAAa,KAAK,IAAI,GAE7B;CAGF,OAAO;AACT"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { authConfig } from "../services/auth-config.mjs";
|
|
2
|
+
import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
|
|
3
|
+
import { ProviderEmailNotVerifiedError } from "../errors/provider-email-not-verified.error.mjs";
|
|
4
|
+
import { ProviderAccount } from "../models/provider-account/provider-account.model.mjs";
|
|
5
|
+
import "../models/provider-account/index.mjs";
|
|
6
|
+
|
|
7
|
+
//#region ../auth/src/providers/resolve-provider-user.ts
|
|
8
|
+
/** Default creation for a first provider login: email, name, and the verified stamp. */
|
|
9
|
+
async function createUserFor(Model, profile) {
|
|
10
|
+
const createUser = authConfig.providers.createUser();
|
|
11
|
+
if (createUser) return await createUser(profile, Model);
|
|
12
|
+
return await Model.create({
|
|
13
|
+
[authConfig.providers.emailField()]: profile.email,
|
|
14
|
+
...profile.name ? { name: profile.name } : {},
|
|
15
|
+
[authConfig.verification.field()]: /* @__PURE__ */ new Date()
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find or create the app user for a provider profile, and link them.
|
|
20
|
+
*
|
|
21
|
+
* 1. An existing `provider_accounts` link wins — the provider's stable id, not
|
|
22
|
+
* the email, identifies the account from then on.
|
|
23
|
+
* 2. Otherwise the provider email MUST be verified. An unverified (or absent)
|
|
24
|
+
* email throws {@link ProviderEmailNotVerifiedError} and nothing is linked
|
|
25
|
+
* or created: linking on it would let anyone who can add an unverified
|
|
26
|
+
* address at the provider take over the matching account.
|
|
27
|
+
* 3. Verified: the user whose `auth.providers.emailField` matches, else a new
|
|
28
|
+
* user (`auth.providers.createUser`, or the default) — then the link is stored.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveProviderUser(Model, profile) {
|
|
31
|
+
const link = await ProviderAccount.findLink(profile.provider, profile.providerUserId);
|
|
32
|
+
if (link) {
|
|
33
|
+
const linked = await Model.find(link.userId);
|
|
34
|
+
if (!linked || linked.userType !== link.userType) throw new InvalidProviderCallbackError("link-points-to-missing-user");
|
|
35
|
+
return linked;
|
|
36
|
+
}
|
|
37
|
+
if (!profile.emailVerified || !profile.email) throw new ProviderEmailNotVerifiedError();
|
|
38
|
+
const user = await Model.first({ [authConfig.providers.emailField()]: profile.email }) ?? await createUserFor(Model, profile);
|
|
39
|
+
await ProviderAccount.link(profile.provider, profile.providerUserId, user, profile.email);
|
|
40
|
+
return user;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { resolveProviderUser };
|
|
45
|
+
//# sourceMappingURL=resolve-provider-user.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-provider-user.mjs","names":[],"sources":["../../../../../../../auth/src/providers/resolve-provider-user.ts"],"sourcesContent":["import type { ChildModel } from \"@warlock.js/cascade\";\nimport type { ProviderProfile } from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { ProviderEmailNotVerifiedError } from \"../errors/provider-email-not-verified.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { ProviderAccount } from \"../models/provider-account\";\nimport { authConfig } from \"../services/auth-config\";\n\n/** Default creation for a first provider login: email, name, and the verified stamp. */\nasync function createUserFor<T extends Auth>(\n Model: ChildModel<T>,\n profile: ProviderProfile,\n): Promise<T> {\n const createUser = authConfig.providers.createUser();\n\n if (createUser) {\n return (await createUser(profile, Model as unknown as ChildModel<Auth>)) as T;\n }\n\n return (await Model.create({\n [authConfig.providers.emailField()]: profile.email,\n ...(profile.name ? { name: profile.name } : {}),\n // The provider verified this address; record it the way verifyEmail does.\n [authConfig.verification.field()]: new Date(),\n } as never)) as T;\n}\n\n/**\n * Find or create the app user for a provider profile, and link them.\n *\n * 1. An existing `provider_accounts` link wins — the provider's stable id, not\n * the email, identifies the account from then on.\n * 2. Otherwise the provider email MUST be verified. An unverified (or absent)\n * email throws {@link ProviderEmailNotVerifiedError} and nothing is linked\n * or created: linking on it would let anyone who can add an unverified\n * address at the provider take over the matching account.\n * 3. Verified: the user whose `auth.providers.emailField` matches, else a new\n * user (`auth.providers.createUser`, or the default) — then the link is stored.\n */\nexport async function resolveProviderUser<T extends Auth>(\n Model: ChildModel<T>,\n profile: ProviderProfile,\n): Promise<T> {\n const link = await ProviderAccount.findLink(profile.provider, profile.providerUserId);\n\n if (link) {\n const linked = (await Model.find(link.userId)) as T | null;\n\n if (!linked || linked.userType !== link.userType) {\n throw new InvalidProviderCallbackError(\"link-points-to-missing-user\");\n }\n\n return linked;\n }\n\n if (!profile.emailVerified || !profile.email) {\n throw new ProviderEmailNotVerifiedError();\n }\n\n const existing = (await Model.first({\n [authConfig.providers.emailField()]: profile.email,\n })) as T | null;\n\n const user = existing ?? (await createUserFor(Model, profile));\n\n await ProviderAccount.link(profile.provider, profile.providerUserId, user, profile.email);\n\n return user;\n}\n"],"mappings":";;;;;;;;AASA,eAAe,cACb,OACA,SACY;CACZ,MAAM,aAAa,WAAW,UAAU,WAAW;CAEnD,IAAI,YACF,OAAQ,MAAM,WAAW,SAAS,KAAoC;CAGxE,OAAQ,MAAM,MAAM,OAAO;GACxB,WAAW,UAAU,WAAW,IAAI,QAAQ;EAC7C,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;GAE5C,WAAW,aAAa,MAAM,oBAAI,IAAI,KAAK;CAC9C,CAAU;AACZ;;;;;;;;;;;;;AAcA,eAAsB,oBACpB,OACA,SACY;CACZ,MAAM,OAAO,MAAM,gBAAgB,SAAS,QAAQ,UAAU,QAAQ,cAAc;CAEpF,IAAI,MAAM;EACR,MAAM,SAAU,MAAM,MAAM,KAAK,KAAK,MAAM;EAE5C,IAAI,CAAC,UAAU,OAAO,aAAa,KAAK,UACtC,MAAM,IAAI,6BAA6B,6BAA6B;EAGtE,OAAO;CACT;CAEA,IAAI,CAAC,QAAQ,iBAAiB,CAAC,QAAQ,OACrC,MAAM,IAAI,8BAA8B;CAO1C,MAAM,OAAO,MAJW,MAAM,MAAM,GACjC,WAAW,UAAU,WAAW,IAAI,QAAQ,MAC/C,CAAC,KAEyB,MAAM,cAAc,OAAO,OAAO;CAE5D,MAAM,gBAAgB,KAAK,QAAQ,UAAU,QAAQ,gBAAgB,MAAM,QAAQ,KAAK;CAExF,OAAO;AACT"}
|
|
@@ -42,6 +42,14 @@ function parseDuration(key, raw) {
|
|
|
42
42
|
if (typeof parsed !== "number" || !Number.isFinite(parsed) || parsed <= 0) throw new Error(`auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — use a positive duration string such as "1h", "7d", or NO_EXPIRATION.`);
|
|
43
43
|
return parsed;
|
|
44
44
|
}
|
|
45
|
+
/** Email-verification token lifetime used when nothing is configured. */
|
|
46
|
+
const DEFAULT_VERIFICATION_EXPIRES_IN = "24h";
|
|
47
|
+
/** Password-reset token lifetime used when nothing is configured. */
|
|
48
|
+
const DEFAULT_PASSWORD_RESET_EXPIRES_IN = "60m";
|
|
49
|
+
/** Passkey challenge lifetime used when nothing is configured. */
|
|
50
|
+
const DEFAULT_PASSKEY_CHALLENGE_EXPIRES_IN = "5m";
|
|
51
|
+
/** OTP lifetime used when nothing is configured. */
|
|
52
|
+
const DEFAULT_OTP_EXPIRES_IN = "5m";
|
|
45
53
|
/** Access-token lifetime used when nothing is configured. */
|
|
46
54
|
const DEFAULT_ACCESS_TOKEN_EXPIRES_IN = "1h";
|
|
47
55
|
/**
|
|
@@ -110,6 +118,82 @@ const authConfig = {
|
|
|
110
118
|
*/
|
|
111
119
|
returnUrlParam: () => config.key("auth.pageAuth.returnUrlParam", "returnUrl")
|
|
112
120
|
},
|
|
121
|
+
cookie: {
|
|
122
|
+
/** Cookie name `setAuthCookie`/`clearAuthCookie` use. @default "access_token" */
|
|
123
|
+
name: () => config.key("auth.cookie.name", "access_token"),
|
|
124
|
+
/** Cookie `Path` attribute `setAuthCookie`/`clearAuthCookie` use. @default "/" */
|
|
125
|
+
path: () => config.key("auth.cookie.path", "/")
|
|
126
|
+
},
|
|
127
|
+
csrf: {
|
|
128
|
+
/**
|
|
129
|
+
* Extra origins allowed on a cookie-authenticated unsafe-method request,
|
|
130
|
+
* beyond the request's own origin. @default []
|
|
131
|
+
*/
|
|
132
|
+
allowedOrigins: () => config.key("auth.csrf.allowedOrigins", []) },
|
|
133
|
+
verification: {
|
|
134
|
+
/** Lifetime in ms of an email-verification token; throws naming the key when unusable. @default "24h" */
|
|
135
|
+
expiresInMs: () => parseDuration("verification.expiresIn", config.key("auth.verification.expiresIn", DEFAULT_VERIFICATION_EXPIRES_IN)),
|
|
136
|
+
/** User attribute stamped with the verification date. @default "emailVerifiedAt" */
|
|
137
|
+
field: () => config.key("auth.verification.field", "emailVerifiedAt"),
|
|
138
|
+
/** App notification replacing the default verification email. */
|
|
139
|
+
notification: () => config.key("auth.verification.notification"),
|
|
140
|
+
/** Link builder whose result is handed to the notification as `url`. */
|
|
141
|
+
url: () => config.key("auth.verification.url")
|
|
142
|
+
},
|
|
143
|
+
passwordReset: {
|
|
144
|
+
/** Lifetime in ms of a password-reset token; throws naming the key when unusable. @default "60m" */
|
|
145
|
+
expiresInMs: () => parseDuration("passwordReset.expiresIn", config.key("auth.passwordReset.expiresIn", DEFAULT_PASSWORD_RESET_EXPIRES_IN)),
|
|
146
|
+
/** User attribute `requestPasswordReset` looks the account up by. @default "email" */
|
|
147
|
+
identifierField: () => config.key("auth.passwordReset.identifierField", "email"),
|
|
148
|
+
/** App notification replacing the default reset email. */
|
|
149
|
+
notification: () => config.key("auth.passwordReset.notification"),
|
|
150
|
+
/** Link builder whose result is handed to the notification as `url`. */
|
|
151
|
+
url: () => config.key("auth.passwordReset.url"),
|
|
152
|
+
/** App-owned password writer replacing the default. */
|
|
153
|
+
setPassword: () => config.key("auth.passwordReset.setPassword")
|
|
154
|
+
},
|
|
155
|
+
providers: {
|
|
156
|
+
/** `auth.providers.google`, when configured. */
|
|
157
|
+
google: () => config.key("auth.providers.google"),
|
|
158
|
+
/** An app-registered provider under `auth.providers.custom.<name>`. */
|
|
159
|
+
custom: (name) => config.key("auth.providers.custom", {})[name],
|
|
160
|
+
/** User attribute a verified provider email is matched against. @default "email" */
|
|
161
|
+
emailField: () => config.key("auth.providers.emailField", "email"),
|
|
162
|
+
/** App-owned user creation for a first provider login. */
|
|
163
|
+
createUser: () => config.key("auth.providers.createUser")
|
|
164
|
+
},
|
|
165
|
+
passkeys: {
|
|
166
|
+
/** `auth.passkeys`; throws naming the keys when `rpID`, `rpName` or `origin` is missing. */
|
|
167
|
+
settings: () => {
|
|
168
|
+
const settings = config.key("auth.passkeys");
|
|
169
|
+
if (!settings?.rpID || !settings.rpName || !settings.origin) throw new Error("@warlock.js/auth: passkeys need `auth.passkeys.rpID`, `auth.passkeys.rpName` and `auth.passkeys.origin`.");
|
|
170
|
+
return settings;
|
|
171
|
+
},
|
|
172
|
+
/** Allowed ceremony origins, as a list. */
|
|
173
|
+
origins: () => {
|
|
174
|
+
const { origin } = authConfig.passkeys.settings();
|
|
175
|
+
return Array.isArray(origin) ? origin : [origin];
|
|
176
|
+
},
|
|
177
|
+
/** Challenge lifetime in ms; throws naming the key when unusable. @default "5m" */
|
|
178
|
+
challengeExpiresInMs: () => parseDuration("passkeys.challengeExpiresIn", config.key("auth.passkeys.challengeExpiresIn", DEFAULT_PASSKEY_CHALLENGE_EXPIRES_IN))
|
|
179
|
+
},
|
|
180
|
+
otp: {
|
|
181
|
+
/** Notifications channel the code is sent through. @default "sms" */
|
|
182
|
+
channel: () => config.key("auth.otp.channel", "sms"),
|
|
183
|
+
/** User attribute holding the phone number. @default "phone" */
|
|
184
|
+
phoneField: () => config.key("auth.otp.phoneField", "phone"),
|
|
185
|
+
/** Code lifetime in ms; throws naming the key when unusable. @default "5m" */
|
|
186
|
+
expiresInMs: () => parseDuration("otp.expiresIn", config.key("auth.otp.expiresIn", DEFAULT_OTP_EXPIRES_IN)),
|
|
187
|
+
/** Verify attempts before the code is invalidated. @default 5 */
|
|
188
|
+
maxAttempts: () => config.key("auth.otp.maxAttempts", 5),
|
|
189
|
+
/** The message text for a code. */
|
|
190
|
+
message: (code) => {
|
|
191
|
+
const build = config.key("auth.otp.message");
|
|
192
|
+
return build ? build(code) : `Your verification code is ${code}`;
|
|
193
|
+
},
|
|
194
|
+
/** App-owned delivery replacing the notifications channel send. */
|
|
195
|
+
send: () => config.key("auth.otp.send")
|
|
196
|
+
},
|
|
113
197
|
refreshToken: {
|
|
114
198
|
/** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */
|
|
115
199
|
secret: () => resolve("refreshToken.secret", "jwt.refresh.secret"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\nimport type { Auth } from \"../models/auth.model\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\r\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n canAuthenticate: async (user: Auth): Promise<boolean> => {\r\n const canAuthenticate = config.key<CanAuthenticate>(\r\n \"auth.canAuthenticate\",\r\n permitAuthentication,\r\n );\r\n\r\n return canAuthenticate(user);\r\n },\r\n accessToken: {\r\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n pageAuth: {\r\n /**\r\n * Login path a guarded PAGE route redirects a logged-out browser to.\r\n *\r\n * Unset (the default) ⇒ page-route auth failures keep the JSON 401 API\r\n * contract, so existing apps see NO behavior change. Set it (e.g. \"/login\")\r\n * to opt a browser-facing app into login-redirect UX: a logged-out human\r\n * navigating to a guarded page lands on the login screen instead of reading\r\n * a raw JSON error blob. Finding b9ab9804.\r\n */\r\n loginPath: (): string | undefined => config.key(\"auth.pageAuth.loginPath\"),\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed. Defaults to\r\n * `\"returnUrl\"`.\r\n */\r\n returnUrlParam: (): string => config.key(\"auth.pageAuth.returnUrlParam\", \"returnUrl\"),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,CAAC,CAAC,IAAI;CAC7B;CACA,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,UAAU;;;;;;;;;;EAUR,iBAAqC,OAAO,IAAI,yBAAyB;;;;;;EAMzE,sBAA8B,OAAO,IAAI,gCAAgC,WAAW;CACtF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
1
|
+
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type {\r\n AuthNotification,\r\n CanAuthenticate,\r\n LogoutWithoutTokenBehavior,\r\n OneTimeTokenUrlBuilder,\r\n PasswordSetter,\r\n} from \"../contracts/types\";\r\nimport type {\r\n AuthProvider,\r\n GoogleProviderConfig,\r\n OtpSender,\r\n PasskeysConfig,\r\n ProviderUserCreator,\r\n} from \"../contracts/providers\";\r\nimport type { Auth } from \"../models/auth.model\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\r\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Email-verification token lifetime used when nothing is configured. */\r\nconst DEFAULT_VERIFICATION_EXPIRES_IN = \"24h\";\r\n\r\n/** Password-reset token lifetime used when nothing is configured. */\r\nconst DEFAULT_PASSWORD_RESET_EXPIRES_IN = \"60m\";\r\n\r\n/** Passkey challenge lifetime used when nothing is configured. */\r\nconst DEFAULT_PASSKEY_CHALLENGE_EXPIRES_IN = \"5m\";\r\n\r\n/** OTP lifetime used when nothing is configured. */\r\nconst DEFAULT_OTP_EXPIRES_IN = \"5m\";\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\r\n \"auth\",\r\n \"config-deprecation\",\r\n `auth.${legacyKey} is deprecated — use auth.${newKey}`,\r\n );\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n canAuthenticate: async (user: Auth): Promise<boolean> => {\r\n const canAuthenticate = config.key<CanAuthenticate>(\r\n \"auth.canAuthenticate\",\r\n permitAuthentication,\r\n );\r\n\r\n return canAuthenticate(user);\r\n },\r\n accessToken: {\r\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n pageAuth: {\r\n /**\r\n * Login path a guarded PAGE route redirects a logged-out browser to.\r\n *\r\n * Unset (the default) ⇒ page-route auth failures keep the JSON 401 API\r\n * contract, so existing apps see NO behavior change. Set it (e.g. \"/login\")\r\n * to opt a browser-facing app into login-redirect UX: a logged-out human\r\n * navigating to a guarded page lands on the login screen instead of reading\r\n * a raw JSON error blob. Finding b9ab9804.\r\n */\r\n loginPath: (): string | undefined => config.key(\"auth.pageAuth.loginPath\"),\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed. Defaults to\r\n * `\"returnUrl\"`.\r\n */\r\n returnUrlParam: (): string => config.key(\"auth.pageAuth.returnUrlParam\", \"returnUrl\"),\r\n },\r\n cookie: {\r\n /** Cookie name `setAuthCookie`/`clearAuthCookie` use. @default \"access_token\" */\r\n name: (): string => config.key(\"auth.cookie.name\", \"access_token\"),\r\n /** Cookie `Path` attribute `setAuthCookie`/`clearAuthCookie` use. @default \"/\" */\r\n path: (): string => config.key(\"auth.cookie.path\", \"/\"),\r\n },\r\n csrf: {\r\n /**\r\n * Extra origins allowed on a cookie-authenticated unsafe-method request,\r\n * beyond the request's own origin. @default []\r\n */\r\n allowedOrigins: (): string[] => config.key(\"auth.csrf.allowedOrigins\", []),\r\n },\r\n verification: {\r\n /** Lifetime in ms of an email-verification token; throws naming the key when unusable. @default \"24h\" */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"verification.expiresIn\",\r\n config.key(\"auth.verification.expiresIn\", DEFAULT_VERIFICATION_EXPIRES_IN),\r\n ),\r\n /** User attribute stamped with the verification date. @default \"emailVerifiedAt\" */\r\n field: (): string => config.key(\"auth.verification.field\", \"emailVerifiedAt\"),\r\n /** App notification replacing the default verification email. */\r\n notification: (): AuthNotification | undefined => config.key(\"auth.verification.notification\"),\r\n /** Link builder whose result is handed to the notification as `url`. */\r\n url: (): OneTimeTokenUrlBuilder | undefined => config.key(\"auth.verification.url\"),\r\n },\r\n passwordReset: {\r\n /** Lifetime in ms of a password-reset token; throws naming the key when unusable. @default \"60m\" */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"passwordReset.expiresIn\",\r\n config.key(\"auth.passwordReset.expiresIn\", DEFAULT_PASSWORD_RESET_EXPIRES_IN),\r\n ),\r\n /** User attribute `requestPasswordReset` looks the account up by. @default \"email\" */\r\n identifierField: (): string => config.key(\"auth.passwordReset.identifierField\", \"email\"),\r\n /** App notification replacing the default reset email. */\r\n notification: (): AuthNotification | undefined => config.key(\"auth.passwordReset.notification\"),\r\n /** Link builder whose result is handed to the notification as `url`. */\r\n url: (): OneTimeTokenUrlBuilder | undefined => config.key(\"auth.passwordReset.url\"),\r\n /** App-owned password writer replacing the default. */\r\n setPassword: (): PasswordSetter | undefined => config.key(\"auth.passwordReset.setPassword\"),\r\n },\r\n providers: {\r\n /** `auth.providers.google`, when configured. */\r\n google: (): GoogleProviderConfig | undefined => config.key(\"auth.providers.google\"),\r\n /** An app-registered provider under `auth.providers.custom.<name>`. */\r\n custom: (name: string): AuthProvider | undefined =>\r\n config.key<Record<string, AuthProvider>>(\"auth.providers.custom\", {})[name],\r\n /** User attribute a verified provider email is matched against. @default \"email\" */\r\n emailField: (): string => config.key(\"auth.providers.emailField\", \"email\"),\r\n /** App-owned user creation for a first provider login. */\r\n createUser: (): ProviderUserCreator | undefined => config.key(\"auth.providers.createUser\"),\r\n },\r\n passkeys: {\r\n /** `auth.passkeys`; throws naming the keys when `rpID`, `rpName` or `origin` is missing. */\r\n settings: (): PasskeysConfig => {\r\n const settings = config.key<PasskeysConfig | undefined>(\"auth.passkeys\");\r\n\r\n if (!settings?.rpID || !settings.rpName || !settings.origin) {\r\n throw new Error(\r\n \"@warlock.js/auth: passkeys need `auth.passkeys.rpID`, `auth.passkeys.rpName` and `auth.passkeys.origin`.\",\r\n );\r\n }\r\n\r\n return settings;\r\n },\r\n /** Allowed ceremony origins, as a list. */\r\n origins: (): string[] => {\r\n const { origin } = authConfig.passkeys.settings();\r\n\r\n return Array.isArray(origin) ? origin : [origin];\r\n },\r\n /** Challenge lifetime in ms; throws naming the key when unusable. @default \"5m\" */\r\n challengeExpiresInMs: (): number =>\r\n parseDuration(\r\n \"passkeys.challengeExpiresIn\",\r\n config.key(\"auth.passkeys.challengeExpiresIn\", DEFAULT_PASSKEY_CHALLENGE_EXPIRES_IN),\r\n ),\r\n },\r\n otp: {\r\n /** Notifications channel the code is sent through. @default \"sms\" */\r\n channel: (): string => config.key(\"auth.otp.channel\", \"sms\"),\r\n /** User attribute holding the phone number. @default \"phone\" */\r\n phoneField: (): string => config.key(\"auth.otp.phoneField\", \"phone\"),\r\n /** Code lifetime in ms; throws naming the key when unusable. @default \"5m\" */\r\n expiresInMs: (): number =>\r\n parseDuration(\"otp.expiresIn\", config.key(\"auth.otp.expiresIn\", DEFAULT_OTP_EXPIRES_IN)),\r\n /** Verify attempts before the code is invalidated. @default 5 */\r\n maxAttempts: (): number => config.key(\"auth.otp.maxAttempts\", 5),\r\n /** The message text for a code. */\r\n message: (code: string): string => {\r\n const build = config.key<((code: string) => string) | undefined>(\"auth.otp.message\");\r\n\r\n return build ? build(code) : `Your verification code is ${code}`;\r\n },\r\n /** App-owned delivery replacing the notifications channel send. */\r\n send: (): OtpSender | undefined => config.key(\"auth.otp.send\"),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAoBA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;AAGxC,MAAM,oCAAoC;;AAG1C,MAAM,uCAAuC;;AAG7C,MAAM,yBAAyB;;AAG/B,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KACF,QACA,sBACA,QAAQ,UAAU,4BAA4B,QAChD;EACF;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,CAAC,CAAC,IAAI;CAC7B;CACA,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,UAAU;;;;;;;;;;EAUR,iBAAqC,OAAO,IAAI,yBAAyB;;;;;;EAMzE,sBAA8B,OAAO,IAAI,gCAAgC,WAAW;CACtF;CACA,QAAQ;;EAEN,YAAoB,OAAO,IAAI,oBAAoB,cAAc;;EAEjE,YAAoB,OAAO,IAAI,oBAAoB,GAAG;CACxD;CACA,MAAM;;;;;AAKJ,sBAAgC,OAAO,IAAI,4BAA4B,CAAC,CAAC,EAC3E;CACA,cAAc;;EAEZ,mBACE,cACE,0BACA,OAAO,IAAI,+BAA+B,+BAA+B,CAC3E;;EAEF,aAAqB,OAAO,IAAI,2BAA2B,iBAAiB;;EAE5E,oBAAkD,OAAO,IAAI,gCAAgC;;EAE7F,WAA+C,OAAO,IAAI,uBAAuB;CACnF;CACA,eAAe;;EAEb,mBACE,cACE,2BACA,OAAO,IAAI,gCAAgC,iCAAiC,CAC9E;;EAEF,uBAA+B,OAAO,IAAI,sCAAsC,OAAO;;EAEvF,oBAAkD,OAAO,IAAI,iCAAiC;;EAE9F,WAA+C,OAAO,IAAI,wBAAwB;;EAElF,mBAA+C,OAAO,IAAI,gCAAgC;CAC5F;CACA,WAAW;;EAET,cAAgD,OAAO,IAAI,uBAAuB;;EAElF,SAAS,SACP,OAAO,IAAkC,yBAAyB,CAAC,CAAC,CAAC,CAAC;;EAExE,kBAA0B,OAAO,IAAI,6BAA6B,OAAO;;EAEzE,kBAAmD,OAAO,IAAI,2BAA2B;CAC3F;CACA,UAAU;;EAER,gBAAgC;GAC9B,MAAM,WAAW,OAAO,IAAgC,eAAe;GAEvE,IAAI,CAAC,UAAU,QAAQ,CAAC,SAAS,UAAU,CAAC,SAAS,QACnD,MAAM,IAAI,MACR,0GACF;GAGF,OAAO;EACT;;EAEA,eAAyB;GACvB,MAAM,EAAE,WAAW,WAAW,SAAS,SAAS;GAEhD,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;EACjD;;EAEA,4BACE,cACE,+BACA,OAAO,IAAI,oCAAoC,oCAAoC,CACrF;CACJ;CACA,KAAK;;EAEH,eAAuB,OAAO,IAAI,oBAAoB,KAAK;;EAE3D,kBAA0B,OAAO,IAAI,uBAAuB,OAAO;;EAEnE,mBACE,cAAc,iBAAiB,OAAO,IAAI,sBAAsB,sBAAsB,CAAC;;EAEzF,mBAA2B,OAAO,IAAI,wBAAwB,CAAC;;EAE/D,UAAU,SAAyB;GACjC,MAAM,QAAQ,OAAO,IAA4C,kBAAkB;GAEnF,OAAO,QAAQ,MAAM,IAAI,IAAI,6BAA6B;EAC5D;;EAEA,YAAmC,OAAO,IAAI,eAAe;CAC/D;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
2
2
|
import { AccessToken } from "../models/access-token/access-token.model.mjs";
|
|
3
|
-
import { AccessTokenOutput, AuthCredentials, DeviceInfo, LoginResult, TokenPair } from "../contracts/types.mjs";
|
|
3
|
+
import { AccessTokenOutput, AuthCredentials, ClearAuthCookieOptions, DeviceInfo, LoginResult, SetAuthCookieOptions, TokenPair } from "../contracts/types.mjs";
|
|
4
4
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
5
|
+
import { Response } from "@warlock.js/core";
|
|
5
6
|
import { ChildModel } from "@warlock.js/cascade";
|
|
6
7
|
|
|
7
8
|
//#region ../auth/src/services/auth.service.d.ts
|
|
@@ -17,6 +18,10 @@ declare class AuthService {
|
|
|
17
18
|
* Resolve the active refresh-token model (default or registered override).
|
|
18
19
|
*/
|
|
19
20
|
private get refreshTokenModel();
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the active one-time-token model (default or registered override).
|
|
23
|
+
*/
|
|
24
|
+
private get oneTimeTokenModel();
|
|
20
25
|
/**
|
|
21
26
|
* Build the default access-token JWT payload from a user.
|
|
22
27
|
*/
|
|
@@ -66,6 +71,18 @@ declare class AuthService {
|
|
|
66
71
|
* the user + token pair on success, `null` on failure.
|
|
67
72
|
*/
|
|
68
73
|
login<T extends Auth>(Model: ChildModel<T>, credentials: AuthCredentials, deviceInfo?: DeviceInfo): Promise<LoginResult<T> | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Log in a user some OTHER method already authenticated — a provider
|
|
76
|
+
* callback, a passkey assertion, a one-time code. Applies the
|
|
77
|
+
* `auth.canAuthenticate` policy (403 on refusal), then produces exactly
|
|
78
|
+
* what {@link login} produces: the same tokens, rows and events. Pair with
|
|
79
|
+
* {@link setAuthCookie} for a cookie session, as with password login.
|
|
80
|
+
*
|
|
81
|
+
* Never call this for a user you have not authenticated.
|
|
82
|
+
*/
|
|
83
|
+
completeLogin<T extends Auth>(user: T, deviceInfo?: DeviceInfo): Promise<LoginResult<T>>;
|
|
84
|
+
/** The one token-issuing tail every login method shares. */
|
|
85
|
+
private finalizeLogin;
|
|
69
86
|
/**
|
|
70
87
|
* Log a user out.
|
|
71
88
|
*
|
|
@@ -100,7 +117,8 @@ declare class AuthService {
|
|
|
100
117
|
*/
|
|
101
118
|
revokeTokenFamily(familyId: string): Promise<void>;
|
|
102
119
|
/**
|
|
103
|
-
* Delete expired tokens (refresh + access)
|
|
120
|
+
* Delete expired tokens (refresh + access) and spent one-time tokens
|
|
121
|
+
* (expired or consumed verification/reset/OTP/passkey rows). Emits `token.expired` per refresh
|
|
104
122
|
* token and `cleanup.completed` with the refresh count. Drives the
|
|
105
123
|
* `auth.cleanup` CLI command.
|
|
106
124
|
*/
|
|
@@ -135,6 +153,45 @@ declare class AuthService {
|
|
|
135
153
|
* Active, unexpired sessions for the user, newest first.
|
|
136
154
|
*/
|
|
137
155
|
getActiveSessions(user: Auth): Promise<RefreshToken[]>;
|
|
156
|
+
/**
|
|
157
|
+
* `Max-Age` (seconds) `setAuthCookie` should apply, derived from an
|
|
158
|
+
* `AccessTokenOutput`'s `expiresAt` — or `undefined` for a bare token
|
|
159
|
+
* string (a session cookie) or an unparseable `expiresAt`.
|
|
160
|
+
*/
|
|
161
|
+
private cookieMaxAgeFromToken;
|
|
162
|
+
/**
|
|
163
|
+
* Write the auth cookie on `response` — the write side of the `cookie:<name>`
|
|
164
|
+
* token source `authMiddleware([], "cookie:<name>")` already reads. An explicit
|
|
165
|
+
* app-controller call, never a side effect of {@link login}, so an existing
|
|
166
|
+
* bearer-only app never starts emitting `Set-Cookie` just by upgrading.
|
|
167
|
+
*
|
|
168
|
+
* Attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come
|
|
169
|
+
* from `response.cookie()`'s own `secureCookieDefaults()` floor — this
|
|
170
|
+
* method never overrides them, only `name`/`path`/`maxAge`. `raw: true`
|
|
171
|
+
* writes the token unquoted, so the same string a `cookie:<name>` source
|
|
172
|
+
* reads back is exactly what was issued, not a JSON-wrapped copy.
|
|
173
|
+
*
|
|
174
|
+
* @param token - the token string, or an `AccessTokenOutput` (e.g. from
|
|
175
|
+
* {@link login}) whose `expiresAt` sets `Max-Age` automatically when
|
|
176
|
+
* `options.maxAge` is not given. A bare string with no `options.maxAge`
|
|
177
|
+
* produces a session cookie.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* const { user, tokens } = await authService.login(User, credentials);
|
|
181
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
182
|
+
*/
|
|
183
|
+
setAuthCookie(response: Response, token: string | AccessTokenOutput, options?: SetAuthCookieOptions): void;
|
|
184
|
+
/**
|
|
185
|
+
* Clear the auth cookie {@link setAuthCookie} wrote. An explicit
|
|
186
|
+
* app-controller call — pair it with {@link logout} after the token row is
|
|
187
|
+
* revoked. `path` must match what the cookie was set with, or the browser
|
|
188
|
+
* silently ignores the clear (see `response.clearCookie`'s own doc comment).
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* await authService.logout(user, accessToken, refreshToken);
|
|
192
|
+
* authService.clearAuthCookie(response);
|
|
193
|
+
*/
|
|
194
|
+
clearAuthCookie(response: Response, options?: ClearAuthCookieOptions): void;
|
|
138
195
|
}
|
|
139
196
|
declare const authService: AuthService;
|
|
140
197
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;;cA0BM,WAAA;;;;AALiD;;;cAYzC,gBAAA;EAqBgC;;;EAAA,YAdhC,iBAAA;EAwDA;;;EAAA,YAjDA,iBAAA;EA0FG;;;EAnFR,uBAAA,CAAwB,IAAA,EAAM,IAAA,GAAO,MAAA;EAsHU;EA7GzC,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAAA,QAI5B,qBAAA;EAAA,QAMA,gBAAA;EAiHH;;;;;EA5FE,mBAAA,CACX,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,MAAA,oBACT,OAAA,CAAQ,iBAAA;EAAA,QAMG,iBAAA;EA4KN;;;;EA5IK,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAAA,QAQG,cAAA;EAoKC;;;EA1IF,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4IxE;;;;;EAjIU,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EAoJR;;;EA5EU,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EA2IjB;;;EApI9C,YAAA,CAAa,QAAA,WAAmB,OAAA;EAkJe;;;;EA1I/C,YAAA,WAAuB,IAAA,EAClC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,CAAA;EAqMK;;;;EArKH,KAAA,WAAgB,IAAA,EAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,EAAa,eAAA,EACb,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EAsM6B;;;;;;;;;EAnLvC,aAAA,WAAwB,IAAA,EACnC,IAAA,EAAM,CAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EAtSX;EAAA,QA6SE,aAAA;EA/RP;;;;;;;;;;EA0TM,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EAjRtE;;;EA8SK,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EA5SxD;;;EAmTU,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA5QxC;;;EAmRK,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAjRzD;;;;;EA0RU,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAxPY;;;EAuQzC,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EA5PrC;;;;;;EAwQA,oBAAA,IAAwB,OAAA;EA7LT;;;;;;;;;;EAsNf,uBAAA,IAA2B,OAAA;IACtC,YAAA,EAAc,WAAA;IACd,aAAA,EAAe,YAAA;EAAA;EAtMd;;;;;;;;EAsNU,wBAAA,IAA4B,OAAA;IACvC,YAAA;IACA,aAAA;EAAA;EApLC;;;EAsMU,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAnLzB;;;;;EAAA,QA4LnB,qBAAA;EAzLL;;;;;;;;;;;;;;;;;;;;;EAwNI,aAAA,CACL,QAAA,EAAU,QAAA,EACV,KAAA,WAAgB,iBAAA,EAChB,OAAA,GAAS,oBAAA;EA9IiC;;;;;;;;;;EAsKrC,eAAA,CAAgB,QAAA,EAAU,QAAA,EAAU,OAAA,GAAS,sBAAA;AAAA;AAAA,cAQzC,WAAA,EAAW,WAAoB"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AccessToken } from "../models/access-token/access-token.model.mjs";
|
|
2
2
|
import "../models/access-token/index.mjs";
|
|
3
|
+
import { OneTimeToken } from "../models/one-time-token/one-time-token.model.mjs";
|
|
4
|
+
import "../models/one-time-token/index.mjs";
|
|
3
5
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
4
6
|
import "../models/refresh-token/index.mjs";
|
|
5
7
|
import { authConfig } from "./auth-config.mjs";
|
|
@@ -26,6 +28,12 @@ var AuthService = class {
|
|
|
26
28
|
return config.key("auth.refreshToken.model", RefreshToken);
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
31
|
+
* Resolve the active one-time-token model (default or registered override).
|
|
32
|
+
*/
|
|
33
|
+
get oneTimeTokenModel() {
|
|
34
|
+
return config.key("auth.oneTimeToken.model", OneTimeToken);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
29
37
|
* Build the default access-token JWT payload from a user.
|
|
30
38
|
*/
|
|
31
39
|
buildAccessTokenPayload(user) {
|
|
@@ -188,6 +196,23 @@ var AuthService = class {
|
|
|
188
196
|
async login(Model, credentials, deviceInfo) {
|
|
189
197
|
const user = await this.attemptLogin(Model, credentials);
|
|
190
198
|
if (!user) return null;
|
|
199
|
+
return this.finalizeLogin(user, deviceInfo);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Log in a user some OTHER method already authenticated — a provider
|
|
203
|
+
* callback, a passkey assertion, a one-time code. Applies the
|
|
204
|
+
* `auth.canAuthenticate` policy (403 on refusal), then produces exactly
|
|
205
|
+
* what {@link login} produces: the same tokens, rows and events. Pair with
|
|
206
|
+
* {@link setAuthCookie} for a cookie session, as with password login.
|
|
207
|
+
*
|
|
208
|
+
* Never call this for a user you have not authenticated.
|
|
209
|
+
*/
|
|
210
|
+
async completeLogin(user, deviceInfo) {
|
|
211
|
+
await this.assertCanAuthenticate(user);
|
|
212
|
+
return this.finalizeLogin(user, deviceInfo);
|
|
213
|
+
}
|
|
214
|
+
/** The one token-issuing tail every login method shares. */
|
|
215
|
+
async finalizeLogin(user, deviceInfo) {
|
|
191
216
|
if (!authConfig.refreshToken.enabled()) return {
|
|
192
217
|
user,
|
|
193
218
|
tokens: { accessToken: await this.issueAccessToken(user, deviceInfo?.payload) }
|
|
@@ -261,7 +286,8 @@ var AuthService = class {
|
|
|
261
286
|
authEvents.emit("token.familyRevoked", familyId, revokedTokens);
|
|
262
287
|
}
|
|
263
288
|
/**
|
|
264
|
-
* Delete expired tokens (refresh + access)
|
|
289
|
+
* Delete expired tokens (refresh + access) and spent one-time tokens
|
|
290
|
+
* (expired or consumed verification/reset/OTP/passkey rows). Emits `token.expired` per refresh
|
|
265
291
|
* token and `cleanup.completed` with the refresh count. Drives the
|
|
266
292
|
* `auth.cleanup` CLI command.
|
|
267
293
|
*/
|
|
@@ -269,6 +295,7 @@ var AuthService = class {
|
|
|
269
295
|
const expiredTokens = await this.refreshTokenModel.purgeExpired();
|
|
270
296
|
for (const token of expiredTokens) authEvents.emit("token.expired", token);
|
|
271
297
|
await this.accessTokenModel.purgeExpired();
|
|
298
|
+
await this.oneTimeTokenModel.purgeSpent();
|
|
272
299
|
authEvents.emit("cleanup.completed", expiredTokens.length);
|
|
273
300
|
return expiredTokens.length;
|
|
274
301
|
}
|
|
@@ -311,6 +338,64 @@ var AuthService = class {
|
|
|
311
338
|
async getActiveSessions(user) {
|
|
312
339
|
return this.refreshTokenModel.activeFor(user);
|
|
313
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* `Max-Age` (seconds) `setAuthCookie` should apply, derived from an
|
|
343
|
+
* `AccessTokenOutput`'s `expiresAt` — or `undefined` for a bare token
|
|
344
|
+
* string (a session cookie) or an unparseable `expiresAt`.
|
|
345
|
+
*/
|
|
346
|
+
cookieMaxAgeFromToken(token) {
|
|
347
|
+
if (typeof token === "string") return void 0;
|
|
348
|
+
const expiresAt = new Date(token.expiresAt).getTime();
|
|
349
|
+
if (Number.isNaN(expiresAt)) return void 0;
|
|
350
|
+
return Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Write the auth cookie on `response` — the write side of the `cookie:<name>`
|
|
354
|
+
* token source `authMiddleware([], "cookie:<name>")` already reads. An explicit
|
|
355
|
+
* app-controller call, never a side effect of {@link login}, so an existing
|
|
356
|
+
* bearer-only app never starts emitting `Set-Cookie` just by upgrading.
|
|
357
|
+
*
|
|
358
|
+
* Attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) come
|
|
359
|
+
* from `response.cookie()`'s own `secureCookieDefaults()` floor — this
|
|
360
|
+
* method never overrides them, only `name`/`path`/`maxAge`. `raw: true`
|
|
361
|
+
* writes the token unquoted, so the same string a `cookie:<name>` source
|
|
362
|
+
* reads back is exactly what was issued, not a JSON-wrapped copy.
|
|
363
|
+
*
|
|
364
|
+
* @param token - the token string, or an `AccessTokenOutput` (e.g. from
|
|
365
|
+
* {@link login}) whose `expiresAt` sets `Max-Age` automatically when
|
|
366
|
+
* `options.maxAge` is not given. A bare string with no `options.maxAge`
|
|
367
|
+
* produces a session cookie.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* const { user, tokens } = await authService.login(User, credentials);
|
|
371
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
372
|
+
*/
|
|
373
|
+
setAuthCookie(response, token, options = {}) {
|
|
374
|
+
const name = options.name ?? authConfig.cookie.name();
|
|
375
|
+
const path = options.path ?? authConfig.cookie.path();
|
|
376
|
+
const tokenValue = typeof token === "string" ? token : token.token;
|
|
377
|
+
const maxAge = options.maxAge ?? this.cookieMaxAgeFromToken(token);
|
|
378
|
+
response.cookie(name, tokenValue, {
|
|
379
|
+
raw: true,
|
|
380
|
+
path,
|
|
381
|
+
...maxAge !== void 0 ? { maxAge } : {}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Clear the auth cookie {@link setAuthCookie} wrote. An explicit
|
|
386
|
+
* app-controller call — pair it with {@link logout} after the token row is
|
|
387
|
+
* revoked. `path` must match what the cookie was set with, or the browser
|
|
388
|
+
* silently ignores the clear (see `response.clearCookie`'s own doc comment).
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* await authService.logout(user, accessToken, refreshToken);
|
|
392
|
+
* authService.clearAuthCookie(response);
|
|
393
|
+
*/
|
|
394
|
+
clearAuthCookie(response, options = {}) {
|
|
395
|
+
const name = options.name ?? authConfig.cookie.name();
|
|
396
|
+
const path = options.path ?? authConfig.cookie.path();
|
|
397
|
+
response.clearCookie(name, { path });
|
|
398
|
+
}
|
|
314
399
|
};
|
|
315
400
|
const authService = new AuthService();
|
|
316
401
|
|