najm-auth 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ export const products = sqliteTable('products', {
45
45
 
46
46
  // Combined schema (always include authSchema)
47
47
  export const schema = {
48
- ...authSchema, // users, roles, permissions, tokens, rolePermissions
48
+ ...authSchema, // includes users, tokens, and credentialSetupSessions
49
49
  products,
50
50
  };
51
51
 
@@ -727,7 +727,46 @@ async resetPassword(token: string, newPassword: string) {
727
727
  }
728
728
  ```
729
729
 
730
- ### Session Management
730
+ ### Purpose-Bound Credential Setup
731
+
732
+ Use `CredentialSetupService` when valid credentials should open only a
733
+ short-lived setup flow, not a complete application session. The default auth
734
+ schema includes the durable `credential_setup_sessions` table for PostgreSQL
735
+ and SQLite; generate and apply a consumer migration after upgrading.
736
+
737
+ ```typescript
738
+ import { AuthService, CredentialSetupService } from 'najm-auth';
739
+
740
+ const options = {
741
+ purpose: 'password-setup',
742
+ cookieName: 'my-app.password-setup',
743
+ ttlMs: 10 * 60 * 1000,
744
+ };
745
+
746
+ // Verify the password without minting access/refresh tokens.
747
+ const user = await authService.verifyCredentials({ identifier, password });
748
+
749
+ if (await appRequiresPasswordSetup(user.id)) {
750
+ // Revokes normal sessions and writes only an HttpOnly, SameSite=Strict,
751
+ // browser-session cookie. The database stores only its SHA-256 hash.
752
+ return credentialSetup.begin(user.id, options);
753
+ }
754
+
755
+ return authService.establishSession(user);
756
+
757
+ // Complete an app-owned mutation in the same transaction as one-time
758
+ // consumption. If the callback fails, token consumption rolls back.
759
+ await credentialSetup.consume(options, async ({ userId }) => {
760
+ await replaceApplicationCredential(userId, newCredential);
761
+ });
762
+ ```
763
+
764
+ Setup tokens are bound to a server-owned purpose, expire automatically, are
765
+ replaced when the same user starts that purpose again, and can be cancelled or
766
+ consumed exactly once. `require()` validates the current setup cookie without
767
+ consuming it; `cancel()` revokes it and clears the cookie.
768
+
769
+ ### Session Management
731
770
 
732
771
  - Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
733
772
  - A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
package/dist/index.d.ts CHANGED
@@ -6,11 +6,12 @@ import { EmailPluginConfig, EmailService } from 'najm-email';
6
6
  import { I18nService } from 'najm-i18n';
7
7
  import { TDb, SeedEntry } from 'najm-database';
8
8
  import { User, NewUser, RoleEntity, NewRoleEntity, Permission, NewPermission, RolePermission } from './schema/pg.js';
9
- export { NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
9
+ export { CredentialSetupSession, NewCredentialSetupSession, NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, credentialSetupSessionsTable, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
10
10
  import { CacheService } from 'najm-cache';
11
11
  import { z } from 'zod';
12
12
  import { Context } from 'hono';
13
13
  import { GuardResult } from 'najm-guard';
14
+ import { CookieService } from 'najm-cookies';
14
15
  import 'drizzle-orm';
15
16
  import 'drizzle-orm/pg-core';
16
17
 
@@ -130,6 +131,8 @@ interface AuthConfig {
130
131
  interface AuthSchema {
131
132
  users: any;
132
133
  tokens: any;
134
+ /** Durable one-time browser sessions used by CredentialSetupService. */
135
+ credentialSetupSessions?: any;
133
136
  roles: any;
134
137
  permissions: any;
135
138
  rolePermissions: any;
@@ -1263,6 +1266,16 @@ declare class AuthService {
1263
1266
  loginUser(body: LoginDto): Promise<TokenPair & {
1264
1267
  user: SanitizedUser;
1265
1268
  }>;
1269
+ /**
1270
+ * Verify credentials and account policy without minting access/refresh
1271
+ * tokens or writing normal auth cookies. Sensitive onboarding flows can use
1272
+ * this before issuing a purpose-bound CredentialSetupService session.
1273
+ */
1274
+ verifyCredentials(body: LoginDto): Promise<SanitizedUser>;
1275
+ /** Establish a complete normal auth session for an already verified user. */
1276
+ establishSession(user: SanitizedUser): Promise<TokenPair & {
1277
+ user: SanitizedUser;
1278
+ }>;
1266
1279
  refreshTokens(): Promise<TokenPair>;
1267
1280
  /**
1268
1281
  * Reissue the short-lived signed session snapshot from a fully validated
@@ -2195,6 +2208,74 @@ declare class UserController {
2195
2208
  removeRole(params: UserIdInParam): Promise<SanitizedUser>;
2196
2209
  }
2197
2210
 
2211
+ type NewSetupSession = {
2212
+ userId: string;
2213
+ purpose: string;
2214
+ tokenHash: string;
2215
+ expiresAt: string;
2216
+ };
2217
+ declare class CredentialSetupRepository {
2218
+ private db;
2219
+ private schema;
2220
+ private get sessions();
2221
+ replaceActive(data: NewSetupSession): Promise<any>;
2222
+ findActive(tokenHash: string, purpose: string): Promise<any>;
2223
+ consume(tokenHash: string, purpose: string): Promise<any>;
2224
+ revoke(tokenHash: string, purpose: string): Promise<any>;
2225
+ deleteExpired(): Promise<any>;
2226
+ }
2227
+
2228
+ interface CredentialSetupOptions {
2229
+ /** Stable server-owned purpose, for example `password-setup`. */
2230
+ purpose: string;
2231
+ /** HttpOnly browser-session cookie name (default: `najm.credential-setup`). */
2232
+ cookieName?: string;
2233
+ /** Session lifetime in milliseconds (default: 10 minutes, maximum: 24 hours). */
2234
+ ttlMs?: number;
2235
+ /** Cookie path (default: `/`). */
2236
+ cookiePath?: string;
2237
+ }
2238
+ interface CredentialSetupSessionInfo {
2239
+ userId: string;
2240
+ purpose: string;
2241
+ expiresAt: string;
2242
+ }
2243
+ interface CredentialSetupStarted {
2244
+ purpose: string;
2245
+ expiresAt: string;
2246
+ }
2247
+
2248
+ declare class CredentialSetupService {
2249
+ private readonly repository;
2250
+ private readonly tokens;
2251
+ private readonly authCookies;
2252
+ private readonly cookies;
2253
+ constructor(repository: CredentialSetupRepository, tokens: TokenService, authCookies: CookieManager, cookies: CookieService);
2254
+ /**
2255
+ * Revoke normal auth sessions and issue a single-purpose, short-lived,
2256
+ * browser-session cookie. No access or refresh token is minted.
2257
+ */
2258
+ begin(userId: string, options: CredentialSetupOptions): Promise<CredentialSetupStarted>;
2259
+ /** Validate and return the current active setup session without consuming it. */
2260
+ require(options: CredentialSetupOptions): Promise<CredentialSetupSessionInfo>;
2261
+ /**
2262
+ * Atomically consume the setup session and execute the application-owned
2263
+ * credential mutation in the same database transaction. If the callback
2264
+ * fails, consumption rolls back and the browser may safely retry.
2265
+ */
2266
+ consume<T>(options: CredentialSetupOptions, complete: (session: CredentialSetupSessionInfo) => Promise<T> | T): Promise<T>;
2267
+ cancel(options: CredentialSetupOptions): Promise<{
2268
+ cancelled: true;
2269
+ }>;
2270
+ pruneExpired(): Promise<void>;
2271
+ private resolveOptions;
2272
+ private hashToken;
2273
+ private setCookie;
2274
+ private clearCookie;
2275
+ }
2276
+
2277
+ declare const CREDENTIAL_SETUP_MODULE: readonly [typeof CredentialSetupRepository, typeof CredentialSetupService];
2278
+
2198
2279
  declare const USER_STATUS: readonly ["active", "inactive", "pending"];
2199
2280
  declare const TOKEN_STATUS: readonly ["active", "revoked", "expired"];
2200
2281
  declare const TOKEN_TYPE: readonly ["access", "refresh"];
@@ -2336,4 +2417,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
2336
2417
  */
2337
2418
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
2338
2419
 
2339
- export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
2420
+ export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, CREDENTIAL_SETUP_MODULE, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type CredentialSetupOptions, CredentialSetupRepository, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, type DefineRolesOptions, type EmailParam, EncryptionService, type GoogleOAuthConfig, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/AuthPlugin.ts
9
- import { Err as Err11, plugin } from "najm-core";
9
+ import { Err as Err13, plugin } from "najm-core";
10
10
  import { cache } from "najm-cache";
11
11
 
12
12
  // src/auth.tokens.ts
@@ -90,6 +90,18 @@ var tokensTable = pgTable("tokens", {
90
90
  userIdIdx: index("tokens_user_id_idx").on(table.userId),
91
91
  expiresAtIdx: index("tokens_expires_at_idx").on(table.expiresAt)
92
92
  }));
93
+ var credentialSetupSessionsTable = pgTable("credential_setup_sessions", {
94
+ ...baseFields(16),
95
+ userId: text("user_id").references(() => usersTable.id, { onDelete: "cascade" }).notNull(),
96
+ purpose: text("purpose").notNull(),
97
+ tokenHash: text("token_hash").notNull().unique(),
98
+ expiresAt: timestamp("expires_at", { mode: "string" }).notNull(),
99
+ consumedAt: timestamp("consumed_at", { mode: "string" }),
100
+ revokedAt: timestamp("revoked_at", { mode: "string" })
101
+ }, (table) => ({
102
+ userPurposeIdx: index("credential_setup_sessions_user_purpose_idx").on(table.userId, table.purpose),
103
+ expiresAtIdx: index("credential_setup_sessions_expires_at_idx").on(table.expiresAt)
104
+ }));
93
105
  var rolePermissionsTable = pgTable("role_permissions", {
94
106
  roleId: text("role_id").notNull().references(() => rolesTable.id, { onDelete: "cascade" }),
95
107
  permissionId: text("permission_id").notNull().references(() => permissionsTable.id, { onDelete: "cascade" }),
@@ -101,6 +113,7 @@ var authSchema = {
101
113
  users: usersTable,
102
114
  oauthAccounts: oauthAccountsTable,
103
115
  tokens: tokensTable,
116
+ credentialSetupSessions: credentialSetupSessionsTable,
104
117
  roles: rolesTable,
105
118
  permissions: permissionsTable,
106
119
  rolePermissions: rolePermissionsTable
@@ -169,6 +182,18 @@ var tokensTable2 = sqliteTable("tokens", {
169
182
  userIdIdx: index2("tokens_user_id_idx").on(table.userId),
170
183
  expiresAtIdx: index2("tokens_expires_at_idx").on(table.expiresAt)
171
184
  }));
185
+ var credentialSetupSessionsTable2 = sqliteTable("credential_setup_sessions", {
186
+ ...baseFields2(16),
187
+ userId: text2("user_id").references(() => usersTable2.id, { onDelete: "cascade" }).notNull(),
188
+ purpose: text2("purpose").notNull(),
189
+ tokenHash: text2("token_hash").notNull().unique(),
190
+ expiresAt: text2("expires_at").notNull(),
191
+ consumedAt: text2("consumed_at"),
192
+ revokedAt: text2("revoked_at")
193
+ }, (table) => ({
194
+ userPurposeIdx: index2("credential_setup_sessions_user_purpose_idx").on(table.userId, table.purpose),
195
+ expiresAtIdx: index2("credential_setup_sessions_expires_at_idx").on(table.expiresAt)
196
+ }));
172
197
  var rolePermissionsTable2 = sqliteTable("role_permissions", {
173
198
  ...baseFields2(10),
174
199
  roleId: text2("role_id").notNull().references(() => rolesTable2.id, { onDelete: "cascade" }),
@@ -180,6 +205,7 @@ var authSchema2 = {
180
205
  users: usersTable2,
181
206
  oauthAccounts: oauthAccountsTable2,
182
207
  tokens: tokensTable2,
208
+ credentialSetupSessions: credentialSetupSessionsTable2,
183
209
  roles: rolesTable2,
184
210
  permissions: permissionsTable2,
185
211
  rolePermissions: rolePermissionsTable2
@@ -2288,6 +2314,15 @@ var AuthService = class AuthService2 {
2288
2314
  return this.inviteUser(body);
2289
2315
  }
2290
2316
  async loginUser(body) {
2317
+ const user = await this.verifyCredentials(body);
2318
+ return this.establishSession(user);
2319
+ }
2320
+ /**
2321
+ * Verify credentials and account policy without minting access/refresh
2322
+ * tokens or writing normal auth cookies. Sensitive onboarding flows can use
2323
+ * this before issuing a purpose-bound CredentialSetupService session.
2324
+ */
2325
+ async verifyCredentials(body) {
2291
2326
  const password = body.password;
2292
2327
  const rawIdentifier = "identifier" in body ? body.identifier : body.email;
2293
2328
  const identifier = normalizeAuthIdentifier(rawIdentifier);
@@ -2328,8 +2363,12 @@ var AuthService = class AuthService2 {
2328
2363
  await this.userService.resetFailedAttempts(user.id);
2329
2364
  }
2330
2365
  const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
2366
+ return sanitized;
2367
+ }
2368
+ /** Establish a complete normal auth session for an already verified user. */
2369
+ async establishSession(user) {
2331
2370
  this.authSessionService ??= new AuthSessionService(this.tokenService, this.userService, this.cookieManager);
2332
- return this.authSessionService.establish(sanitized);
2371
+ return this.authSessionService.establish(user);
2333
2372
  }
2334
2373
  async refreshTokens() {
2335
2374
  const generated = await this.tokenService.refreshTokens();
@@ -4215,7 +4254,7 @@ function toSingular(plural) {
4215
4254
  }
4216
4255
  __name(toSingular, "toSingular");
4217
4256
  function createResourceGuards(ownershipClass, resourceType, resource, options) {
4218
- var _a22, _b12;
4257
+ var _a23, _b13;
4219
4258
  const writeGuard = options?.adminGuard ?? isAdmin;
4220
4259
  let AccessGuard = class AccessGuard {
4221
4260
  static {
@@ -4236,7 +4275,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
4236
4275
  __param10(1, Params4("id")),
4237
4276
  __metadata24("design:type", Function),
4238
4277
  __metadata24("design:paramtypes", [Object, String]),
4239
- __metadata24("design:returntype", typeof (_a22 = typeof Promise !== "undefined" && Promise) === "function" ? _a22 : Object)
4278
+ __metadata24("design:returntype", typeof (_a23 = typeof Promise !== "undefined" && Promise) === "function" ? _a23 : Object)
4240
4279
  ], AccessGuard.prototype, "canActivate", null);
4241
4280
  AccessGuard = __decorate24([
4242
4281
  Injectable12()
@@ -4259,7 +4298,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
4259
4298
  __param10(0, User4()),
4260
4299
  __metadata24("design:type", Function),
4261
4300
  __metadata24("design:paramtypes", [Object]),
4262
- __metadata24("design:returntype", typeof (_b12 = typeof Promise !== "undefined" && Promise) === "function" ? _b12 : Object)
4301
+ __metadata24("design:returntype", typeof (_b13 = typeof Promise !== "undefined" && Promise) === "function" ? _b13 : Object)
4263
4302
  ], ListGuard.prototype, "canActivate", null);
4264
4303
  ListGuard = __decorate24([
4265
4304
  Injectable12()
@@ -4397,7 +4436,7 @@ function configureOwnership(config) {
4397
4436
  Injectable12()
4398
4437
  ], GeneratedOwnershipService);
4399
4438
  function bodyGuard(resourceType, bodyField, optional = false) {
4400
- var _a22;
4439
+ var _a23;
4401
4440
  let BodyAccessGuard = class BodyAccessGuard {
4402
4441
  static {
4403
4442
  __name(this, "BodyAccessGuard");
@@ -4419,7 +4458,7 @@ function configureOwnership(config) {
4419
4458
  __param10(1, Body5()),
4420
4459
  __metadata24("design:type", Function),
4421
4460
  __metadata24("design:paramtypes", [Object, Object]),
4422
- __metadata24("design:returntype", typeof (_a22 = typeof Promise !== "undefined" && Promise) === "function" ? _a22 : Object)
4461
+ __metadata24("design:returntype", typeof (_a23 = typeof Promise !== "undefined" && Promise) === "function" ? _a23 : Object)
4423
4462
  ], BodyAccessGuard.prototype, "canActivate", null);
4424
4463
  BodyAccessGuard = __decorate24([
4425
4464
  Injectable12()
@@ -5456,6 +5495,252 @@ var OAUTH_MODULE = [
5456
5495
  OAuthController
5457
5496
  ];
5458
5497
 
5498
+ // src/credentialSetup/CredentialSetupRepository.ts
5499
+ import { and as and5, eq as eq8, gt, isNull as isNull2, lt as lt2 } from "drizzle-orm";
5500
+ import { Err as Err11, Inject as Inject18, Repository as Repository6 } from "najm-core";
5501
+ import { DB as DB6 } from "najm-database";
5502
+ var __decorate33 = function(decorators, target, key, desc) {
5503
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5504
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5505
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5506
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5507
+ };
5508
+ var __metadata33 = function(k, v) {
5509
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
5510
+ };
5511
+ var CredentialSetupRepository = class CredentialSetupRepository2 {
5512
+ static {
5513
+ __name(this, "CredentialSetupRepository");
5514
+ }
5515
+ db;
5516
+ schema;
5517
+ get sessions() {
5518
+ const sessions = this.schema.credentialSetupSessions;
5519
+ if (!sessions) {
5520
+ Err11.invalidOperation("auth.schema.credentialSetupSessions is required to use CredentialSetupService");
5521
+ }
5522
+ return sessions;
5523
+ }
5524
+ async replaceActive(data) {
5525
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5526
+ await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and5(eq8(this.sessions.userId, data.userId), eq8(this.sessions.purpose, data.purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt)));
5527
+ const [session] = await this.db.insert(this.sessions).values(data).returning({
5528
+ userId: this.sessions.userId,
5529
+ purpose: this.sessions.purpose,
5530
+ expiresAt: this.sessions.expiresAt
5531
+ });
5532
+ return session;
5533
+ }
5534
+ async findActive(tokenHash, purpose) {
5535
+ const [session] = await this.db.select({
5536
+ userId: this.sessions.userId,
5537
+ purpose: this.sessions.purpose,
5538
+ expiresAt: this.sessions.expiresAt
5539
+ }).from(this.sessions).where(and5(eq8(this.sessions.tokenHash, tokenHash), eq8(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt), gt(this.sessions.expiresAt, (/* @__PURE__ */ new Date()).toISOString()))).limit(1);
5540
+ return session;
5541
+ }
5542
+ async consume(tokenHash, purpose) {
5543
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5544
+ const [session] = await this.db.update(this.sessions).set({ consumedAt: now, updatedAt: now }).where(and5(eq8(this.sessions.tokenHash, tokenHash), eq8(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt), gt(this.sessions.expiresAt, now))).returning({
5545
+ userId: this.sessions.userId,
5546
+ purpose: this.sessions.purpose,
5547
+ expiresAt: this.sessions.expiresAt
5548
+ });
5549
+ return session;
5550
+ }
5551
+ async revoke(tokenHash, purpose) {
5552
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5553
+ const [session] = await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and5(eq8(this.sessions.tokenHash, tokenHash), eq8(this.sessions.purpose, purpose), isNull2(this.sessions.consumedAt), isNull2(this.sessions.revokedAt))).returning({ userId: this.sessions.userId });
5554
+ return session;
5555
+ }
5556
+ async deleteExpired() {
5557
+ return this.db.delete(this.sessions).where(lt2(this.sessions.expiresAt, (/* @__PURE__ */ new Date()).toISOString())).returning({ userId: this.sessions.userId });
5558
+ }
5559
+ };
5560
+ __decorate33([
5561
+ DB6(),
5562
+ __metadata33("design:type", Object)
5563
+ ], CredentialSetupRepository.prototype, "db", void 0);
5564
+ __decorate33([
5565
+ Inject18(AUTH_SCHEMA),
5566
+ __metadata33("design:type", Object)
5567
+ ], CredentialSetupRepository.prototype, "schema", void 0);
5568
+ CredentialSetupRepository = __decorate33([
5569
+ Repository6()
5570
+ ], CredentialSetupRepository);
5571
+
5572
+ // src/credentialSetup/CredentialSetupService.ts
5573
+ import { createHash as createHash5, randomBytes as randomBytes4 } from "crypto";
5574
+ import { CookieService as CookieService3 } from "najm-cookies";
5575
+ import { Err as Err12, Injectable as Injectable19 } from "najm-core";
5576
+ import { Transaction as Transaction3 } from "najm-database";
5577
+ var __decorate34 = function(decorators, target, key, desc) {
5578
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5579
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5580
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5581
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5582
+ };
5583
+ var __metadata34 = function(k, v) {
5584
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
5585
+ };
5586
+ var _a22;
5587
+ var _b12;
5588
+ var _c8;
5589
+ var _d5;
5590
+ var _e4;
5591
+ var _f4;
5592
+ var _g3;
5593
+ var DEFAULT_COOKIE_NAME = "najm.credential-setup";
5594
+ var DEFAULT_TTL_MS = 10 * 60 * 1e3;
5595
+ var MAX_TTL_MS = 24 * 60 * 60 * 1e3;
5596
+ var PURPOSE_PATTERN = /^[a-z0-9](?:[a-z0-9:_-]{0,62}[a-z0-9])?$/;
5597
+ var COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
5598
+ var CredentialSetupService = class CredentialSetupService2 {
5599
+ static {
5600
+ __name(this, "CredentialSetupService");
5601
+ }
5602
+ repository;
5603
+ tokens;
5604
+ authCookies;
5605
+ cookies;
5606
+ constructor(repository, tokens, authCookies, cookies2) {
5607
+ this.repository = repository;
5608
+ this.tokens = tokens;
5609
+ this.authCookies = authCookies;
5610
+ this.cookies = cookies2;
5611
+ }
5612
+ /**
5613
+ * Revoke normal auth sessions and issue a single-purpose, short-lived,
5614
+ * browser-session cookie. No access or refresh token is minted.
5615
+ */
5616
+ async begin(userId, options) {
5617
+ const resolved = this.resolveOptions(options);
5618
+ const token = randomBytes4(32).toString("base64url");
5619
+ const expiresAt = new Date(Date.now() + resolved.ttlMs).toISOString();
5620
+ await this.repository.deleteExpired();
5621
+ await this.repository.replaceActive({
5622
+ userId,
5623
+ purpose: resolved.purpose,
5624
+ tokenHash: this.hashToken(token),
5625
+ expiresAt
5626
+ });
5627
+ await this.tokens.invalidateUserAccessTokens(userId);
5628
+ await this.tokens.revokeAllForUser(userId);
5629
+ this.authCookies.clearRefreshToken();
5630
+ this.authCookies.clearSessionCookie();
5631
+ this.setCookie(token, resolved);
5632
+ return { purpose: resolved.purpose, expiresAt };
5633
+ }
5634
+ /** Validate and return the current active setup session without consuming it. */
5635
+ async require(options) {
5636
+ const resolved = this.resolveOptions(options);
5637
+ const token = this.cookies.get(resolved.cookieName);
5638
+ if (!token)
5639
+ Err12("Credential setup session is required", 401);
5640
+ const session = await this.repository.findActive(this.hashToken(token), resolved.purpose);
5641
+ if (!session) {
5642
+ this.clearCookie(resolved);
5643
+ Err12("Credential setup session is invalid or expired", 401);
5644
+ }
5645
+ return session;
5646
+ }
5647
+ /**
5648
+ * Atomically consume the setup session and execute the application-owned
5649
+ * credential mutation in the same database transaction. If the callback
5650
+ * fails, consumption rolls back and the browser may safely retry.
5651
+ */
5652
+ async consume(options, complete) {
5653
+ const resolved = this.resolveOptions(options);
5654
+ const token = this.cookies.get(resolved.cookieName);
5655
+ if (!token)
5656
+ Err12("Credential setup session is required", 401);
5657
+ const session = await this.repository.consume(this.hashToken(token), resolved.purpose);
5658
+ if (!session) {
5659
+ this.clearCookie(resolved);
5660
+ Err12("Credential setup session is invalid or expired", 401);
5661
+ }
5662
+ const result = await complete(session);
5663
+ this.clearCookie(resolved);
5664
+ return result;
5665
+ }
5666
+ async cancel(options) {
5667
+ const resolved = this.resolveOptions(options);
5668
+ const token = this.cookies.get(resolved.cookieName);
5669
+ if (token) {
5670
+ await this.repository.revoke(this.hashToken(token), resolved.purpose);
5671
+ }
5672
+ this.clearCookie(resolved);
5673
+ return { cancelled: true };
5674
+ }
5675
+ async pruneExpired() {
5676
+ await this.repository.deleteExpired();
5677
+ }
5678
+ resolveOptions(options) {
5679
+ const purpose = options.purpose?.trim().toLowerCase();
5680
+ const cookieName = options.cookieName?.trim() || DEFAULT_COOKIE_NAME;
5681
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
5682
+ const cookiePath = options.cookiePath ?? "/";
5683
+ if (!purpose || !PURPOSE_PATTERN.test(purpose)) {
5684
+ Err12("Credential setup purpose must be 1-64 lowercase letters, numbers, colon, underscore, or hyphen", 500);
5685
+ }
5686
+ if (!COOKIE_NAME_PATTERN.test(cookieName) || cookieName.length > 128) {
5687
+ Err12("Credential setup cookie name is invalid", 500);
5688
+ }
5689
+ if (!Number.isInteger(ttlMs) || ttlMs < 1e3 || ttlMs > MAX_TTL_MS) {
5690
+ Err12("Credential setup ttlMs must be an integer between 1000 and 86400000", 500);
5691
+ }
5692
+ if (!cookiePath.startsWith("/") || cookiePath.includes(";") || cookiePath.includes("\\")) {
5693
+ Err12("Credential setup cookiePath must be a same-origin path", 500);
5694
+ }
5695
+ return { purpose, cookieName, ttlMs, cookiePath };
5696
+ }
5697
+ hashToken(token) {
5698
+ return createHash5("sha256").update(token).digest("hex");
5699
+ }
5700
+ setCookie(token, options) {
5701
+ this.cookies.setSession(options.cookieName, token, {
5702
+ httpOnly: true,
5703
+ path: options.cookiePath,
5704
+ sameSite: "Strict",
5705
+ secure: process.env.NODE_ENV === "production"
5706
+ });
5707
+ }
5708
+ clearCookie(options) {
5709
+ this.cookies.delete(options.cookieName, {
5710
+ path: options.cookiePath,
5711
+ secure: process.env.NODE_ENV === "production"
5712
+ });
5713
+ }
5714
+ };
5715
+ __decorate34([
5716
+ Transaction3({ retries: 2 }),
5717
+ __metadata34("design:type", Function),
5718
+ __metadata34("design:paramtypes", [String, Object]),
5719
+ __metadata34("design:returntype", typeof (_e4 = typeof Promise !== "undefined" && Promise) === "function" ? _e4 : Object)
5720
+ ], CredentialSetupService.prototype, "begin", null);
5721
+ __decorate34([
5722
+ Transaction3({ retries: 2 }),
5723
+ __metadata34("design:type", Function),
5724
+ __metadata34("design:paramtypes", [Object, Function]),
5725
+ __metadata34("design:returntype", typeof (_f4 = typeof Promise !== "undefined" && Promise) === "function" ? _f4 : Object)
5726
+ ], CredentialSetupService.prototype, "consume", null);
5727
+ __decorate34([
5728
+ Transaction3({ retries: 2 }),
5729
+ __metadata34("design:type", Function),
5730
+ __metadata34("design:paramtypes", [Object]),
5731
+ __metadata34("design:returntype", typeof (_g3 = typeof Promise !== "undefined" && Promise) === "function" ? _g3 : Object)
5732
+ ], CredentialSetupService.prototype, "cancel", null);
5733
+ CredentialSetupService = __decorate34([
5734
+ Injectable19(),
5735
+ __metadata34("design:paramtypes", [typeof (_a22 = typeof CredentialSetupRepository !== "undefined" && CredentialSetupRepository) === "function" ? _a22 : Object, typeof (_b12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _b12 : Object, typeof (_c8 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c8 : Object, typeof (_d5 = typeof CookieService3 !== "undefined" && CookieService3) === "function" ? _d5 : Object])
5736
+ ], CredentialSetupService);
5737
+
5738
+ // src/credentialSetup/index.ts
5739
+ var CREDENTIAL_SETUP_MODULE = [
5740
+ CredentialSetupRepository,
5741
+ CredentialSetupService
5742
+ ];
5743
+
5459
5744
  // src/AuthPlugin.ts
5460
5745
  var DEFAULT_JWT = {
5461
5746
  accessSecret: process.env.JWT_ACCESS_SECRET || "",
@@ -5477,9 +5762,9 @@ var resolveGoogleConfig = /* @__PURE__ */ __name((config) => {
5477
5762
  const clientId = google.clientId ?? process.env.GOOGLE_CLIENT_ID ?? "";
5478
5763
  const clientSecret = google.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET ?? "";
5479
5764
  if (!clientId)
5480
- throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_ID");
5765
+ throw Err13.configRequired("auth.oauth.google", "GOOGLE_CLIENT_ID");
5481
5766
  if (!clientSecret)
5482
- throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_SECRET");
5767
+ throw Err13.configRequired("auth.oauth.google", "GOOGLE_CLIENT_SECRET");
5483
5768
  const frontendUrl = config?.frontendUrl ?? process.env.FRONTEND_URL ?? "http://localhost:3000";
5484
5769
  const callbackUrl = google.callbackUrl ?? process.env.GOOGLE_CALLBACK_URL ?? `${frontendUrl.replace(/\/$/, "")}/api/auth/oauth/google/callback`;
5485
5770
  let callback;
@@ -5538,10 +5823,10 @@ var resolveAuthConfig = /* @__PURE__ */ __name((config) => {
5538
5823
  }
5539
5824
  };
5540
5825
  if (!finalConfig.jwt.accessSecret) {
5541
- throw Err11.configRequired("auth", "JWT_ACCESS_SECRET");
5826
+ throw Err13.configRequired("auth", "JWT_ACCESS_SECRET");
5542
5827
  }
5543
5828
  if (!finalConfig.jwt.refreshSecret) {
5544
- throw Err11.configRequired("auth", "JWT_REFRESH_SECRET");
5829
+ throw Err13.configRequired("auth", "JWT_REFRESH_SECRET");
5545
5830
  }
5546
5831
  return finalConfig;
5547
5832
  }, "resolveAuthConfig");
@@ -5561,7 +5846,7 @@ var selectAuthSchema = /* @__PURE__ */ __name((config) => {
5561
5846
  return authSchema;
5562
5847
  }
5563
5848
  }, "selectAuthSchema");
5564
- var auth = /* @__PURE__ */ __name((config) => plugin("auth").version("1.0.0").depends(cache(), cookies(), i18n(), guards(), validation(config?.validation), rateLimit(config?.rateLimit), email(config?.email)).requires("database").contributes(I18N_CONTRIBUTIONS, AUTH_LOCALES).services(AUTH_MODULE, OAUTH_MODULE, users_exports, roles_exports, permissions_exports, tokens_exports, ScopeContext).config(AUTH_CONFIG, resolveAuthConfig(config)).set(AUTH_SCHEMA, selectAuthSchema(config)).set(AUTH_ENCRYPTION_KEY, config?.encryptionKey ?? null).build(), "auth");
5849
+ var auth = /* @__PURE__ */ __name((config) => plugin("auth").version("1.0.0").depends(cache(), cookies(), i18n(), guards(), validation(config?.validation), rateLimit(config?.rateLimit), email(config?.email)).requires("database").contributes(I18N_CONTRIBUTIONS, AUTH_LOCALES).services(AUTH_MODULE, OAUTH_MODULE, users_exports, roles_exports, permissions_exports, tokens_exports, CREDENTIAL_SETUP_MODULE, ScopeContext).config(AUTH_CONFIG, resolveAuthConfig(config)).set(AUTH_SCHEMA, selectAuthSchema(config)).set(AUTH_ENCRYPTION_KEY, config?.encryptionKey ?? null).build(), "auth");
5565
5850
 
5566
5851
  // src/seed.ts
5567
5852
  var toSeedId = /* @__PURE__ */ __name((prefix, value) => {
@@ -5708,6 +5993,7 @@ export {
5708
5993
  AuthResolver,
5709
5994
  AuthService,
5710
5995
  AuthSessionService,
5996
+ CREDENTIAL_SETUP_MODULE,
5711
5997
  Can,
5712
5998
  CanCreate,
5713
5999
  CanDelete,
@@ -5715,6 +6001,8 @@ export {
5715
6001
  CanRead,
5716
6002
  CanUpdate,
5717
6003
  CookieManager,
6004
+ CredentialSetupRepository,
6005
+ CredentialSetupService,
5718
6006
  EncryptionService,
5719
6007
  Owned,
5720
6008
  OwnershipToken,
@@ -5762,6 +6050,7 @@ export {
5762
6050
  createRoleDto,
5763
6051
  createTokenDto,
5764
6052
  createUserDto,
6053
+ credentialSetupSessionsTable,
5765
6054
  defineRoles,
5766
6055
  emailParam,
5767
6056
  formatDate,