@warlock.js/auth 5.29.0 → 5.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/esm/commands/auth-cleanup-command.d.mts +1 -1
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/commands/auth-purge-never-expiring-command.d.mts +1 -1
- package/esm/commands/auth-purge-never-expiring-command.d.mts.map +1 -1
- package/esm/commands/auth-purge-never-expiring-command.mjs +1 -1
- package/esm/commands/auth-purge-never-expiring-command.mjs.map +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs.map +1 -1
- package/esm/contracts/auth-contract.d.mts +1 -1
- package/esm/contracts/auth-contract.d.mts.map +1 -1
- package/esm/contracts/providers.d.mts +1 -1
- package/esm/contracts/providers.d.mts.map +1 -1
- package/esm/contracts/request-locals.d.mts +1 -1
- package/esm/contracts/request-locals.d.mts.map +1 -1
- package/esm/contracts/types.d.mts +1 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.mjs +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -1
- package/esm/errors/email-not-verified.error.d.mts +1 -1
- package/esm/errors/email-not-verified.error.d.mts.map +1 -1
- package/esm/errors/email-not-verified.error.mjs +1 -1
- package/esm/errors/email-not-verified.error.mjs.map +1 -1
- package/esm/errors/invalid-one-time-token.error.d.mts +1 -1
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -1
- package/esm/errors/invalid-one-time-token.error.mjs +1 -1
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -1
- package/esm/errors/invalid-passkey.error.d.mts +1 -1
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -1
- package/esm/errors/invalid-passkey.error.mjs +1 -1
- package/esm/errors/invalid-passkey.error.mjs.map +1 -1
- package/esm/errors/invalid-provider-callback.error.d.mts +1 -1
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -1
- package/esm/errors/invalid-provider-callback.error.mjs +1 -1
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -1
- package/esm/errors/notifications-unavailable.error.d.mts +1 -1
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -1
- package/esm/errors/notifications-unavailable.error.mjs +1 -1
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -1
- package/esm/errors/provider-email-not-verified.error.d.mts +1 -1
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -1
- package/esm/errors/provider-email-not-verified.error.mjs +1 -1
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/localization/ar.translations.mjs +1 -1
- package/esm/localization/ar.translations.mjs.map +1 -1
- package/esm/localization/auth-default-translations.d.mts +1 -1
- package/esm/localization/auth-default-translations.d.mts.map +1 -1
- package/esm/localization/auth-default-translations.mjs +1 -1
- package/esm/localization/auth-default-translations.mjs.map +1 -1
- package/esm/localization/auth-translations.type.d.mts +1 -1
- package/esm/localization/auth-translations.type.d.mts.map +1 -1
- package/esm/localization/en.translations.mjs +1 -1
- package/esm/localization/en.translations.mjs.map +1 -1
- package/esm/localization/init.mjs +1 -1
- package/esm/localization/init.mjs.map +1 -1
- package/esm/localization/register-auth-translations.d.mts +1 -1
- package/esm/localization/register-auth-translations.d.mts.map +1 -1
- package/esm/localization/register-auth-translations.mjs +1 -1
- package/esm/localization/register-auth-translations.mjs.map +1 -1
- package/esm/middleware/auth.middleware.d.mts +1 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +1 -1
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -1
- package/esm/middleware/csrf-origin-check.mjs +1 -1
- package/esm/middleware/csrf-origin-check.mjs.map +1 -1
- package/esm/middleware/index.d.mts +1 -1
- package/esm/middleware/index.mjs +1 -1
- package/esm/middleware/localized-login-path.mjs +1 -1
- package/esm/middleware/localized-login-path.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.d.mts +17 -2
- package/esm/middleware/login-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs +84 -6
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.mjs +1 -1
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/otp-throttle.middleware.d.mts +1 -1
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -1
- package/esm/middleware/otp-throttle.middleware.mjs +1 -1
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -1
- package/esm/middleware/require-verified-email.middleware.d.mts +1 -1
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -1
- package/esm/middleware/require-verified-email.middleware.mjs +1 -1
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.d.mts +27 -35
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/family-association.migration.mjs +1 -1
- package/esm/models/access-token/family-association.migration.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.d.mts +27 -43
- package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
- package/esm/models/auth-token-family/migration.mjs +1 -1
- package/esm/models/auth-token-family/migration.mjs.map +1 -1
- package/esm/models/auth.model.d.mts +1 -1
- package/esm/models/auth.model.d.mts.map +1 -1
- package/esm/models/auth.model.mjs +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/index.d.mts +1 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +1 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/migration.mjs +1 -1
- package/esm/models/one-time-token/migration.mjs.map +1 -1
- package/esm/models/one-time-token/one-time-token.model.d.mts +35 -67
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -1
- package/esm/models/one-time-token/one-time-token.model.mjs +1 -1
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -1
- package/esm/models/passkey-credential/migration.mjs +1 -1
- package/esm/models/passkey-credential/migration.mjs.map +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +31 -47
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.mjs +1 -1
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -1
- package/esm/models/provider-account/migration.mjs +1 -1
- package/esm/models/provider-account/migration.mjs.map +1 -1
- package/esm/models/provider-account/provider-account.model.d.mts +27 -35
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -1
- package/esm/models/provider-account/provider-account.model.mjs +1 -1
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts +47 -95
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/models/refresh-token/successor-association.migration.mjs +1 -1
- package/esm/models/refresh-token/successor-association.migration.mjs.map +1 -1
- package/esm/otp/otp.d.mts +1 -1
- package/esm/otp/otp.d.mts.map +1 -1
- package/esm/otp/otp.mjs +1 -1
- package/esm/otp/otp.mjs.map +1 -1
- package/esm/passkeys/passkey-authentication.d.mts +1 -1
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -1
- package/esm/passkeys/passkey-authentication.mjs +1 -1
- package/esm/passkeys/passkey-authentication.mjs.map +1 -1
- package/esm/passkeys/passkey-challenges.mjs +1 -1
- package/esm/passkeys/passkey-challenges.mjs.map +1 -1
- package/esm/passkeys/passkey-registration.d.mts +1 -1
- package/esm/passkeys/passkey-registration.d.mts.map +1 -1
- package/esm/passkeys/passkey-registration.mjs +1 -1
- package/esm/passkeys/passkey-registration.mjs.map +1 -1
- package/esm/passkeys/simplewebauthn.d.mts +1 -1
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -1
- package/esm/passkeys/simplewebauthn.mjs +1 -1
- package/esm/passkeys/simplewebauthn.mjs.map +1 -1
- package/esm/providers/apple-provider.d.mts +1 -1
- package/esm/providers/apple-provider.d.mts.map +1 -1
- package/esm/providers/apple-provider.mjs +1 -1
- package/esm/providers/apple-provider.mjs.map +1 -1
- package/esm/providers/discord-provider.d.mts +1 -1
- package/esm/providers/discord-provider.d.mts.map +1 -1
- package/esm/providers/discord-provider.mjs +1 -1
- package/esm/providers/discord-provider.mjs.map +1 -1
- package/esm/providers/facebook-provider.d.mts +1 -1
- package/esm/providers/facebook-provider.d.mts.map +1 -1
- package/esm/providers/facebook-provider.mjs +1 -1
- package/esm/providers/facebook-provider.mjs.map +1 -1
- package/esm/providers/github-provider.d.mts +1 -1
- package/esm/providers/github-provider.d.mts.map +1 -1
- package/esm/providers/github-provider.mjs +1 -1
- package/esm/providers/github-provider.mjs.map +1 -1
- package/esm/providers/google-provider.d.mts +1 -1
- package/esm/providers/google-provider.d.mts.map +1 -1
- package/esm/providers/google-provider.mjs +1 -1
- package/esm/providers/google-provider.mjs.map +1 -1
- package/esm/providers/linkedin-provider.d.mts +1 -1
- package/esm/providers/linkedin-provider.d.mts.map +1 -1
- package/esm/providers/linkedin-provider.mjs +1 -1
- package/esm/providers/linkedin-provider.mjs.map +1 -1
- package/esm/providers/provider-login.d.mts +1 -1
- package/esm/providers/provider-login.d.mts.map +1 -1
- package/esm/providers/provider-login.mjs +1 -1
- package/esm/providers/provider-login.mjs.map +1 -1
- package/esm/providers/provider-state-cookie.mjs +1 -1
- package/esm/providers/provider-state-cookie.mjs.map +1 -1
- package/esm/providers/resolve-provider-user.mjs +1 -1
- package/esm/providers/resolve-provider-user.mjs.map +1 -1
- package/esm/providers/x-provider.d.mts +1 -1
- package/esm/providers/x-provider.d.mts.map +1 -1
- package/esm/providers/x-provider.mjs +1 -1
- package/esm/providers/x-provider.mjs.map +1 -1
- package/esm/services/auth-config.mjs +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.d.mts +1 -1
- package/esm/services/auth-events.d.mts.map +1 -1
- package/esm/services/auth-events.mjs +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +1 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +1 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +1 -1
- package/esm/services/current-user.d.mts.map +1 -1
- package/esm/services/current-user.mjs +1 -1
- package/esm/services/current-user.mjs.map +1 -1
- package/esm/services/email-verification.d.mts +1 -1
- package/esm/services/email-verification.d.mts.map +1 -1
- package/esm/services/email-verification.mjs +1 -1
- package/esm/services/email-verification.mjs.map +1 -1
- package/esm/services/generate-jwt-secret.d.mts +1 -1
- package/esm/services/generate-jwt-secret.d.mts.map +1 -1
- package/esm/services/generate-jwt-secret.mjs +1 -1
- package/esm/services/generate-jwt-secret.mjs.map +1 -1
- package/esm/services/jwt.d.mts +1 -1
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +1 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/services/one-time-tokens.d.mts +1 -1
- package/esm/services/one-time-tokens.d.mts.map +1 -1
- package/esm/services/one-time-tokens.mjs +1 -1
- package/esm/services/one-time-tokens.mjs.map +1 -1
- package/esm/services/optional-peer.mjs +1 -1
- package/esm/services/optional-peer.mjs.map +1 -1
- package/esm/services/password-reset.d.mts +1 -1
- package/esm/services/password-reset.d.mts.map +1 -1
- package/esm/services/password-reset.mjs +1 -1
- package/esm/services/password-reset.mjs.map +1 -1
- package/esm/services/resolve-request-user.d.mts +1 -1
- package/esm/services/resolve-request-user.d.mts.map +1 -1
- package/esm/services/resolve-request-user.mjs +1 -1
- package/esm/services/resolve-request-user.mjs.map +1 -1
- package/esm/services/token-family-operation.mjs +1 -1
- package/esm/services/token-family-operation.mjs.map +1 -1
- package/esm/services/token-notifications.mjs +1 -1
- package/esm/services/token-notifications.mjs.map +1 -1
- package/esm/session/page-session.d.mts +1 -1
- package/esm/session/page-session.d.mts.map +1 -1
- package/esm/session/page-session.mjs +1 -1
- package/esm/session/page-session.mjs.map +1 -1
- package/esm/utils/auth-error-codes.d.mts +1 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/safe-equal.d.mts +1 -1
- package/esm/utils/safe-equal.d.mts.map +1 -1
- package/esm/utils/safe-equal.mjs +1 -1
- package/esm/utils/safe-equal.mjs.map +1 -1
- package/esm/utils/token-expiry.d.mts +1 -1
- package/esm/utils/token-expiry.d.mts.map +1 -1
- package/esm/utils/token-expiry.mjs +1 -1
- package/esm/utils/token-expiry.mjs.map +1 -1
- package/llms-full.txt +3 -1
- package/package.json +10 -9
- package/skills/throttle-login-attempts/SKILL.md +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-jwt-secret.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"generate-jwt-secret.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/services/generate-jwt-secret.ts"],"mappings":";iBAKsB,iBAAA,IAAiB,OAAA"}
|
|
@@ -3,7 +3,7 @@ import { environment, rootPath } from "@warlock.js/core";
|
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
import { fs } from "@warlock.js/fs";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../../packages/auth/src/services/generate-jwt-secret.ts
|
|
7
7
|
async function generateJWTSecret() {
|
|
8
8
|
let envFile = rootPath(".env");
|
|
9
9
|
log.info("jwt", "generating", "Generating JWT secrets");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-jwt-secret.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"generate-jwt-secret.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/generate-jwt-secret.ts"],"sourcesContent":["import { fs } from \"@warlock.js/fs\";\nimport { Random } from \"@mongez/reinforcements\";\nimport { environment, rootPath } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\n\nexport async function generateJWTSecret() {\n let envFile = rootPath(\".env\");\n\n log.info(\"jwt\", \"generating\", \"Generating JWT secrets\");\n\n const environmentMode = environment();\n\n if (!(await fs.files.exists(envFile))) {\n const envFileType = environmentMode === \"production\" ? \".env.production\" : \".env.development\";\n envFile = rootPath(envFileType);\n }\n\n if (!(await fs.files.exists(envFile))) {\n log.error(\"jwt\", \"error\", \".env file not found\");\n return;\n }\n\n let contents = await fs.files.get(envFile);\n\n const hasJwtSecret = contents.includes(\"JWT_SECRET\");\n const hasJwtRefreshSecret = contents.includes(\"JWT_REFRESH_SECRET\");\n\n if (hasJwtSecret && hasJwtRefreshSecret) {\n log.warn(\"jwt\", \"exists\", \"JWT secrets already exist in the .env file.\");\n return;\n }\n\n let secretsToAdd = \"\";\n\n if (!hasJwtSecret) {\n const jwtSecret = Random.token(32);\n secretsToAdd += `\n# JWT Secret\nJWT_SECRET=${jwtSecret}\n`;\n log.success(\"jwt\", \"generated\", \"JWT_SECRET generated and added to the .env file.\");\n } else {\n log.info(\"jwt\", \"exists\", \"JWT_SECRET already exists in the .env file.\");\n }\n\n if (!hasJwtRefreshSecret) {\n const jwtRefreshSecret = Random.token(32);\n secretsToAdd += `\n# JWT Refresh Secret\nJWT_REFRESH_SECRET=${jwtRefreshSecret}\n`;\n log.success(\"jwt\", \"generated\", \"JWT_REFRESH_SECRET generated and added to the .env file.\");\n } else {\n log.info(\"jwt\", \"exists\", \"JWT_REFRESH_SECRET already exists in the .env file.\");\n }\n\n if (secretsToAdd) {\n contents += secretsToAdd;\n await fs.files.put(envFile, contents);\n }\n}\n"],"mappings":";;;;;;AAKA,eAAsB,oBAAoB;CACxC,IAAI,UAAU,SAAS,MAAM;CAE7B,IAAI,KAAK,OAAO,cAAc,wBAAwB;CAEtD,MAAM,kBAAkB,YAAY;CAEpC,IAAI,CAAE,MAAM,GAAG,MAAM,OAAO,OAAO,GAEjC,UAAU,SADU,oBAAoB,eAAe,oBAAoB,kBAC7C;CAGhC,IAAI,CAAE,MAAM,GAAG,MAAM,OAAO,OAAO,GAAI;EACrC,IAAI,MAAM,OAAO,SAAS,qBAAqB;EAC/C;CACF;CAEA,IAAI,WAAW,MAAM,GAAG,MAAM,IAAI,OAAO;CAEzC,MAAM,eAAe,SAAS,SAAS,YAAY;CACnD,MAAM,sBAAsB,SAAS,SAAS,oBAAoB;CAElE,IAAI,gBAAgB,qBAAqB;EACvC,IAAI,KAAK,OAAO,UAAU,6CAA6C;EACvE;CACF;CAEA,IAAI,eAAe;CAEnB,IAAI,CAAC,cAAc;EACjB,MAAM,YAAY,OAAO,MAAM,EAAE;EACjC,gBAAgB;;aAEP,UAAU;;EAEnB,IAAI,QAAQ,OAAO,aAAa,kDAAkD;CACpF,OACE,IAAI,KAAK,OAAO,UAAU,6CAA6C;CAGzE,IAAI,CAAC,qBAAqB;EACxB,MAAM,mBAAmB,OAAO,MAAM,EAAE;EACxC,gBAAgB;;qBAEC,iBAAiB;;EAElC,IAAI,QAAQ,OAAO,aAAa,0DAA0D;CAC5F,OACE,IAAI,KAAK,OAAO,UAAU,qDAAqD;CAGjF,IAAI,cAAc;EAChB,YAAY;EACZ,MAAM,GAAG,MAAM,IAAI,SAAS,QAAQ;CACtC;AACF"}
|
package/esm/services/jwt.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
|
|
2
2
|
import { SignerOptions, VerifierOptions } from "fast-jwt";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/auth/src/services/jwt.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Token class. Stamped as the `tokenType` claim on every signed token and
|
|
7
7
|
* checked on verify so an access token can never be accepted where a refresh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"jwt.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/services/jwt.ts"],"mappings":";;;;;;AAoBA;;;;AAAqB;AAoCrB;KApCY,SAAA;;;AAoC2C;AAevD;;iBAfgB,wBAAA,CAAyB,KAAc;;;;;;;;;;cAe1C,cAAA,SAAuB,KAAA;EAAA,SACzB,IAAA,GAAI,cAAA,CAAA,gBAAA;cAED,QAAA,EAAU,SAAA,EAAW,MAAA;AAAA;AAAA,cA+CtB,GAAA;EA/CoC;AA+CjD;;;;IAMgB,GAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAKT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EAAA;;;;;sBAaW,KAAA;IACC,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;EAmBG;;;;IAAA,GAAA;IAAA,SAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAMT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EASY;;;kCADW,KAAA;IACX,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;AAAA"}
|
package/esm/services/jwt.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { authConfig } from "./auth-config.mjs";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { createSigner, createVerifier } from "fast-jwt";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../../packages/auth/src/services/jwt.ts
|
|
7
7
|
const getSecretKey = () => authConfig.accessToken.secret();
|
|
8
8
|
const getAlgorithm = () => authConfig.accessToken.algorithm();
|
|
9
9
|
const getRefreshSecretKey = () => authConfig.refreshToken.secret() || getSecretKey();
|
package/esm/services/jwt.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"jwt.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/jwt.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { createSigner, createVerifier, type SignerOptions, type VerifierOptions } from \"fast-jwt\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\nimport { authConfig } from \"./auth-config\";\n\nconst getSecretKey = () => authConfig.accessToken.secret();\nconst getAlgorithm = () => authConfig.accessToken.algorithm();\n\n// Refresh tokens may declare their own secret; when unset/empty we fall back to\n// the access-token secret (the documented optional behavior).\nconst getRefreshSecretKey = () => authConfig.refreshToken.secret() || getSecretKey();\n\n/**\n * Token class. Stamped as the `tokenType` claim on every signed token and\n * checked on verify so an access token can never be accepted where a refresh\n * token is expected (and vice versa) — even when both share the same secret\n * under the documented refresh-secret fallback. Legacy tokens minted before\n * this claim existed carry no `tokenType` and remain accepted; a *mismatched*\n * type is always rejected.\n */\nexport type TokenType = \"access\" | \"refresh\";\n\nconst ACCESS_TOKEN_TYPE: TokenType = \"access\";\nconst REFRESH_TOKEN_TYPE: TokenType = \"refresh\";\n\n/**\n * Error codes that mean \"the credential itself is bad\", as opposed to \"this\n * server could not check it\". This is an ALLOWLIST and must stay one. The codes\n * deliberately left out — `FAST_JWT_INVALID_KEY`, `FAST_JWT_MISSING_KEY`,\n * `FAST_JWT_KEY_FETCHING_ERROR`, `FAST_JWT_INVALID_OPTION`,\n * `FAST_JWT_VERIFY_ERROR`, and `FAST_JWT_SIGN_ERROR` — describe a broken\n * server, not a broken token, and so does every unknown future code.\n */\nconst INVALID_CREDENTIAL_ERROR_CODES = new Set<string>([\n \"FAST_JWT_MALFORMED\",\n \"FAST_JWT_INVALID_SIGNATURE\",\n \"FAST_JWT_MISSING_SIGNATURE\",\n \"FAST_JWT_INVALID_ALGORITHM\",\n \"FAST_JWT_EXPIRED\",\n \"FAST_JWT_INACTIVE\",\n // How a token carrying no `exp` is rejected — `jwt.verify` forces `exp` into\n // `requiredClaims`, so this code IS the missing-deadline guard firing.\n \"FAST_JWT_MISSING_REQUIRED_CLAIM\",\n \"FAST_JWT_INVALID_CLAIM_VALUE\",\n \"FAST_JWT_INVALID_CLAIM_TYPE\",\n \"FAST_JWT_INVALID_CRIT_HEADER\",\n \"FAST_JWT_INVALID_TYPE\",\n \"FAST_JWT_INVALID_PAYLOAD\",\n AuthErrorCodes.InvalidTokenType,\n]);\n\n/**\n * Only coded credential failures become authentication misses. Plain errors,\n * including missing-secret configuration failures, propagate to the central\n * server-error path instead of reading as \"everyone's token is bad\".\n */\nexport function isInvalidCredentialError(error: unknown): boolean {\n const code = (error as { code?: unknown } | null | undefined)?.code;\n\n return typeof code === \"string\" && INVALID_CREDENTIAL_ERROR_CODES.has(code);\n}\n\n/**\n * A `tokenType` claim that does not match what the caller expects. This IS a\n * bad credential (an access-token cookie can only hold a refresh token\n * through an app bug) — but unlike `fast-jwt`'s own rejections it was\n * previously a plain `Error`, unclassifiable by a caller that wants to answer\n * 401 without string-matching a message that could reword on any release.\n * `code` mirrors how `fast-jwt`'s `TokenError` carries its own code, so a\n * caller can classify both through `isInvalidCredentialError` above.\n */\nexport class TokenTypeError extends Error {\n readonly code = AuthErrorCodes.InvalidTokenType;\n\n constructor(expected: TokenType, actual: string) {\n super(`Invalid token type: expected \"${expected}\", received \"${actual}\".`);\n this.name = \"TokenTypeError\";\n }\n}\n\n/**\n * Reject the token when its `tokenType` claim is present and does not match the\n * expected class. Absent claim ⇒ legacy token, accepted (backward compatible).\n */\nfunction assertTokenType(decoded: unknown, expected: TokenType): void {\n const actual = (decoded as { tokenType?: unknown } | null | undefined)?.tokenType;\n\n if (typeof actual === \"string\" && actual !== expected) {\n throw new TokenTypeError(expected, actual);\n }\n}\n\n/**\n * Claims no token may be missing, whatever the caller asks for.\n *\n * A JWT with no `exp` is not \"a token with a long life\" — it is a credential\n * with *no* life, because a verifier with no deadline to check simply succeeds,\n * forever (measured against `fast-jwt@6.2.4`: a token with no `exp` verifies\n * unchanged at `clockTimestamp` + 100 years). Nothing in this package can mint\n * one as of 4.12.0, but tokens minted by an earlier version are already in the\n * wild, so the rejection lives on the *verify* side where it catches a token\n * from any version, including one signed by a service this package never ran.\n *\n * There is no legitimate source to preserve: an app that wants a token that\n * effectively never expires sets `expiresIn: NO_EXPIRATION` (`\"100y\"`), which\n * mints a real `exp` roughly a century out (measured: `ms(\"100y\")` ⇒\n * `3155760000000`, `exp - iat` ⇒ `3155760000` seconds). \"No deadline\" and \"a\n * distant deadline\" are different things, and only the second one is asked for.\n */\nconst REQUIRED_CLAIMS = [\"exp\"];\n\n/**\n * Union the caller's `requiredClaims` with the mandatory ones — a caller may\n * add requirements, never drop them.\n */\nfunction withRequiredClaims(callerClaims?: string[]): string[] {\n if (!callerClaims?.length) return REQUIRED_CLAIMS;\n\n return [...new Set([...callerClaims, ...REQUIRED_CLAIMS])];\n}\n\nexport const jwt = {\n /**\n * Generate a new JWT token for the user.\n * @param payload The payload to encode in the JWT token.\n */\n async generate(\n payload: any,\n {\n key = getSecretKey(),\n algorithm = getAlgorithm(),\n ...options\n }: SignerOptions & { key?: string } = {},\n ): Promise<string> {\n // Create a signer function with predefined options\n const sign = createSigner({ key, ...options, algorithm });\n\n const token = await sign({ jti: randomUUID(), ...payload, tokenType: ACCESS_TOKEN_TYPE });\n return token;\n },\n\n /**\n * Verify the given token.\n * @param token The JWT token to verify.\n * @returns The decoded token payload if verification is successful.\n */\n async verify<T = unknown>(\n token: string,\n {\n key = getSecretKey(),\n algorithms = [getAlgorithm()],\n requiredClaims,\n ...options\n }: VerifierOptions & { key?: string } = {},\n ): Promise<T> {\n const verify = createVerifier({\n key,\n ...options,\n algorithms,\n requiredClaims: withRequiredClaims(requiredClaims),\n });\n\n const decoded = await verify(token as string);\n\n assertTokenType(decoded, ACCESS_TOKEN_TYPE);\n\n return decoded;\n },\n\n /**\n * Generate a new refresh token for the user.\n */\n async generateRefreshToken(\n payload: any,\n {\n key = getRefreshSecretKey(),\n expiresIn,\n algorithm = getAlgorithm(),\n ...options\n }: SignerOptions & { key?: string } = {},\n ): Promise<string> {\n const sign = createSigner({ key, expiresIn, algorithm, ...options });\n return sign({ jti: randomUUID(), ...payload, tokenType: REFRESH_TOKEN_TYPE });\n },\n\n /**\n * Verify the given refresh token.\n */\n async verifyRefreshToken<T = unknown>(\n token: string,\n {\n key = getRefreshSecretKey(),\n algorithms = [getAlgorithm()],\n requiredClaims,\n ...options\n }: VerifierOptions & { key?: string } = {},\n ): Promise<T> {\n const verify = createVerifier({\n key,\n algorithms,\n ...options,\n requiredClaims: withRequiredClaims(requiredClaims),\n });\n\n const decoded = await verify(token);\n\n assertTokenType(decoded, REFRESH_TOKEN_TYPE);\n\n return decoded;\n },\n};\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB,WAAW,YAAY,OAAO;AACzD,MAAM,qBAAqB,WAAW,YAAY,UAAU;AAI5D,MAAM,4BAA4B,WAAW,aAAa,OAAO,KAAK,aAAa;AAYnF,MAAM,oBAA+B;AACrC,MAAM,qBAAgC;;;;;;;;;AAUtC,MAAM,iCAAiC,IAAI,IAAY;CACrD;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;;AAEF,CAAC;;;;;;AAOD,SAAgB,yBAAyB,OAAyB;CAChE,MAAM,OAAQ,OAAiD;CAE/D,OAAO,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI;AAC5E;;;;;;;;;;AAWA,IAAa,iBAAb,cAAoC,MAAM;CAGxC,YAAY,UAAqB,QAAgB;EAC/C,MAAM,iCAAiC,SAAS,eAAe,OAAO,GAAG;;EACzE,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAS,gBAAgB,SAAkB,UAA2B;CACpE,MAAM,SAAU,SAAwD;CAExE,IAAI,OAAO,WAAW,YAAY,WAAW,UAC3C,MAAM,IAAI,eAAe,UAAU,MAAM;AAE7C;;;;;;;;;;;;;;;;;;AAmBA,MAAM,kBAAkB,CAAC,KAAK;;;;;AAM9B,SAAS,mBAAmB,cAAmC;CAC7D,IAAI,CAAC,cAAc,QAAQ,OAAO;CAElC,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,cAAc,GAAG,eAAe,CAAC,CAAC;AAC3D;AAEA,MAAa,MAAM;;;;;CAKjB,MAAM,SACJ,SACA,EACE,MAAM,aAAa,GACnB,YAAY,aAAa,GACzB,GAAG,YACiC,CAAC,GACtB;EAKjB,OAAO,MAHM,aAAa;GAAE;GAAK,GAAG;GAAS;EAAU,CAEhC,CAAC,CAAC;GAAE,KAAK,WAAW;GAAG,GAAG;GAAS,WAAW;EAAkB,CAAC;CAE1F;;;;;;CAOA,MAAM,OACJ,OACA,EACE,MAAM,aAAa,GACnB,aAAa,CAAC,aAAa,CAAC,GAC5B,gBACA,GAAG,YACmC,CAAC,GAC7B;EAQZ,MAAM,UAAU,MAPD,eAAe;GAC5B;GACA,GAAG;GACH;GACA,gBAAgB,mBAAmB,cAAc;EACnD,CAE2B,CAAC,CAAC,KAAe;EAE5C,gBAAgB,SAAS,iBAAiB;EAE1C,OAAO;CACT;;;;CAKA,MAAM,qBACJ,SACA,EACE,MAAM,oBAAoB,GAC1B,WACA,YAAY,aAAa,GACzB,GAAG,YACiC,CAAC,GACtB;EAEjB,OADa,aAAa;GAAE;GAAK;GAAW;GAAW,GAAG;EAAQ,CACxD,CAAC,CAAC;GAAE,KAAK,WAAW;GAAG,GAAG;GAAS,WAAW;EAAmB,CAAC;CAC9E;;;;CAKA,MAAM,mBACJ,OACA,EACE,MAAM,oBAAoB,GAC1B,aAAa,CAAC,aAAa,CAAC,GAC5B,gBACA,GAAG,YACmC,CAAC,GAC7B;EAQZ,MAAM,UAAU,MAPD,eAAe;GAC5B;GACA;GACA,GAAG;GACH,gBAAgB,mBAAmB,cAAc;EACnD,CAE2B,CAAC,CAAC,KAAK;EAElC,gBAAgB,SAAS,kBAAkB;EAE3C,OAAO;CACT;AACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
2
2
|
import { OneTimeToken, OneTimeTokenPurpose } from "../models/one-time-token/one-time-token.model.mjs";
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/auth/src/services/one-time-tokens.d.ts
|
|
4
4
|
/** The raw token handed to the notification, with its expiry. */
|
|
5
5
|
type IssuedOneTimeToken = {
|
|
6
6
|
token: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"one-time-tokens.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"one-time-tokens.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/services/one-time-tokens.ts"],"mappings":";;;;KAOY,kBAAA;EACV,KAAA;EACA,SAAA,EAAW,IAAI;AAAA;;iBAID,iBAAA,WAA4B,YAAY;;iBAKxC,gBAAA,CAAiB,KAAa;;;AAT7B;AAIjB;;;;iBAgBsB,iBAAA,CACpB,IAAA,EAAM,IAAA,EACN,OAAA,EAAS,mBAAA,EACT,WAAA,UACA,kBAAA,aACC,OAAA,CAAQ,kBAAA;AAhBX;;;;AAA8C;AAW9C;;;AAXA,iBAuCsB,mBAAA,CACpB,QAAA,UACA,OAAA,EAAS,mBAAA,GACR,OAAA,CAAQ,IAAA"}
|
|
@@ -4,7 +4,7 @@ import { InvalidOneTimeTokenError } from "../errors/invalid-one-time-token.error
|
|
|
4
4
|
import { config } from "@warlock.js/core";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
|
|
7
|
-
//#region
|
|
7
|
+
//#region ../../packages/auth/src/services/one-time-tokens.ts
|
|
8
8
|
/** Resolve the active one-time-token model — the default or `auth.oneTimeToken.model`. */
|
|
9
9
|
function oneTimeTokenModel() {
|
|
10
10
|
return config.key("auth.oneTimeToken.model", OneTimeToken);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"one-time-tokens.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"one-time-tokens.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/one-time-tokens.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { InvalidOneTimeTokenError } from \"../errors/invalid-one-time-token.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { OneTimeToken, type OneTimeTokenPurpose } from \"../models/one-time-token\";\n\n/** The raw token handed to the notification, with its expiry. */\nexport type IssuedOneTimeToken = {\n token: string;\n expiresAt: Date;\n};\n\n/** Resolve the active one-time-token model — the default or `auth.oneTimeToken.model`. */\nexport function oneTimeTokenModel(): typeof OneTimeToken {\n return config.key(\"auth.oneTimeToken.model\", OneTimeToken);\n}\n\n/** SHA-256 hex digest of a raw one-time token — the only form ever persisted. */\nexport function hashOneTimeToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n/**\n * Create and persist a one-time token for the user: 32 random bytes,\n * base64url-encoded. Only the hash reaches storage.\n *\n * @param invalidatePrevious - consume the user's still-unused tokens of the\n * same purpose first (password reset: only the newest link works).\n */\nexport async function issueOneTimeToken(\n user: Auth,\n purpose: OneTimeTokenPurpose,\n expiresInMs: number,\n invalidatePrevious = false,\n): Promise<IssuedOneTimeToken> {\n const Model = oneTimeTokenModel();\n\n if (invalidatePrevious) {\n await Model.invalidateActiveFor(user, purpose);\n }\n\n const token = randomBytes(32).toString(\"base64url\");\n const expiresAt = new Date(Date.now() + expiresInMs);\n\n await Model.issue(user, purpose, hashOneTimeToken(token), expiresAt);\n\n return { token, expiresAt };\n}\n\n/**\n * Consume a raw one-time token of the given purpose and resolve its user.\n *\n * Unknown, other-purpose, expired, already-consumed, lost-the-race and\n * orphaned (user gone) tokens all throw the same {@link InvalidOneTimeTokenError}.\n * The consume is a conditional update, so of N concurrent callers exactly one\n * proceeds.\n */\nexport async function consumeOneTimeToken(\n rawToken: string,\n purpose: OneTimeTokenPurpose,\n): Promise<Auth> {\n if (typeof rawToken !== \"string\" || rawToken.length === 0) {\n throw new InvalidOneTimeTokenError();\n }\n\n const row = await oneTimeTokenModel().findByHash(hashOneTimeToken(rawToken), purpose);\n\n if (!row || row.isConsumed || row.isExpired) {\n throw new InvalidOneTimeTokenError();\n }\n\n const UserModel = config.key(`auth.userType.${row.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${row.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(row.userId)) as Auth | null;\n\n if (!user) {\n throw new InvalidOneTimeTokenError();\n }\n\n if (!(await row.consumeIfActive())) {\n throw new InvalidOneTimeTokenError();\n }\n\n return user;\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,oBAAyC;CACvD,OAAO,OAAO,IAAI,2BAA2B,YAAY;AAC3D;;AAGA,SAAgB,iBAAiB,OAAuB;CACtD,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AACxD;;;;;;;;AASA,eAAsB,kBACpB,MACA,SACA,aACA,qBAAqB,OACQ;CAC7B,MAAM,QAAQ,kBAAkB;CAEhC,IAAI,oBACF,MAAM,MAAM,oBAAoB,MAAM,OAAO;CAG/C,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;CAClD,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,WAAW;CAEnD,MAAM,MAAM,MAAM,MAAM,SAAS,iBAAiB,KAAK,GAAG,SAAS;CAEnE,OAAO;EAAE;EAAO;CAAU;AAC5B;;;;;;;;;AAUA,eAAsB,oBACpB,UACA,SACe;CACf,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GACtD,MAAM,IAAI,yBAAyB;CAGrC,MAAM,MAAM,MAAM,kBAAkB,CAAC,CAAC,WAAW,iBAAiB,QAAQ,GAAG,OAAO;CAEpF,IAAI,CAAC,OAAO,IAAI,cAAc,IAAI,WAChC,MAAM,IAAI,yBAAyB;CAGrC,MAAM,YAAY,OAAO,IAAI,iBAAiB,IAAI,UAAU;CAE5D,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,aAAa,IAAI,SAAS,kBAAkB;CAG9D,MAAM,OAAQ,MAAM,UAAU,KAAK,IAAI,MAAM;CAE7C,IAAI,CAAC,MACH,MAAM,IAAI,yBAAyB;CAGrC,IAAI,CAAE,MAAM,IAAI,gBAAgB,GAC9B,MAAM,IAAI,yBAAyB;CAGrC,OAAO;AACT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthProviderSdkMissingError } from "../errors/auth-provider-sdk-missing.error.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/auth/src/services/optional-peer.ts
|
|
4
4
|
/**
|
|
5
5
|
* Whether `error` (or an error it wraps as `cause`) is the resolver reporting
|
|
6
6
|
* that `packageName` ITSELF is absent. A missing transitive dependency names
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optional-peer.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"optional-peer.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/optional-peer.ts"],"sourcesContent":["import { AuthProviderSdkMissingError } from \"../errors/auth-provider-sdk-missing.error\";\n\n/**\n * Whether `error` (or an error it wraps as `cause`) is the resolver reporting\n * that `packageName` ITSELF is absent. A missing transitive dependency names\n * a different package and is not treated as \"not installed\".\n */\nfunction isPackageMissing(error: unknown, packageName: string): boolean {\n for (let current = error, depth = 0; current && depth < 5; depth++) {\n const { code, message } = current as { code?: unknown; message?: unknown };\n\n if (\n (code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\") &&\n typeof message === \"string\" &&\n message.includes(`'${packageName}'`)\n ) {\n return true;\n }\n\n current = (current as { cause?: unknown }).cause;\n }\n\n return false;\n}\n\n/**\n * Load an optional vendor SDK lazily. The specifier reaches `import()` as a\n * variable, so neither the type-checker nor a bundler ever requires the peer;\n * nothing is loaded until a login method actually runs.\n *\n * @param packageName - the optional peer, e.g. `\"jose\"`.\n * @param feature - the `warlock add` feature that installs it, e.g. `\"auth-google\"`.\n * @throws AuthProviderSdkMissingError when the package is not installed.\n */\nexport async function loadOptionalPeer<TModule extends object>(\n packageName: string,\n feature: string,\n): Promise<TModule> {\n try {\n return (await import(packageName)) as TModule;\n } catch (error) {\n if (isPackageMissing(error, packageName)) {\n throw new AuthProviderSdkMissingError(packageName, feature, error);\n }\n\n throw error;\n }\n}\n"],"mappings":";;;;;;;;AAOA,SAAS,iBAAiB,OAAgB,aAA8B;CACtE,KAAK,IAAI,UAAU,OAAO,QAAQ,GAAG,WAAW,QAAQ,GAAG,SAAS;EAClE,MAAM,EAAE,MAAM,YAAY;EAE1B,KACG,SAAS,0BAA0B,SAAS,uBAC7C,OAAO,YAAY,YACnB,QAAQ,SAAS,IAAI,YAAY,EAAE,GAEnC,OAAO;EAGT,UAAW,QAAgC;CAC7C;CAEA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,iBACpB,aACA,SACkB;CAClB,IAAI;EACF,OAAQ,MAAM,OAAO;CACvB,SAAS,OAAO;EACd,IAAI,iBAAiB,OAAO,WAAW,GACrC,MAAM,IAAI,4BAA4B,aAAa,SAAS,KAAK;EAGnE,MAAM;CACR;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
2
2
|
import { ChildModel } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/auth/src/services/password-reset.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Start a password reset for the account whose `auth.passwordReset.identifierField`
|
|
7
7
|
* (default `email`) equals `identifier`: invalidate the account's unused reset
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-reset.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"password-reset.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/services/password-reset.ts"],"mappings":";;;;;;AAwDA;;;;;;;;;;;;;iBAAsB,oBAAA,WAA+B,IAAA,EACnD,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,UAAA,WACC,OAAA;;;;AAAO;AAmCV;;;;;;iBAAsB,aAAA,CAAc,KAAA,UAAe,WAAA,WAAsB,OAAO,CAAC,IAAA"}
|
|
@@ -4,7 +4,7 @@ import { authService } from "./auth.service.mjs";
|
|
|
4
4
|
import { consumeOneTimeToken, issueOneTimeToken } from "./one-time-tokens.mjs";
|
|
5
5
|
import { resolveTokenNotification } from "./token-notifications.mjs";
|
|
6
6
|
|
|
7
|
-
//#region
|
|
7
|
+
//#region ../../packages/auth/src/services/password-reset.ts
|
|
8
8
|
/**
|
|
9
9
|
* Default password writer. Login verifies with `authService.verifyPassword`,
|
|
10
10
|
* so that is the authority on what "stored correctly" means:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password-reset.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"password-reset.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/password-reset.ts"],"sourcesContent":["import type { ChildModel } from \"@warlock.js/cascade\";\nimport type { Auth } from \"../models/auth.model\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { authService } from \"./auth.service\";\nimport { consumeOneTimeToken, issueOneTimeToken } from \"./one-time-tokens\";\nimport { resolveTokenNotification } from \"./token-notifications\";\n\n/**\n * Default password writer. Login verifies with `authService.verifyPassword`,\n * so that is the authority on what \"stored correctly\" means:\n *\n * 1. Save `hashPassword(plain)` — right for a model that stores what it's given.\n * 2. If that no longer verifies, the model hashed on save (core's\n * `useHashedPassword()`), double-hashing it: save the plaintext so the\n * model's own transformer hashes it once.\n * 3. Still not verifiable ⇒ throw. Never leave a reset that \"succeeded\" with a\n * password nobody can log in with.\n *\n * The first write is always a hash, so a model without a transformer never\n * has plaintext written.\n */\nasync function writePassword(user: Auth, plainPassword: string): Promise<void> {\n await user.merge({ password: await authService.hashPassword(plainPassword) } as never).save();\n\n if (await authService.verifyPassword(plainPassword, user.string(\"password\") ?? \"\")) return;\n\n await user.merge({ password: plainPassword } as never).save();\n\n const stored = user.string(\"password\") ?? \"\";\n\n if (stored !== plainPassword && (await authService.verifyPassword(plainPassword, stored))) {\n return;\n }\n\n throw new Error(\n \"@warlock.js/auth: resetPassword could not store a password that verifies — \" +\n \"set `auth.passwordReset.setPassword` to write it the way your user model expects.\",\n );\n}\n\n/**\n * Start a password reset for the account whose `auth.passwordReset.identifierField`\n * (default `email`) equals `identifier`: invalidate the account's unused reset\n * tokens, issue a new one, and send it through the reset notification.\n *\n * **Anti-enumeration:** resolves `undefined` whether or not the account exists\n * — an unknown identifier simply sends nothing. Notification availability is\n * checked before the lookup, so a misconfigured app fails identically for both.\n *\n * @throws NotificationsUnavailableError when `@warlock.js/notifications` is not installed or not configured.\n *\n * @example\n * await requestPasswordReset(User, request.input(\"email\"));\n * return response.success({ message: \"If the account exists, we sent a reset link.\" });\n */\nexport async function requestPasswordReset<T extends Auth>(\n Model: ChildModel<T>,\n identifier: string,\n): Promise<void> {\n const notification = await resolveTokenNotification(\"password-reset\");\n\n if (typeof identifier !== \"string\" || identifier.length === 0) return;\n\n const user = (await Model.first({\n [authConfig.passwordReset.identifierField()]: identifier,\n })) as T | null;\n\n if (!user) return;\n\n const { token, expiresAt } = await issueOneTimeToken(\n user,\n \"password-reset\",\n authConfig.passwordReset.expiresInMs(),\n true,\n );\n\n authEvents.emit(\"password.resetRequested\", user, token);\n\n const url = authConfig.passwordReset.url()?.(token, user);\n\n await notification.send(user, { token, expiresAt, ...(url ? { url } : {}) });\n}\n\n/**\n * Complete a password reset: consume the token, store the new password, then\n * revoke every access token, refresh token and cookie session the user holds\n * (`authService.revokeAllTokens`). Resolves to the user.\n *\n * Validate password strength BEFORE calling this — the token is consumed first.\n *\n * @throws InvalidOneTimeTokenError for an unknown, verification-purpose,\n * expired, superseded or already-used token.\n */\nexport async function resetPassword(token: string, newPassword: string): Promise<Auth> {\n const user = await consumeOneTimeToken(token, \"password-reset\");\n\n const setPassword = authConfig.passwordReset.setPassword();\n\n if (setPassword) {\n await setPassword(user, newPassword);\n } else {\n await writePassword(user, newPassword);\n }\n\n await authService.revokeAllTokens(user);\n\n authEvents.emit(\"password.reset\", user);\n\n return user;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,eAAe,cAAc,MAAY,eAAsC;CAC7E,MAAM,KAAK,MAAM,EAAE,UAAU,MAAM,YAAY,aAAa,aAAa,EAAE,CAAU,CAAC,CAAC,KAAK;CAE5F,IAAI,MAAM,YAAY,eAAe,eAAe,KAAK,OAAO,UAAU,KAAK,EAAE,GAAG;CAEpF,MAAM,KAAK,MAAM,EAAE,UAAU,cAAc,CAAU,CAAC,CAAC,KAAK;CAE5D,MAAM,SAAS,KAAK,OAAO,UAAU,KAAK;CAE1C,IAAI,WAAW,iBAAkB,MAAM,YAAY,eAAe,eAAe,MAAM,GACrF;CAGF,MAAM,IAAI,MACR,8JAEF;AACF;;;;;;;;;;;;;;;;AAiBA,eAAsB,qBACpB,OACA,YACe;CACf,MAAM,eAAe,MAAM,yBAAyB,gBAAgB;CAEpE,IAAI,OAAO,eAAe,YAAY,WAAW,WAAW,GAAG;CAE/D,MAAM,OAAQ,MAAM,MAAM,MAAM,GAC7B,WAAW,cAAc,gBAAgB,IAAI,WAChD,CAAC;CAED,IAAI,CAAC,MAAM;CAEX,MAAM,EAAE,OAAO,cAAc,MAAM,kBACjC,MACA,kBACA,WAAW,cAAc,YAAY,GACrC,IACF;CAEA,WAAW,KAAK,2BAA2B,MAAM,KAAK;CAEtD,MAAM,MAAM,WAAW,cAAc,IAAI,CAAC,GAAG,OAAO,IAAI;CAExD,MAAM,aAAa,KAAK,MAAM;EAAE;EAAO;EAAW,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;CAAG,CAAC;AAC7E;;;;;;;;;;;AAYA,eAAsB,cAAc,OAAe,aAAoC;CACrF,MAAM,OAAO,MAAM,oBAAoB,OAAO,gBAAgB;CAE9D,MAAM,cAAc,WAAW,cAAc,YAAY;CAEzD,IAAI,aACF,MAAM,YAAY,MAAM,WAAW;MAEnC,MAAM,cAAc,MAAM,WAAW;CAGvC,MAAM,YAAY,gBAAgB,IAAI;CAEtC,WAAW,KAAK,kBAAkB,IAAI;CAEtC,OAAO;AACT"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CookieDomainResolver, TokenFrom } from "../contracts/types.mjs";
|
|
2
2
|
import { Request, Response } from "@warlock.js/core";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/auth/src/services/resolve-request-user.d.ts
|
|
5
5
|
type ResolveRequestUserOptions = {
|
|
6
6
|
/** Where the access token is read from. Defaults to `"header"`. */tokenFrom?: TokenFrom; /** Refresh credential source; enables one automatic renewal per call. */
|
|
7
7
|
refreshCredential?: TokenFrom; /** Refresh overlap window forwarded to `authService.renewAutomaticSession`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-request-user.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"resolve-request-user.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/services/resolve-request-user.ts"],"mappings":";;;;KAgBY,yBAAA;qEAEV,SAAA,GAAY,SAAA,EAFuB;EAInC,iBAAA,GAAoB,SAAA,EAFR;EAIZ,SAAA,WAQe;EANf,YAAA,aAMmC;EAJnC,QAAA,WARY;EAUZ,eAAA,WARoB;EAUpB,YAAA,GAAe,oBAAA;AAAA;;;;;KAOL,kBAAA;AAAA,KAEA,kBAAA;EACR,IAAA,EAAM,KAAA;EAAO,OAAA;AAAA;EAA0B,IAAA;EAAY,OAAA,EAAS,kBAAkB;AAAA;;iBAoBlE,cAAA,CAAe,OAAA,EAAS,OAAA,EAAS,SAAA,EAAW,SAAS;;;;;;;;;;AApBa;AAoBlF;iBAqBsB,yBAAA,CACpB,OAAA,EAAS,OAAA,EACT,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,yBAAA,GACR,OAAA,CAAQ,kBAAA;;;;;;;;iBAmHK,kBAAA,CACd,OAAA,EAAS,OAAA,EACT,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,yBAAA,GACR,OAAA"}
|
|
@@ -5,7 +5,7 @@ import { authService } from "./auth.service.mjs";
|
|
|
5
5
|
import { config } from "@warlock.js/core";
|
|
6
6
|
import { log } from "@warlock.js/logger";
|
|
7
7
|
|
|
8
|
-
//#region
|
|
8
|
+
//#region ../../packages/auth/src/services/resolve-request-user.ts
|
|
9
9
|
/**
|
|
10
10
|
* Whether the persisted row says the token is dead.
|
|
11
11
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-request-user.mjs","names":[],"sources":["../../../../../../../auth/src/services/resolve-request-user.ts"],"sourcesContent":["import { config, type Request, type Response } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport type { CookieDomainResolver, TokenFrom } from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport { authService } from \"./auth.service\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\n/**\n * Decoded access-token claims the resolver reads. The full payload carries\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\n */\ntype DecodedAccessToken = {\n id: string | number;\n userType?: string;\n};\n\nexport type ResolveRequestUserOptions = {\n /** Where the access token is read from. Defaults to `\"header\"`. */\n tokenFrom?: TokenFrom;\n /** Refresh credential source; enables one automatic renewal per call. */\n refreshCredential?: TokenFrom;\n /** Refresh overlap window forwarded to `authService.renewAutomaticSession`. */\n overlapMs?: number;\n /** User types allowed to pass. Empty means any authenticated type. */\n allowedTypes?: string[];\n /** Absolute family lifetime in ms; rotation never extends a family past it. */\n maxAgeMs?: number;\n /** The single user type a renewal mints for. Required for renewal. */\n renewalUserType?: string;\n /** Select a request-scoped Domain for renewed cookies. */\n cookieDomain?: CookieDomainResolver;\n};\n\n/**\n * Why a request did not resolve to a user. The middleware maps each reason to\n * its own response; `resolveRequestUser` collapses them all to `null`.\n */\nexport type RequestUserFailure = \"missing\" | \"invalid\" | \"forbidden\" | \"unauthorized\";\n\nexport type RequestUserOutcome<TUser = any> =\n { user: TUser; failure?: undefined } | { user: null; failure: RequestUserFailure };\n\n/**\n * Whether the persisted row says the token is dead.\n *\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\n * override that renames or reshapes its expiry column stays authoritative. A\n * row that cannot answer at all — an override that dropped the getter — is\n * treated as **expired**: a check must never quietly answer \"fine\" when it has\n * nothing to check.\n *\n * This is independent of the `exp` claim required by `jwt.verify`: that guard\n * catches a token whose *claims* carry no deadline; this one catches a token\n * whose *row* says the deadline has passed.\n */\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\n}\n\n/** Read one credential (access or refresh) from the header or a cookie. */\nexport function readCredential(request: Request, tokenFrom: TokenFrom): string {\n if (tokenFrom === \"header\") {\n return request.authorizationValue;\n }\n\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\n\n return value ? String(value) : \"\";\n}\n\n/**\n * Resolve a request's credential to a user, reporting WHY it failed.\n *\n * Side effects match what `authMiddleware` always did: `request.locals.user` is\n * cleared on every failure, `request.decodedAccessToken` is set once a token\n * verifies, and stale rows are destroyed. It does NOT set `request.locals.user`\n * on success — the caller decides that.\n *\n * At most one renewal is attempted per call. The renewed access token is\n * checked through the same path; no handler or unsafe request replay happens.\n */\nexport async function resolveRequestUserOutcome(\n request: Request,\n response: Response,\n options: ResolveRequestUserOptions = {},\n): Promise<RequestUserOutcome> {\n const tokenFrom = options.tokenFrom ?? \"header\";\n const allowedTypes = options.allowedTypes ?? [];\n const { refreshCredential } = options;\n let authorizationValue = readCredential(request, tokenFrom);\n let renewalAttempted = false;\n\n const renew = async (): Promise<boolean> => {\n if (!refreshCredential || renewalAttempted) return false;\n\n renewalAttempted = true;\n const refreshToken = readCredential(request, refreshCredential);\n if (!refreshToken) return false;\n\n const pair = await authService.renewAutomaticSession(refreshToken, options.renewalUserType!, {\n overlapMs: options.overlapMs,\n maxAgeMs: options.maxAgeMs,\n });\n if (!pair?.refreshToken) return false;\n\n authService.setAuthCookie(response, pair.accessToken, {\n name: tokenFrom.slice(\"cookie:\".length),\n domain: options.cookieDomain?.(request),\n });\n authService.setAuthCookie(response, pair.refreshToken, {\n name: refreshCredential.slice(\"cookie:\".length),\n domain: options.cookieDomain?.(request),\n });\n authorizationValue = pair.accessToken.token;\n\n return true;\n };\n\n const fail = (failure: RequestUserFailure): RequestUserOutcome => {\n request.locals.user = undefined;\n\n return { user: null, failure };\n };\n\n let decoded: DecodedAccessToken;\n let accessToken: AccessToken | null;\n\n while (true) {\n if (!authorizationValue) {\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"missing\");\n }\n\n try {\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\n } catch (error) {\n if (!isInvalidCredentialError(error)) throw error;\n log.error(\"http\", \"auth\", error);\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n request.decodedAccessToken = decoded;\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\n accessToken = await AccessTokenModel.findByToken(authorizationValue);\n\n if (!accessToken) {\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n if (accessTokenRowIsExpired(accessToken)) {\n await accessToken.destroy();\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n break;\n }\n\n const userType = decoded.userType ?? accessToken.userType;\n\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\n return { user: null, failure: \"forbidden\" };\n }\n\n const UserModel = config.key(`auth.userType.${userType}`);\n\n if (!UserModel) {\n // Configuration, not credentials. Throwing keeps a mis-registered app\n // loudly broken instead of quietly rejecting every request of this type.\n throw new Error(`User type ${userType} is unknown type.`);\n }\n\n const currentUser = await UserModel.find(decoded.id);\n\n if (!currentUser) {\n await accessToken.destroy();\n\n return fail(\"invalid\");\n }\n\n if (!(await authService.canAuthenticate(currentUser))) {\n return fail(\"unauthorized\");\n }\n\n return { user: currentUser };\n}\n\n/**\n * Resolve the request's credential to its user model, or `null` when there is\n * none (missing, invalid, expired, wrong type, or not allowed to authenticate).\n *\n * Includes the middleware's automatic renewal when `options.refreshCredential`\n * is set. Use `resolveRequestUserOutcome` when the failure reason matters.\n */\nexport function resolveRequestUser(\n request: Request,\n response: Response,\n options: ResolveRequestUserOptions = {},\n): Promise<any> {\n // Single-flight: the PROMISE is stored, so concurrent and repeat callers in\n // one request share one verify, one lookup and at most one renewal. The first\n // caller's options win for the rest of the request.\n if (!request.locals.session) {\n request.locals.session = resolveRequestUserOutcome(request, response, options).then(\n (outcome) => outcome.user,\n );\n }\n\n return request.locals.session;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resolve-request-user.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/resolve-request-user.ts"],"sourcesContent":["import { config, type Request, type Response } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport type { CookieDomainResolver, TokenFrom } from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport { authService } from \"./auth.service\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\n/**\n * Decoded access-token claims the resolver reads. The full payload carries\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\n */\ntype DecodedAccessToken = {\n id: string | number;\n userType?: string;\n};\n\nexport type ResolveRequestUserOptions = {\n /** Where the access token is read from. Defaults to `\"header\"`. */\n tokenFrom?: TokenFrom;\n /** Refresh credential source; enables one automatic renewal per call. */\n refreshCredential?: TokenFrom;\n /** Refresh overlap window forwarded to `authService.renewAutomaticSession`. */\n overlapMs?: number;\n /** User types allowed to pass. Empty means any authenticated type. */\n allowedTypes?: string[];\n /** Absolute family lifetime in ms; rotation never extends a family past it. */\n maxAgeMs?: number;\n /** The single user type a renewal mints for. Required for renewal. */\n renewalUserType?: string;\n /** Select a request-scoped Domain for renewed cookies. */\n cookieDomain?: CookieDomainResolver;\n};\n\n/**\n * Why a request did not resolve to a user. The middleware maps each reason to\n * its own response; `resolveRequestUser` collapses them all to `null`.\n */\nexport type RequestUserFailure = \"missing\" | \"invalid\" | \"forbidden\" | \"unauthorized\";\n\nexport type RequestUserOutcome<TUser = any> =\n { user: TUser; failure?: undefined } | { user: null; failure: RequestUserFailure };\n\n/**\n * Whether the persisted row says the token is dead.\n *\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\n * override that renames or reshapes its expiry column stays authoritative. A\n * row that cannot answer at all — an override that dropped the getter — is\n * treated as **expired**: a check must never quietly answer \"fine\" when it has\n * nothing to check.\n *\n * This is independent of the `exp` claim required by `jwt.verify`: that guard\n * catches a token whose *claims* carry no deadline; this one catches a token\n * whose *row* says the deadline has passed.\n */\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\n}\n\n/** Read one credential (access or refresh) from the header or a cookie. */\nexport function readCredential(request: Request, tokenFrom: TokenFrom): string {\n if (tokenFrom === \"header\") {\n return request.authorizationValue;\n }\n\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\n\n return value ? String(value) : \"\";\n}\n\n/**\n * Resolve a request's credential to a user, reporting WHY it failed.\n *\n * Side effects match what `authMiddleware` always did: `request.locals.user` is\n * cleared on every failure, `request.decodedAccessToken` is set once a token\n * verifies, and stale rows are destroyed. It does NOT set `request.locals.user`\n * on success — the caller decides that.\n *\n * At most one renewal is attempted per call. The renewed access token is\n * checked through the same path; no handler or unsafe request replay happens.\n */\nexport async function resolveRequestUserOutcome(\n request: Request,\n response: Response,\n options: ResolveRequestUserOptions = {},\n): Promise<RequestUserOutcome> {\n const tokenFrom = options.tokenFrom ?? \"header\";\n const allowedTypes = options.allowedTypes ?? [];\n const { refreshCredential } = options;\n let authorizationValue = readCredential(request, tokenFrom);\n let renewalAttempted = false;\n\n const renew = async (): Promise<boolean> => {\n if (!refreshCredential || renewalAttempted) return false;\n\n renewalAttempted = true;\n const refreshToken = readCredential(request, refreshCredential);\n if (!refreshToken) return false;\n\n const pair = await authService.renewAutomaticSession(refreshToken, options.renewalUserType!, {\n overlapMs: options.overlapMs,\n maxAgeMs: options.maxAgeMs,\n });\n if (!pair?.refreshToken) return false;\n\n authService.setAuthCookie(response, pair.accessToken, {\n name: tokenFrom.slice(\"cookie:\".length),\n domain: options.cookieDomain?.(request),\n });\n authService.setAuthCookie(response, pair.refreshToken, {\n name: refreshCredential.slice(\"cookie:\".length),\n domain: options.cookieDomain?.(request),\n });\n authorizationValue = pair.accessToken.token;\n\n return true;\n };\n\n const fail = (failure: RequestUserFailure): RequestUserOutcome => {\n request.locals.user = undefined;\n\n return { user: null, failure };\n };\n\n let decoded: DecodedAccessToken;\n let accessToken: AccessToken | null;\n\n while (true) {\n if (!authorizationValue) {\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"missing\");\n }\n\n try {\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\n } catch (error) {\n if (!isInvalidCredentialError(error)) throw error;\n log.error(\"http\", \"auth\", error);\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n request.decodedAccessToken = decoded;\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\n accessToken = await AccessTokenModel.findByToken(authorizationValue);\n\n if (!accessToken) {\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n if (accessTokenRowIsExpired(accessToken)) {\n await accessToken.destroy();\n request.locals.user = undefined;\n if (await renew()) continue;\n return fail(\"invalid\");\n }\n\n break;\n }\n\n const userType = decoded.userType ?? accessToken.userType;\n\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\n return { user: null, failure: \"forbidden\" };\n }\n\n const UserModel = config.key(`auth.userType.${userType}`);\n\n if (!UserModel) {\n // Configuration, not credentials. Throwing keeps a mis-registered app\n // loudly broken instead of quietly rejecting every request of this type.\n throw new Error(`User type ${userType} is unknown type.`);\n }\n\n const currentUser = await UserModel.find(decoded.id);\n\n if (!currentUser) {\n await accessToken.destroy();\n\n return fail(\"invalid\");\n }\n\n if (!(await authService.canAuthenticate(currentUser))) {\n return fail(\"unauthorized\");\n }\n\n return { user: currentUser };\n}\n\n/**\n * Resolve the request's credential to its user model, or `null` when there is\n * none (missing, invalid, expired, wrong type, or not allowed to authenticate).\n *\n * Includes the middleware's automatic renewal when `options.refreshCredential`\n * is set. Use `resolveRequestUserOutcome` when the failure reason matters.\n */\nexport function resolveRequestUser(\n request: Request,\n response: Response,\n options: ResolveRequestUserOptions = {},\n): Promise<any> {\n // Single-flight: the PROMISE is stored, so concurrent and repeat callers in\n // one request share one verify, one lookup and at most one renewal. The first\n // caller's options win for the rest of the request.\n if (!request.locals.session) {\n request.locals.session = resolveRequestUserOutcome(request, response, options).then(\n (outcome) => outcome.user,\n );\n }\n\n return request.locals.session;\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import "../models/auth-token-family/index.mjs";
|
|
|
3
3
|
import { databaseTransactionContext, transaction } from "@warlock.js/cascade";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../../packages/auth/src/services/token-family-operation.ts
|
|
7
7
|
const operationEffects = new AsyncLocalStorage();
|
|
8
8
|
/** Defer events until this coordinator's owned transaction commits. */
|
|
9
9
|
function afterTokenFamilyOperation(effect) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-family-operation.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"token-family-operation.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/token-family-operation.ts"],"sourcesContent":["import { databaseTransactionContext, transaction } from \"@warlock.js/cascade\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { AuthTokenFamily } from \"../models/auth-token-family\";\n\nconst operationEffects = new AsyncLocalStorage<Array<() => void>>();\n\n/** Defer events until this coordinator's owned transaction commits. */\nexport function afterTokenFamilyOperation(effect: () => void): void {\n const effects = operationEffects.getStore();\n if (effects) effects.push(effect);\n else effect();\n}\n\n/** A CAS miss is retried only when this helper owns the transaction boundary. */\nclass TokenFamilyConflictError extends Error {\n public constructor() {\n super(\"Token family changed concurrently\");\n this.name = \"TokenFamilyConflictError\";\n }\n}\n\n/** A missing or revoked family cannot become usable through a retry. */\nexport class TokenFamilyUnavailableError extends Error {\n public constructor() {\n super(\"Token family is unavailable\");\n this.name = \"TokenFamilyUnavailableError\";\n }\n}\n\ntype TransactionError = {\n code?: unknown;\n hasErrorLabel?: (label: string) => boolean;\n};\n\nconst MAX_OWNED_TRANSACTION_ATTEMPTS = 3;\n\nfunction isRetryableTransactionError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n\n const transactionError = error as Error & TransactionError;\n\n return (\n transactionError.code === \"40001\" ||\n transactionError.hasErrorLabel?.(\"TransientTransactionError\") === true\n );\n}\n\n/**\n * Perform one family state transition in a transaction and advance its durable\n * revision with a conditional update. An existing Cascade transaction is\n * joined, never retried here: PostgreSQL marks an outer transaction aborted on\n * a serialization error, so only that transaction's owner can safely retry.\n *\n * The finite internal cap prevents request handlers from retrying forever.\n * Transaction setup failures are deliberately propagated; this auth boundary\n * has no non-transactional fallback.\n */\nexport async function runTokenFamilyOperation<T>(\n familyId: string,\n operation: (family: AuthTokenFamily) => Promise<T>,\n): Promise<T> {\n const joinsExistingTransaction = databaseTransactionContext.hasActiveTransaction();\n const attempts = joinsExistingTransaction ? 1 : MAX_OWNED_TRANSACTION_ATTEMPTS;\n let lastError: unknown;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n const parentEffects = operationEffects.getStore();\n const effects: Array<() => void> = [];\n let result: T;\n try {\n result = await operationEffects.run(effects, () =>\n transaction(\n async () => {\n const family = await AuthTokenFamily.findByFamilyId(familyId);\n\n if (!family || family.isRevoked) {\n throw new TokenFamilyUnavailableError();\n }\n\n const revision = family.get<number>(\"revision\");\n const advanced = await AuthTokenFamily.advanceRevision(familyId, revision);\n\n if (advanced !== 1) {\n throw new TokenFamilyConflictError();\n }\n\n return operation(family);\n },\n { isolationLevel: \"SERIALIZABLE\" },\n ),\n );\n } catch (error) {\n lastError = error;\n\n if (\n joinsExistingTransaction ||\n (!isRetryableTransactionError(error) && !(error instanceof TokenFamilyConflictError))\n ) {\n throw error;\n }\n continue;\n }\n\n // Listener failures must never retry a transaction that already committed.\n // Nested family operations join their parent's queue. An app-owned outer\n // transaction has no Cascade commit hook, so its existing event timing is\n // retained; its owner remains responsible for external side effects.\n if (parentEffects) parentEffects.push(...effects);\n else for (const effect of effects) effect();\n return result;\n }\n\n throw lastError;\n}\n\nexport { TokenFamilyConflictError };\n"],"mappings":";;;;;;AAIA,MAAM,mBAAmB,IAAI,kBAAqC;;AAGlE,SAAgB,0BAA0B,QAA0B;CAClE,MAAM,UAAU,iBAAiB,SAAS;CAC1C,IAAI,SAAS,QAAQ,KAAK,MAAM;MAC3B,OAAO;AACd;;AAGA,IAAM,2BAAN,cAAuC,MAAM;CAC3C,AAAO,cAAc;EACnB,MAAM,mCAAmC;EACzC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,8BAAb,cAAiD,MAAM;CACrD,AAAO,cAAc;EACnB,MAAM,6BAA6B;EACnC,KAAK,OAAO;CACd;AACF;AAOA,MAAM,iCAAiC;AAEvC,SAAS,4BAA4B,OAAyB;CAC5D,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CAEtC,MAAM,mBAAmB;CAEzB,OACE,iBAAiB,SAAS,WAC1B,iBAAiB,gBAAgB,2BAA2B,MAAM;AAEtE;;;;;;;;;;;AAYA,eAAsB,wBACpB,UACA,WACY;CACZ,MAAM,2BAA2B,2BAA2B,qBAAqB;CACjF,MAAM,WAAW,2BAA2B,IAAI;CAChD,IAAI;CAEJ,KAAK,IAAI,UAAU,GAAG,UAAU,UAAU,WAAW;EACnD,MAAM,gBAAgB,iBAAiB,SAAS;EAChD,MAAM,UAA6B,CAAC;EACpC,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,iBAAiB,IAAI,eAClC,YACE,YAAY;IACV,MAAM,SAAS,MAAM,gBAAgB,eAAe,QAAQ;IAE5D,IAAI,CAAC,UAAU,OAAO,WACpB,MAAM,IAAI,4BAA4B;IAGxC,MAAM,WAAW,OAAO,IAAY,UAAU;IAG9C,IAAI,MAFmB,gBAAgB,gBAAgB,UAAU,QAAQ,MAExD,GACf,MAAM,IAAI,yBAAyB;IAGrC,OAAO,UAAU,MAAM;GACzB,GACA,EAAE,gBAAgB,eAAe,CACnC,CACF;EACF,SAAS,OAAO;GACd,YAAY;GAEZ,IACE,4BACC,CAAC,4BAA4B,KAAK,KAAK,EAAE,iBAAiB,2BAE3D,MAAM;GAER;EACF;EAMA,IAAI,eAAe,cAAc,KAAK,GAAG,OAAO;OAC3C,KAAK,MAAM,UAAU,SAAS,OAAO;EAC1C,OAAO;CACT;CAEA,MAAM;AACR"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authConfig } from "./auth-config.mjs";
|
|
2
2
|
import { NotificationsUnavailableError } from "../errors/notifications-unavailable.error.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/auth/src/services/token-notifications.ts
|
|
5
5
|
/** Non-literal on purpose: keeps type-checkers and bundlers from requiring the optional peer. */
|
|
6
6
|
const NOTIFICATIONS_PACKAGE = "@warlock.js/notifications";
|
|
7
7
|
const defaults = /* @__PURE__ */ new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-notifications.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"token-notifications.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/services/token-notifications.ts"],"sourcesContent":["import type { AuthNotification, OneTimeTokenNotificationData } from \"../contracts/types\";\nimport { NotificationsUnavailableError } from \"../errors/notifications-unavailable.error\";\nimport { authConfig } from \"./auth-config\";\n\n/** Which token notification to resolve. */\nexport type TokenNotificationType = \"email-verification\" | \"password-reset\";\n\n/**\n * The slice of `@warlock.js/notifications` auth uses. Declared locally because\n * the package is an OPTIONAL peer: auth must type-check and load without it.\n */\ntype NotificationsModule = {\n getNotificationConfig: () => unknown;\n defineNotification: (definition: {\n type: string;\n via: string[];\n mail: (data: OneTimeTokenNotificationData) => { subject: string; html: string; text: string };\n }) => AuthNotification;\n};\n\n/** Non-literal on purpose: keeps type-checkers and bundlers from requiring the optional peer. */\nconst NOTIFICATIONS_PACKAGE: string = \"@warlock.js/notifications\";\n\nconst defaults = new Map<TokenNotificationType, AuthNotification>();\n\n/**\n * Whether `error` (or an error it wraps as `cause`, as some loaders do) is the\n * resolver saying the notifications package itself is absent. A missing\n * TRANSITIVE dependency names a different package and is rethrown untouched.\n */\nfunction isPackageMissing(error: unknown): boolean {\n for (let current = error, depth = 0; current && depth < 5; depth++) {\n const { code, message } = current as { code?: unknown; message?: unknown };\n\n if (\n (code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\") &&\n typeof message === \"string\" &&\n message.includes(`'${NOTIFICATIONS_PACKAGE}'`)\n ) {\n return true;\n }\n\n current = (current as { cause?: unknown }).cause;\n }\n\n return false;\n}\n\nasync function loadNotifications(): Promise<NotificationsModule> {\n let notifications: NotificationsModule;\n\n try {\n notifications = (await import(NOTIFICATIONS_PACKAGE)) as NotificationsModule;\n } catch (error) {\n if (isPackageMissing(error)) {\n throw new NotificationsUnavailableError(\"not-installed\", error);\n }\n\n throw error;\n }\n\n try {\n notifications.getNotificationConfig();\n } catch (error) {\n throw new NotificationsUnavailableError(\"not-configured\", error);\n }\n\n return notifications;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction mailBody(intro: string, data: OneTimeTokenNotificationData) {\n const action = data.url ?? data.token;\n const expires = data.expiresAt.toISOString();\n\n return {\n html:\n `<p>${intro}</p>` +\n (data.url\n ? `<p><a href=\"${escapeHtml(data.url)}\">${escapeHtml(data.url)}</a></p>`\n : `<p><code>${escapeHtml(data.token)}</code></p>`) +\n `<p>This link expires at ${expires}. If you did not request it, ignore this email.</p>`,\n text: `${intro}\\n\\n${action}\\n\\nThis link expires at ${expires}. If you did not request it, ignore this email.`,\n };\n}\n\nfunction defineDefault(\n notifications: NotificationsModule,\n type: TokenNotificationType,\n): AuthNotification {\n if (type === \"email-verification\") {\n return notifications.defineNotification({\n type: \"auth.email-verification\",\n via: [\"mail\"],\n mail: (data) => ({\n subject: \"Verify your email address\",\n ...mailBody(\"Confirm your email address to finish setting up your account.\", data),\n }),\n });\n }\n\n return notifications.defineNotification({\n type: \"auth.password-reset\",\n via: [\"mail\"],\n mail: (data) => ({\n subject: \"Reset your password\",\n ...mailBody(\"Use this to choose a new password.\", data),\n }),\n });\n}\n\n/**\n * Resolve the notification a token of `type` is delivered through — the app's\n * configured override, else auth's default mail notification.\n *\n * Always confirms `@warlock.js/notifications` is installed AND configured\n * first, even when an override is set, so an app can never issue a token that\n * silently goes nowhere. Throws {@link NotificationsUnavailableError}.\n */\nexport async function resolveTokenNotification(\n type: TokenNotificationType,\n): Promise<AuthNotification> {\n const notifications = await loadNotifications();\n\n const override =\n type === \"email-verification\"\n ? authConfig.verification.notification()\n : authConfig.passwordReset.notification();\n\n if (override) return override;\n\n let notification = defaults.get(type);\n\n if (!notification) {\n notification = defineDefault(notifications, type);\n defaults.set(type, notification);\n }\n\n return notification;\n}\n"],"mappings":";;;;;AAqBA,MAAM,wBAAgC;AAEtC,MAAM,2BAAW,IAAI,IAA6C;;;;;;AAOlE,SAAS,iBAAiB,OAAyB;CACjD,KAAK,IAAI,UAAU,OAAO,QAAQ,GAAG,WAAW,QAAQ,GAAG,SAAS;EAClE,MAAM,EAAE,MAAM,YAAY;EAE1B,KACG,SAAS,0BAA0B,SAAS,uBAC7C,OAAO,YAAY,YACnB,QAAQ,SAAS,IAAI,sBAAsB,EAAE,GAE7C,OAAO;EAGT,UAAW,QAAgC;CAC7C;CAEA,OAAO;AACT;AAEA,eAAe,oBAAkD;CAC/D,IAAI;CAEJ,IAAI;EACF,gBAAiB,MAAM,OAAO;CAChC,SAAS,OAAO;EACd,IAAI,iBAAiB,KAAK,GACxB,MAAM,IAAI,8BAA8B,iBAAiB,KAAK;EAGhE,MAAM;CACR;CAEA,IAAI;EACF,cAAc,sBAAsB;CACtC,SAAS,OAAO;EACd,MAAM,IAAI,8BAA8B,kBAAkB,KAAK;CACjE;CAEA,OAAO;AACT;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,MACJ,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,SAAS,SAAS,OAAe,MAAoC;CACnE,MAAM,SAAS,KAAK,OAAO,KAAK;CAChC,MAAM,UAAU,KAAK,UAAU,YAAY;CAE3C,OAAO;EACL,MACE,MAAM,MAAM,SACX,KAAK,MACF,eAAe,WAAW,KAAK,GAAG,EAAE,IAAI,WAAW,KAAK,GAAG,EAAE,YAC7D,YAAY,WAAW,KAAK,KAAK,EAAE,gBACvC,2BAA2B,QAAQ;EACrC,MAAM,GAAG,MAAM,MAAM,OAAO,2BAA2B,QAAQ;CACjE;AACF;AAEA,SAAS,cACP,eACA,MACkB;CAClB,IAAI,SAAS,sBACX,OAAO,cAAc,mBAAmB;EACtC,MAAM;EACN,KAAK,CAAC,MAAM;EACZ,OAAO,UAAU;GACf,SAAS;GACT,GAAG,SAAS,iEAAiE,IAAI;EACnF;CACF,CAAC;CAGH,OAAO,cAAc,mBAAmB;EACtC,MAAM;EACN,KAAK,CAAC,MAAM;EACZ,OAAO,UAAU;GACf,SAAS;GACT,GAAG,SAAS,sCAAsC,IAAI;EACxD;CACF,CAAC;AACH;;;;;;;;;AAUA,eAAsB,yBACpB,MAC2B;CAC3B,MAAM,gBAAgB,MAAM,kBAAkB;CAE9C,MAAM,WACJ,SAAS,uBACL,WAAW,aAAa,aAAa,IACrC,WAAW,cAAc,aAAa;CAE5C,IAAI,UAAU,OAAO;CAErB,IAAI,eAAe,SAAS,IAAI,IAAI;CAEpC,IAAI,CAAC,cAAc;EACjB,eAAe,cAAc,eAAe,IAAI;EAChD,SAAS,IAAI,MAAM,YAAY;CACjC;CAEA,OAAO;AACT"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CookieDomainResolver } from "../contracts/types.mjs";
|
|
2
2
|
import { Middleware, Request, Response } from "@warlock.js/core";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/auth/src/session/page-session.d.ts
|
|
5
5
|
/** Credential sources a session can be read from. */
|
|
6
6
|
type SessionSource = "cookie" | "header";
|
|
7
7
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-session.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"page-session.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/session/page-session.ts"],"mappings":";;;;;KAcY,aAAA;AAAZ;;;;AAAyB;AAAzB,KAUY,mBAAA;EACV,OAAA,CAAQ,OAAA,EAAS,OAAA,EAAS,QAAA,EAAU,QAAA,GAAW,OAAA;IAAU,KAAA,EAAO,MAAA;IAAQ,IAAA,EAAM,KAAA;EAAA;AAAA;AAAA,KAGpE,sBAAA;EAHqC,oDAK/C,MAAA,WALsD;EAOtD,aAAA,WARgD;EAUhD,OAAA,GAAU,aAAA,IATO;EAWjB,KAAA,YAXoC;EAapC,SAAA,sBAb+C;EAe/C,MAAA,oBAfgE;EAiBhE,SAAA,WAjB8E;EAmB9E,YAAA,GAAe,oBAAoB;AAAA;AAAA,KAGzB,kBAAA,kBAAoC,sBAAA;EAnBd,oFAqBhC,OAAA,GAAU,KAAA,EAAO,MAAA,KAAW,KAAA,GAAQ,OAAA,CAAQ,KAAA;AAAA;AAAA,KAGlC,wBAAA,GAA2B,sBAAsB;EApB3D,uEAsBA,QAAQ;AAAA;;;;;;;;iBAgFM,WAAA,gCACd,OAAA,EAAS,kBAAA,CAAmB,MAAA,EAAQ,KAAA,IACnC,mBAAA,CAAoB,MAAA,EAAQ,KAAA;AAzF/B;;;;;;AAAA,iBA4HgB,iBAAA,CAAkB,OAAA,GAAS,wBAAA,GAAgC,UAAU"}
|
|
@@ -6,7 +6,7 @@ import { config, t } from "@warlock.js/core";
|
|
|
6
6
|
import { log } from "@warlock.js/logger";
|
|
7
7
|
import ms from "ms";
|
|
8
8
|
|
|
9
|
-
//#region
|
|
9
|
+
//#region ../../packages/auth/src/session/page-session.ts
|
|
10
10
|
/** Absolute family lifetime used when `session.maxAge` is not configured. */
|
|
11
11
|
const DEFAULT_SESSION_MAX_AGE = "30d";
|
|
12
12
|
function resolveMaxAgeMs(maxAge) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-session.mjs","names":[],"sources":["../../../../../../../auth/src/session/page-session.ts"],"sourcesContent":["import { config, t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\nimport type { CookieDomainResolver } from \"../contracts/types\";\nimport { log } from \"@warlock.js/logger\";\nimport ms from \"ms\";\nimport {\n assertCsrfOriginAllowed,\n CsrfOriginMismatchError,\n requiresCsrfOriginCheck,\n} from \"../middleware/csrf-origin-check\";\nimport { authConfig } from \"../services/auth-config\";\nimport { resolveRequestUser } from \"../services/resolve-request-user\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/** Credential sources a session can be read from. */\nexport type SessionSource = \"cookie\" | \"header\";\n\n/** Absolute family lifetime used when `session.maxAge` is not configured. */\nconst DEFAULT_SESSION_MAX_AGE = \"30d\";\n\n/**\n * The seam web consumes. Declared here, not imported: web and auth never\n * depend on each other, and `pageSession()` satisfies web's `SessionResolver`\n * structurally.\n */\nexport type PageSessionResolver<TModel = unknown, TUser = unknown> = {\n resolve(request: Request, response: Response): Promise<{ model: TModel; user: TUser } | null>;\n};\n\nexport type SessionResolverOptions = {\n /** Access cookie name. @default auth.cookie.name */\n cookie?: string;\n /** Refresh cookie name. @default auth.cookie.refreshName */\n refreshCookie?: string;\n /** Credential sources. A present Authorization header always wins. @default [\"cookie\", \"header\"] */\n sources?: SessionSource[];\n /** Renew an expired access cookie from the refresh cookie. Cookie source only. @default true */\n renew?: boolean;\n /** Allowed user types. @default auth.defaultUserType */\n userTypes?: string | string[];\n /** Absolute family lifetime, an `ms` string or milliseconds. @default auth.session.maxAge, else \"30d\" */\n maxAge?: string | number;\n /** Refresh overlap window forwarded to renewal. */\n overlapMs?: number;\n /** Select a request-scoped Domain for renewed cookies; `undefined` keeps them host-only. */\n cookieDomain?: CookieDomainResolver;\n};\n\nexport type PageSessionOptions<TModel, TUser> = SessionResolverOptions & {\n /** Model → wire projection. Required: a model never crosses the wire by default. */\n project: (model: TModel) => TUser | Promise<TUser>;\n};\n\nexport type SessionMiddlewareOptions = SessionResolverOptions & {\n /** Let guests through with no user instead of a 401. @default false */\n optional?: boolean;\n};\n\nfunction resolveMaxAgeMs(maxAge: string | number | undefined): number {\n const raw = maxAge ?? config.key(\"auth.session.maxAge\", DEFAULT_SESSION_MAX_AGE);\n const parsed = typeof raw === \"number\" ? raw : ms(raw as ms.StringValue);\n\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\n throw new Error(\n `session.maxAge: ${JSON.stringify(raw)} is not a valid ms duration — use a positive duration such as \"30d\".`,\n );\n }\n\n return parsed;\n}\n\nfunction resolveUserTypes(value: string | string[] | undefined): string[] {\n if (value !== undefined) return Array.isArray(value) ? value : [value];\n\n const configured = config.key(\"auth.defaultUserType\");\n\n return configured ? [configured as string] : [];\n}\n\n/**\n * The shared \"credential → model\" step behind `pageSession` and\n * `sessionMiddleware`. Resolved once per request through\n * `resolveRequestUser`'s single-flight memo.\n */\nfunction createModelResolver(options: SessionResolverOptions) {\n const sources = options.sources?.length ? options.sources : ([\"cookie\", \"header\"] as const);\n const useHeader = sources.includes(\"header\");\n const useCookie = sources.includes(\"cookie\");\n const renew = options.renew ?? true;\n\n const resolveModel = async (\n request: Request,\n response: Response,\n ): Promise<{ model: unknown; source: \"header\" | \"cookie\" } | null> => {\n const accessName = options.cookie ?? authConfig.cookie.name();\n const refreshName = options.refreshCookie ?? authConfig.cookie.refreshName();\n // A present Authorization header wins and never falls back to the cookie.\n const headerWins = useHeader && Boolean(request.authorizationValue);\n\n if (!headerWins && !useCookie) return null;\n\n // Cheap guest path: nothing to decode, so nothing is marked auth-derived.\n if (!headerWins && !request.cookie(accessName) && !request.cookie(refreshName)) return null;\n\n const allowedTypes = resolveUserTypes(options.userTypes);\n const renewalUserType = allowedTypes.length === 1 ? allowedTypes[0] : undefined;\n const canRenew = !headerWins && renew && Boolean(renewalUserType);\n\n const model = await resolveRequestUser(request, response, {\n tokenFrom: headerWins ? \"header\" : `cookie:${accessName}`,\n refreshCredential: canRenew ? `cookie:${refreshName}` : undefined,\n overlapMs: options.overlapMs,\n maxAgeMs: resolveMaxAgeMs(options.maxAge),\n allowedTypes,\n renewalUserType,\n cookieDomain: options.cookieDomain ?? authConfig.cookie.cookieDomain(),\n });\n\n if (!model) return null;\n\n request.locals.user = model;\n\n return { model, source: headerWins ? \"header\" : \"cookie\" };\n };\n\n return resolveModel;\n}\n\n/**\n * Session resolver for pages: cookie-or-header credential, reactive renewal,\n * one resolution per request. Plug it into `web.session`.\n *\n * @example\n * export default { session: pageSession({ project: (user: User) => userResource(user) }) };\n */\nexport function pageSession<TModel = any, TUser = unknown>(\n options: PageSessionOptions<TModel, TUser>,\n): PageSessionResolver<TModel, TUser> {\n if (typeof options?.project !== \"function\") {\n throw new Error(\n \"pageSession requires a `project(model)` function: a model never crosses the wire by default.\",\n );\n }\n\n const resolveModel = createModelResolver(options);\n const projections = new WeakMap<object, Promise<unknown>>();\n\n return {\n async resolve(request, response) {\n const resolved = await resolveModel(request, response);\n\n if (!resolved) return null;\n\n const model = resolved.model as TModel;\n let projected = projections.get(request as object);\n\n if (!projected) {\n projected = Promise.resolve(options.project(model));\n projections.set(request as object, projected);\n }\n\n return { model, user: (await projected) as TUser };\n },\n };\n}\n\n/**\n * Middleware for cookie API routes over the same resolver as `pageSession`.\n * A cookie-authenticated unsafe request must pass the Origin/Referer check\n * (renewal can mint cookies, so it runs even when the access cookie is gone).\n * Guests get a 401 unless `optional`. The user lands on `request.locals.user`.\n */\nexport function sessionMiddleware(options: SessionMiddlewareOptions = {}): Middleware {\n const resolveModel = createModelResolver(options);\n const sources = options.sources?.length ? options.sources : [\"cookie\", \"header\"];\n\n return async ({ request, response }) => {\n const headerWins = sources.includes(\"header\") && Boolean(request.authorizationValue);\n\n if (!headerWins && requiresCsrfOriginCheck(\"cookie:x\", request.method)) {\n const accessName = options.cookie ?? authConfig.cookie.name();\n const refreshName = options.refreshCookie ?? authConfig.cookie.refreshName();\n\n if (request.cookie(accessName) || request.cookie(refreshName)) {\n try {\n assertCsrfOriginAllowed(request);\n } catch (error) {\n if (!(error instanceof CsrfOriginMismatchError)) throw error;\n\n log.error(\"http\", \"auth\", error);\n\n return response.forbidden({\n error: t(\"auth.errors.csrfOriginMismatch\"),\n errorCode: AuthErrorCodes.CsrfOriginMismatch,\n });\n }\n }\n }\n\n const resolved = await resolveModel(request, response);\n\n if (resolved || options.optional) return;\n\n return response.unauthorized({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"page-session.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/session/page-session.ts"],"sourcesContent":["import { config, t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\nimport type { CookieDomainResolver } from \"../contracts/types\";\nimport { log } from \"@warlock.js/logger\";\nimport ms from \"ms\";\nimport {\n assertCsrfOriginAllowed,\n CsrfOriginMismatchError,\n requiresCsrfOriginCheck,\n} from \"../middleware/csrf-origin-check\";\nimport { authConfig } from \"../services/auth-config\";\nimport { resolveRequestUser } from \"../services/resolve-request-user\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/** Credential sources a session can be read from. */\nexport type SessionSource = \"cookie\" | \"header\";\n\n/** Absolute family lifetime used when `session.maxAge` is not configured. */\nconst DEFAULT_SESSION_MAX_AGE = \"30d\";\n\n/**\n * The seam web consumes. Declared here, not imported: web and auth never\n * depend on each other, and `pageSession()` satisfies web's `SessionResolver`\n * structurally.\n */\nexport type PageSessionResolver<TModel = unknown, TUser = unknown> = {\n resolve(request: Request, response: Response): Promise<{ model: TModel; user: TUser } | null>;\n};\n\nexport type SessionResolverOptions = {\n /** Access cookie name. @default auth.cookie.name */\n cookie?: string;\n /** Refresh cookie name. @default auth.cookie.refreshName */\n refreshCookie?: string;\n /** Credential sources. A present Authorization header always wins. @default [\"cookie\", \"header\"] */\n sources?: SessionSource[];\n /** Renew an expired access cookie from the refresh cookie. Cookie source only. @default true */\n renew?: boolean;\n /** Allowed user types. @default auth.defaultUserType */\n userTypes?: string | string[];\n /** Absolute family lifetime, an `ms` string or milliseconds. @default auth.session.maxAge, else \"30d\" */\n maxAge?: string | number;\n /** Refresh overlap window forwarded to renewal. */\n overlapMs?: number;\n /** Select a request-scoped Domain for renewed cookies; `undefined` keeps them host-only. */\n cookieDomain?: CookieDomainResolver;\n};\n\nexport type PageSessionOptions<TModel, TUser> = SessionResolverOptions & {\n /** Model → wire projection. Required: a model never crosses the wire by default. */\n project: (model: TModel) => TUser | Promise<TUser>;\n};\n\nexport type SessionMiddlewareOptions = SessionResolverOptions & {\n /** Let guests through with no user instead of a 401. @default false */\n optional?: boolean;\n};\n\nfunction resolveMaxAgeMs(maxAge: string | number | undefined): number {\n const raw = maxAge ?? config.key(\"auth.session.maxAge\", DEFAULT_SESSION_MAX_AGE);\n const parsed = typeof raw === \"number\" ? raw : ms(raw as ms.StringValue);\n\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\n throw new Error(\n `session.maxAge: ${JSON.stringify(raw)} is not a valid ms duration — use a positive duration such as \"30d\".`,\n );\n }\n\n return parsed;\n}\n\nfunction resolveUserTypes(value: string | string[] | undefined): string[] {\n if (value !== undefined) return Array.isArray(value) ? value : [value];\n\n const configured = config.key(\"auth.defaultUserType\");\n\n return configured ? [configured as string] : [];\n}\n\n/**\n * The shared \"credential → model\" step behind `pageSession` and\n * `sessionMiddleware`. Resolved once per request through\n * `resolveRequestUser`'s single-flight memo.\n */\nfunction createModelResolver(options: SessionResolverOptions) {\n const sources = options.sources?.length ? options.sources : ([\"cookie\", \"header\"] as const);\n const useHeader = sources.includes(\"header\");\n const useCookie = sources.includes(\"cookie\");\n const renew = options.renew ?? true;\n\n const resolveModel = async (\n request: Request,\n response: Response,\n ): Promise<{ model: unknown; source: \"header\" | \"cookie\" } | null> => {\n const accessName = options.cookie ?? authConfig.cookie.name();\n const refreshName = options.refreshCookie ?? authConfig.cookie.refreshName();\n // A present Authorization header wins and never falls back to the cookie.\n const headerWins = useHeader && Boolean(request.authorizationValue);\n\n if (!headerWins && !useCookie) return null;\n\n // Cheap guest path: nothing to decode, so nothing is marked auth-derived.\n if (!headerWins && !request.cookie(accessName) && !request.cookie(refreshName)) return null;\n\n const allowedTypes = resolveUserTypes(options.userTypes);\n const renewalUserType = allowedTypes.length === 1 ? allowedTypes[0] : undefined;\n const canRenew = !headerWins && renew && Boolean(renewalUserType);\n\n const model = await resolveRequestUser(request, response, {\n tokenFrom: headerWins ? \"header\" : `cookie:${accessName}`,\n refreshCredential: canRenew ? `cookie:${refreshName}` : undefined,\n overlapMs: options.overlapMs,\n maxAgeMs: resolveMaxAgeMs(options.maxAge),\n allowedTypes,\n renewalUserType,\n cookieDomain: options.cookieDomain ?? authConfig.cookie.cookieDomain(),\n });\n\n if (!model) return null;\n\n request.locals.user = model;\n\n return { model, source: headerWins ? \"header\" : \"cookie\" };\n };\n\n return resolveModel;\n}\n\n/**\n * Session resolver for pages: cookie-or-header credential, reactive renewal,\n * one resolution per request. Plug it into `web.session`.\n *\n * @example\n * export default { session: pageSession({ project: (user: User) => userResource(user) }) };\n */\nexport function pageSession<TModel = any, TUser = unknown>(\n options: PageSessionOptions<TModel, TUser>,\n): PageSessionResolver<TModel, TUser> {\n if (typeof options?.project !== \"function\") {\n throw new Error(\n \"pageSession requires a `project(model)` function: a model never crosses the wire by default.\",\n );\n }\n\n const resolveModel = createModelResolver(options);\n const projections = new WeakMap<object, Promise<unknown>>();\n\n return {\n async resolve(request, response) {\n const resolved = await resolveModel(request, response);\n\n if (!resolved) return null;\n\n const model = resolved.model as TModel;\n let projected = projections.get(request as object);\n\n if (!projected) {\n projected = Promise.resolve(options.project(model));\n projections.set(request as object, projected);\n }\n\n return { model, user: (await projected) as TUser };\n },\n };\n}\n\n/**\n * Middleware for cookie API routes over the same resolver as `pageSession`.\n * A cookie-authenticated unsafe request must pass the Origin/Referer check\n * (renewal can mint cookies, so it runs even when the access cookie is gone).\n * Guests get a 401 unless `optional`. The user lands on `request.locals.user`.\n */\nexport function sessionMiddleware(options: SessionMiddlewareOptions = {}): Middleware {\n const resolveModel = createModelResolver(options);\n const sources = options.sources?.length ? options.sources : [\"cookie\", \"header\"];\n\n return async ({ request, response }) => {\n const headerWins = sources.includes(\"header\") && Boolean(request.authorizationValue);\n\n if (!headerWins && requiresCsrfOriginCheck(\"cookie:x\", request.method)) {\n const accessName = options.cookie ?? authConfig.cookie.name();\n const refreshName = options.refreshCookie ?? authConfig.cookie.refreshName();\n\n if (request.cookie(accessName) || request.cookie(refreshName)) {\n try {\n assertCsrfOriginAllowed(request);\n } catch (error) {\n if (!(error instanceof CsrfOriginMismatchError)) throw error;\n\n log.error(\"http\", \"auth\", error);\n\n return response.forbidden({\n error: t(\"auth.errors.csrfOriginMismatch\"),\n errorCode: AuthErrorCodes.CsrfOriginMismatch,\n });\n }\n }\n }\n\n const resolved = await resolveModel(request, response);\n\n if (resolved || options.optional) return;\n\n return response.unauthorized({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n };\n}\n"],"mappings":";;;;;;;;;;AAiBA,MAAM,0BAA0B;AAwChC,SAAS,gBAAgB,QAA6C;CACpE,MAAM,MAAM,UAAU,OAAO,IAAI,uBAAuB,uBAAuB;CAC/E,MAAM,SAAS,OAAO,QAAQ,WAAW,MAAM,GAAG,GAAqB;CAEvE,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,mBAAmB,KAAK,UAAU,GAAG,EAAE,qEACzC;CAGF,OAAO;AACT;AAEA,SAAS,iBAAiB,OAAgD;CACxE,IAAI,UAAU,QAAW,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;CAErE,MAAM,aAAa,OAAO,IAAI,sBAAsB;CAEpD,OAAO,aAAa,CAAC,UAAoB,IAAI,CAAC;AAChD;;;;;;AAOA,SAAS,oBAAoB,SAAiC;CAC5D,MAAM,UAAU,QAAQ,SAAS,SAAS,QAAQ,UAAW,CAAC,UAAU,QAAQ;CAChF,MAAM,YAAY,QAAQ,SAAS,QAAQ;CAC3C,MAAM,YAAY,QAAQ,SAAS,QAAQ;CAC3C,MAAM,QAAQ,QAAQ,SAAS;CAE/B,MAAM,eAAe,OACnB,SACA,aACoE;EACpE,MAAM,aAAa,QAAQ,UAAU,WAAW,OAAO,KAAK;EAC5D,MAAM,cAAc,QAAQ,iBAAiB,WAAW,OAAO,YAAY;EAE3E,MAAM,aAAa,aAAa,QAAQ,QAAQ,kBAAkB;EAElE,IAAI,CAAC,cAAc,CAAC,WAAW,OAAO;EAGtC,IAAI,CAAC,cAAc,CAAC,QAAQ,OAAO,UAAU,KAAK,CAAC,QAAQ,OAAO,WAAW,GAAG,OAAO;EAEvF,MAAM,eAAe,iBAAiB,QAAQ,SAAS;EACvD,MAAM,kBAAkB,aAAa,WAAW,IAAI,aAAa,KAAK;EACtE,MAAM,WAAW,CAAC,cAAc,SAAS,QAAQ,eAAe;EAEhE,MAAM,QAAQ,MAAM,mBAAmB,SAAS,UAAU;GACxD,WAAW,aAAa,WAAW,UAAU;GAC7C,mBAAmB,WAAW,UAAU,gBAAgB;GACxD,WAAW,QAAQ;GACnB,UAAU,gBAAgB,QAAQ,MAAM;GACxC;GACA;GACA,cAAc,QAAQ,gBAAgB,WAAW,OAAO,aAAa;EACvE,CAAC;EAED,IAAI,CAAC,OAAO,OAAO;EAEnB,QAAQ,OAAO,OAAO;EAEtB,OAAO;GAAE;GAAO,QAAQ,aAAa,WAAW;EAAS;CAC3D;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgB,YACd,SACoC;CACpC,IAAI,OAAO,SAAS,YAAY,YAC9B,MAAM,IAAI,MACR,8FACF;CAGF,MAAM,eAAe,oBAAoB,OAAO;CAChD,MAAM,8BAAc,IAAI,QAAkC;CAE1D,OAAO,EACL,MAAM,QAAQ,SAAS,UAAU;EAC/B,MAAM,WAAW,MAAM,aAAa,SAAS,QAAQ;EAErD,IAAI,CAAC,UAAU,OAAO;EAEtB,MAAM,QAAQ,SAAS;EACvB,IAAI,YAAY,YAAY,IAAI,OAAiB;EAEjD,IAAI,CAAC,WAAW;GACd,YAAY,QAAQ,QAAQ,QAAQ,QAAQ,KAAK,CAAC;GAClD,YAAY,IAAI,SAAmB,SAAS;EAC9C;EAEA,OAAO;GAAE;GAAO,MAAO,MAAM;EAAoB;CACnD,EACF;AACF;;;;;;;AAQA,SAAgB,kBAAkB,UAAoC,CAAC,GAAe;CACpF,MAAM,eAAe,oBAAoB,OAAO;CAChD,MAAM,UAAU,QAAQ,SAAS,SAAS,QAAQ,UAAU,CAAC,UAAU,QAAQ;CAE/E,OAAO,OAAO,EAAE,SAAS,eAAe;EAGtC,IAAI,EAFe,QAAQ,SAAS,QAAQ,KAAK,QAAQ,QAAQ,kBAAkB,MAEhE,wBAAwB,YAAY,QAAQ,MAAM,GAAG;GACtE,MAAM,aAAa,QAAQ,UAAU,WAAW,OAAO,KAAK;GAC5D,MAAM,cAAc,QAAQ,iBAAiB,WAAW,OAAO,YAAY;GAE3E,IAAI,QAAQ,OAAO,UAAU,KAAK,QAAQ,OAAO,WAAW,GAC1D,IAAI;IACF,wBAAwB,OAAO;GACjC,SAAS,OAAO;IACd,IAAI,EAAE,iBAAiB,0BAA0B,MAAM;IAEvD,IAAI,MAAM,QAAQ,QAAQ,KAAK;IAE/B,OAAO,SAAS,UAAU;KACxB,OAAO,EAAE,gCAAgC;KACzC;IACF,CAAC;GACH;EAEJ;EAIA,IAAI,MAFmB,aAAa,SAAS,QAAQ,KAErC,QAAQ,UAAU;EAElC,OAAO,SAAS,aAAa;GAC3B,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-error-codes.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-error-codes.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/utils/auth-error-codes.ts"],"mappings":";aAAY,cAAA;EAAA;;;;EAKV,kBAAA;EAAA;EAKA;;;;EAAA,kBAAA;EAAA;EAkCA;;;;EA7BA,YAAA;EAAA;EAwDc;AAAA;;;;EAlDd,eAAA;EAAA;;;;;;;;;EASA,gBAAA;EAAA;;;;;;;;EAQA,kBAAA;EAAA;;;;;;EAMA,gBAAA;EAAA;;;;;;;EAOA,mBAAA;EAAA;;;;;;;EAOA,uBAAA;EAAA;;;;;;EAMA,wBAAA;EAAA;;;;;;;EAOA,cAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-error-codes.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/utils/auth-error-codes.ts"],"sourcesContent":["export enum AuthErrorCodes {\n /**\n * Missing Access Token Error Code EC001\n * EC001 = Missing Access Token\n */\n MissingAccessToken = \"EC001\", // Error Code 001\n /**\n * Invalid Access Token Error Code EC002\n * EC002 = Invalid Access Token\n */\n InvalidAccessToken = \"EC002\", // Error Code 002\n /**\n * Unauthorized Error Code EC003\n * EC003 = Unauthorized\n */\n Unauthorized = \"EC003\", // Error Code 003\n /**\n * Too Many Attempts Error Code EC004\n * EC004 = Too Many Attempts — issued by the login-throttle middleware once\n * a per-account or per-IP failure counter trips its threshold.\n */\n TooManyAttempts = \"EC004\", // Error Code 004\n /**\n * Invalid Token Type Error Code EC005\n * EC005 = Invalid Token Type — the token's `tokenType` claim does not match\n * what the caller expects (e.g. a refresh token presented where an access\n * token is required, or vice versa). Carried on the `code` property of the\n * `Error` thrown by `assertTokenType` (`services/jwt.ts`) so callers can\n * classify it as a credential failure without matching on the message.\n */\n InvalidTokenType = \"EC005\", // Error Code 005\n /**\n * CSRF Origin Mismatch Error Code EC006\n * EC006 = a cookie-authenticated, unsafe-method (POST/PUT/PATCH/DELETE)\n * request whose `Origin` (or, absent that, `Referer`) did not match the\n * request's own origin or an entry in `auth.csrf.allowedOrigins`, or whose\n * request carried neither header at all.\n */\n CsrfOriginMismatch = \"EC006\", // Error Code 006\n /**\n * Email Not Verified Error Code EC007\n * EC007 = the authenticated user has not verified their email address —\n * thrown by `requireVerifiedEmail()` as `EmailNotVerifiedError` (403).\n */\n EmailNotVerified = \"EC007\", // Error Code 007\n /**\n * Invalid One-Time Token Error Code EC008\n * EC008 = an email-verification or password-reset token that is unknown,\n * issued for the other purpose, expired, or already used. One code for all\n * four on purpose: the caller learns nothing about which it was.\n */\n InvalidOneTimeToken = \"EC008\", // Error Code 008\n /**\n * Invalid Provider Callback Error Code EC009\n * EC009 = a redirect-provider callback was rejected: missing, forged or\n * expired state cookie, `state` mismatch, failed code exchange, or an\n * id_token with a bad signature, issuer, audience, expiry or nonce.\n */\n InvalidProviderCallback = \"EC009\", // Error Code 009\n /**\n * Provider Email Not Verified Error Code EC010\n * EC010 = the provider did not verify the email, so auth refused to link or\n * create an account from it.\n */\n ProviderEmailNotVerified = \"EC010\", // Error Code 010\n /**\n * Invalid Passkey Error Code EC011\n * EC011 = a passkey ceremony was rejected: unknown, expired or used\n * challenge, unknown credential, failed verification, or a signature counter\n * that did not advance.\n */\n InvalidPasskey = \"EC011\", // Error Code 011\n}\n"],"mappings":";AAAA,IAAY,iBAAL;;;;;CAKL;;;;;CAKA;;;;;CAKA;;;;;;CAMA;;;;;;;;;CASA;;;;;;;;CAQA;;;;;;CAMA;;;;;;;CAOA;;;;;;;CAOA;;;;;;CAMA;;;;;;;CAOA;;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-equal.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"safe-equal.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/utils/safe-equal.ts"],"mappings":";;iBAGgB,SAAA,CAAU,IAAA,UAAc,KAAa"}
|
package/esm/utils/safe-equal.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/auth/src/utils/safe-equal.ts
|
|
4
4
|
/** Constant-time string equality; strings of unequal length are simply unequal. */
|
|
5
5
|
function safeEqual(left, right) {
|
|
6
6
|
const a = Buffer.from(left);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-equal.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"safe-equal.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/utils/safe-equal.ts"],"sourcesContent":["import { timingSafeEqual } from \"node:crypto\";\n\n/** Constant-time string equality; strings of unequal length are simply unequal. */\nexport function safeEqual(left: string, right: string): boolean {\n const a = Buffer.from(left);\n const b = Buffer.from(right);\n\n return a.length === b.length && timingSafeEqual(a, b);\n}\n"],"mappings":";;;;AAGA,SAAgB,UAAU,MAAc,OAAwB;CAC9D,MAAM,IAAI,OAAO,KAAK,IAAI;CAC1B,MAAM,IAAI,OAAO,KAAK,KAAK;CAE3B,OAAO,EAAE,WAAW,EAAE,UAAU,gBAAgB,GAAG,CAAC;AACtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-expiry.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"token-expiry.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/utils/token-expiry.ts"],"mappings":";;AAqBA;;;;AAA6C;AAe7C;;;;iBAfgB,cAAA,CAAe,KAAc;AAoC7C;;;;AAAkE;;;;;AAAlE,iBArBgB,gBAAA,CAAiB,KAAc;;;;;;;;;;iBAqB/B,eAAA,CAAgB,KAAA,WAAgB,SAAkB"}
|