@spfn/auth 0.2.0-beta.43 → 0.2.0-beta.45

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/dist/server.js CHANGED
@@ -4564,7 +4564,7 @@ var init_roles = __esm({
4564
4564
  });
4565
4565
 
4566
4566
  // src/server/entities/users.ts
4567
- import { text as text2, check, boolean as boolean2, index as index2 } from "drizzle-orm/pg-core";
4567
+ import { text as text2, check, boolean as boolean2, index as index2, uuid } from "drizzle-orm/pg-core";
4568
4568
  import { id as id2, timestamps as timestamps2, enumText, utcTimestamp, foreignKey } from "@spfn/core/db";
4569
4569
  import { sql } from "drizzle-orm";
4570
4570
  var users;
@@ -4579,6 +4579,9 @@ var init_users = __esm({
4579
4579
  {
4580
4580
  // Identity
4581
4581
  id: id2(),
4582
+ // Public-facing UUID (for URLs, external APIs)
4583
+ // Never expose internal bigserial ID externally
4584
+ publicId: uuid("public_id").notNull().unique().defaultRandom(),
4582
4585
  // Email address (unique identifier)
4583
4586
  // Used for: login, password reset, notifications
4584
4587
  email: text2("email").unique(),
@@ -4626,6 +4629,7 @@ var init_users = __esm({
4626
4629
  sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
4627
4630
  ),
4628
4631
  // Indexes for query optimization
4632
+ index2("users_public_id_idx").on(table.publicId),
4629
4633
  index2("users_email_idx").on(table.email),
4630
4634
  index2("users_phone_idx").on(table.phone),
4631
4635
  index2("users_username_idx").on(table.username),
@@ -5295,6 +5299,27 @@ var init_user_permissions = __esm({
5295
5299
  }
5296
5300
  });
5297
5301
 
5302
+ // src/server/entities/auth-metadata.ts
5303
+ import { text as text10, timestamp } from "drizzle-orm/pg-core";
5304
+ var authMetadata;
5305
+ var init_auth_metadata = __esm({
5306
+ "src/server/entities/auth-metadata.ts"() {
5307
+ "use strict";
5308
+ init_schema4();
5309
+ authMetadata = authSchema.table(
5310
+ "auth_metadata",
5311
+ {
5312
+ // Metadata key (primary key)
5313
+ key: text10("key").primaryKey(),
5314
+ // Metadata value
5315
+ value: text10("value").notNull(),
5316
+ // Last updated timestamp
5317
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5318
+ }
5319
+ );
5320
+ }
5321
+ });
5322
+
5298
5323
  // src/server/entities/index.ts
5299
5324
  var init_entities = __esm({
5300
5325
  "src/server/entities/index.ts"() {
@@ -5310,6 +5335,7 @@ var init_entities = __esm({
5310
5335
  init_permissions();
5311
5336
  init_role_permissions();
5312
5337
  init_user_permissions();
5338
+ init_auth_metadata();
5313
5339
  }
5314
5340
  });
5315
5341
 
@@ -5355,6 +5381,14 @@ var init_users_repository = __esm({
5355
5381
  const result = await this.readDb.select().from(users).where(eq(users.username, username)).limit(1);
5356
5382
  return result[0] ?? null;
5357
5383
  }
5384
+ /**
5385
+ * Public ID(UUID)로 사용자 조회
5386
+ * Read replica 사용
5387
+ */
5388
+ async findByPublicId(publicId) {
5389
+ const result = await this.readDb.select().from(users).where(eq(users.publicId, publicId)).limit(1);
5390
+ return result[0] ?? null;
5391
+ }
5358
5392
  /**
5359
5393
  * 이메일 또는 전화번호로 사용자 조회
5360
5394
  * Read replica 사용
@@ -5495,6 +5529,7 @@ var init_users_repository = __esm({
5495
5529
  async fetchMinimalUserData(userId) {
5496
5530
  const user = await this.readDb.select({
5497
5531
  id: users.id,
5532
+ publicId: users.publicId,
5498
5533
  email: users.email,
5499
5534
  username: users.username,
5500
5535
  emailVerifiedAt: users.emailVerifiedAt,
@@ -5505,6 +5540,7 @@ var init_users_repository = __esm({
5505
5540
  }
5506
5541
  return {
5507
5542
  userId: user.id,
5543
+ publicId: user.publicId,
5508
5544
  email: user.email,
5509
5545
  username: user.username,
5510
5546
  isEmailVerified: !!user.emailVerifiedAt,
@@ -5521,6 +5557,7 @@ var init_users_repository = __esm({
5521
5557
  async fetchFullUserData(userId) {
5522
5558
  const user = await this.readDb.select({
5523
5559
  id: users.id,
5560
+ publicId: users.publicId,
5524
5561
  email: users.email,
5525
5562
  username: users.username,
5526
5563
  emailVerifiedAt: users.emailVerifiedAt,
@@ -5534,6 +5571,7 @@ var init_users_repository = __esm({
5534
5571
  }
5535
5572
  return {
5536
5573
  userId: user.id,
5574
+ publicId: user.publicId,
5537
5575
  email: user.email,
5538
5576
  username: user.username,
5539
5577
  isEmailVerified: !!user.emailVerifiedAt,
@@ -6299,16 +6337,16 @@ var init_invitations_repository = __esm({
6299
6337
  /**
6300
6338
  * 초대 상태 업데이트
6301
6339
  */
6302
- async updateStatus(id11, status, timestamp) {
6340
+ async updateStatus(id11, status, timestamp2) {
6303
6341
  const updates = {
6304
6342
  status,
6305
6343
  updatedAt: /* @__PURE__ */ new Date()
6306
6344
  };
6307
- if (timestamp) {
6345
+ if (timestamp2) {
6308
6346
  if (status === "accepted") {
6309
- updates.acceptedAt = timestamp;
6347
+ updates.acceptedAt = timestamp2;
6310
6348
  } else if (status === "cancelled") {
6311
- updates.cancelledAt = timestamp;
6349
+ updates.cancelledAt = timestamp2;
6312
6350
  }
6313
6351
  }
6314
6352
  const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id11)).returning();
@@ -6542,6 +6580,43 @@ var init_social_accounts_repository = __esm({
6542
6580
  }
6543
6581
  });
6544
6582
 
6583
+ // src/server/repositories/auth-metadata.repository.ts
6584
+ import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
6585
+ import { eq as eq11 } from "drizzle-orm";
6586
+ var AuthMetadataRepository, authMetadataRepository;
6587
+ var init_auth_metadata_repository = __esm({
6588
+ "src/server/repositories/auth-metadata.repository.ts"() {
6589
+ "use strict";
6590
+ init_auth_metadata();
6591
+ AuthMetadataRepository = class extends BaseRepository11 {
6592
+ /**
6593
+ * 키로 값 조회
6594
+ */
6595
+ async get(key) {
6596
+ const result = await this.readDb.select().from(authMetadata).where(eq11(authMetadata.key, key)).limit(1);
6597
+ return result[0]?.value ?? null;
6598
+ }
6599
+ /**
6600
+ * 키-값 저장 (upsert)
6601
+ */
6602
+ async set(key, value) {
6603
+ await this.db.insert(authMetadata).values({
6604
+ key,
6605
+ value,
6606
+ updatedAt: /* @__PURE__ */ new Date()
6607
+ }).onConflictDoUpdate({
6608
+ target: authMetadata.key,
6609
+ set: {
6610
+ value,
6611
+ updatedAt: /* @__PURE__ */ new Date()
6612
+ }
6613
+ });
6614
+ }
6615
+ };
6616
+ authMetadataRepository = new AuthMetadataRepository();
6617
+ }
6618
+ });
6619
+
6545
6620
  // src/server/repositories/index.ts
6546
6621
  var init_repositories = __esm({
6547
6622
  "src/server/repositories/index.ts"() {
@@ -6556,6 +6631,7 @@ var init_repositories = __esm({
6556
6631
  init_user_profiles_repository();
6557
6632
  init_invitations_repository();
6558
6633
  init_social_accounts_repository();
6634
+ init_auth_metadata_repository();
6559
6635
  }
6560
6636
  });
6561
6637
 
@@ -7246,6 +7322,7 @@ async function registerService(params) {
7246
7322
  });
7247
7323
  const result = {
7248
7324
  userId: String(newUser.id),
7325
+ publicId: newUser.publicId,
7249
7326
  email: newUser.email || void 0,
7250
7327
  phone: newUser.phone || void 0
7251
7328
  };
@@ -7291,6 +7368,7 @@ async function loginService(params) {
7291
7368
  await updateLastLoginService(user.id);
7292
7369
  const result = {
7293
7370
  userId: String(user.id),
7371
+ publicId: user.publicId,
7294
7372
  email: user.email || void 0,
7295
7373
  phone: user.phone || void 0,
7296
7374
  passwordChangeRequired: user.passwordChangeRequired
@@ -7337,6 +7415,7 @@ async function changePasswordService(params) {
7337
7415
  // src/server/services/rbac.service.ts
7338
7416
  init_repositories();
7339
7417
  init_rbac();
7418
+ import { createHash } from "crypto";
7340
7419
 
7341
7420
  // src/server/lib/config.ts
7342
7421
  import { env as env5 } from "@spfn/auth/config";
@@ -7399,6 +7478,33 @@ function getSessionTtl(override) {
7399
7478
  }
7400
7479
 
7401
7480
  // src/server/services/rbac.service.ts
7481
+ var RBAC_HASH_KEY = "rbac_config_hash";
7482
+ function computeConfigHash(allRoles, allPermissions, allMappings) {
7483
+ const payload = JSON.stringify({
7484
+ roles: allRoles.map((r) => ({ name: r.name, displayName: r.displayName, description: r.description, priority: r.priority, isSystem: r.isSystem, isBuiltin: r.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
7485
+ permissions: allPermissions.map((p) => ({ name: p.name, displayName: p.displayName, description: p.description, category: p.category, isSystem: p.isSystem, isBuiltin: p.isBuiltin })).sort((a, b) => a.name.localeCompare(b.name)),
7486
+ mappings: Object.keys(allMappings).sort().reduce((acc, key) => {
7487
+ acc[key] = [...allMappings[key]].sort();
7488
+ return acc;
7489
+ }, {})
7490
+ });
7491
+ return createHash("sha256").update(payload).digest("hex");
7492
+ }
7493
+ function collectMappings(options) {
7494
+ const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
7495
+ if (options.rolePermissions) {
7496
+ for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
7497
+ if (allMappings[roleName]) {
7498
+ allMappings[roleName] = [
7499
+ .../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
7500
+ ];
7501
+ } else {
7502
+ allMappings[roleName] = permNames;
7503
+ }
7504
+ }
7505
+ }
7506
+ return allMappings;
7507
+ }
7402
7508
  async function initializeAuth(options = {}) {
7403
7509
  authLogger.service.info("\u{1F510} Initializing RBAC system...");
7404
7510
  if (options.sessionTtl !== void 0) {
@@ -7411,100 +7517,100 @@ async function initializeAuth(options = {}) {
7411
7517
  ...Object.values(BUILTIN_ROLES),
7412
7518
  ...options.roles || []
7413
7519
  ];
7414
- for (const roleConfig of allRoles) {
7415
- await upsertRole(roleConfig);
7416
- }
7417
7520
  const allPermissions = [
7418
7521
  ...Object.values(BUILTIN_PERMISSIONS),
7419
7522
  ...options.permissions || []
7420
7523
  ];
7421
- for (const permConfig of allPermissions) {
7422
- await upsertPermission(permConfig);
7423
- }
7424
- const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
7425
- if (options.rolePermissions) {
7426
- for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
7427
- if (allMappings[roleName]) {
7428
- allMappings[roleName] = [
7429
- .../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
7430
- ];
7431
- } else {
7432
- allMappings[roleName] = permNames;
7433
- }
7434
- }
7435
- }
7436
- for (const [roleName, permNames] of Object.entries(allMappings)) {
7437
- await assignPermissionsToRole(roleName, permNames);
7524
+ const allMappings = collectMappings(options);
7525
+ const configHash = computeConfigHash(allRoles, allPermissions, allMappings);
7526
+ const storedHash = await authMetadataRepository.get(RBAC_HASH_KEY);
7527
+ if (storedHash === configHash) {
7528
+ authLogger.service.info("\u2705 RBAC config unchanged, skipping initialization");
7529
+ return;
7438
7530
  }
7531
+ authLogger.service.info("\u{1F504} RBAC config changed, applying updates...");
7532
+ const existingRoles = await rolesRepository.findAll();
7533
+ const existingPermissions = await permissionsRepository.findAll();
7534
+ const rolesByName = new Map(existingRoles.map((r) => [r.name, r]));
7535
+ const permsByName = new Map(existingPermissions.map((p) => [p.name, p]));
7536
+ await syncRoles(allRoles, rolesByName);
7537
+ await syncPermissions(allPermissions, permsByName);
7538
+ const updatedRoles = await rolesRepository.findAll();
7539
+ const updatedPermissions = await permissionsRepository.findAll();
7540
+ const updatedRolesByName = new Map(updatedRoles.map((r) => [r.name, r]));
7541
+ const updatedPermsByName = new Map(updatedPermissions.map((p) => [p.name, p]));
7542
+ await syncMappings(allMappings, updatedRolesByName, updatedPermsByName);
7543
+ await authMetadataRepository.set(RBAC_HASH_KEY, configHash);
7439
7544
  authLogger.service.info("\u2705 RBAC initialization complete");
7440
7545
  authLogger.service.info(`\u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
7441
7546
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
7442
7547
  }
7443
- async function upsertRole(config) {
7444
- const existing = await rolesRepository.findByName(config.name);
7445
- if (!existing) {
7446
- await rolesRepository.create({
7447
- name: config.name,
7448
- displayName: config.displayName,
7449
- description: config.description || null,
7450
- priority: config.priority ?? 10,
7451
- isSystem: config.isSystem ?? false,
7452
- isBuiltin: config.isBuiltin ?? false,
7453
- isActive: true
7454
- });
7455
- authLogger.service.info(` \u2705 Created role: ${config.name}`);
7456
- } else {
7457
- const updateData = {
7458
- displayName: config.displayName,
7459
- description: config.description || null
7460
- };
7461
- if (!existing.isBuiltin) {
7462
- updateData.priority = config.priority ?? existing.priority;
7548
+ async function syncRoles(configs, existingByName) {
7549
+ for (const config of configs) {
7550
+ const existing = existingByName.get(config.name);
7551
+ if (!existing) {
7552
+ await rolesRepository.create({
7553
+ name: config.name,
7554
+ displayName: config.displayName,
7555
+ description: config.description || null,
7556
+ priority: config.priority ?? 10,
7557
+ isSystem: config.isSystem ?? false,
7558
+ isBuiltin: config.isBuiltin ?? false,
7559
+ isActive: true
7560
+ });
7561
+ authLogger.service.info(` \u2705 Created role: ${config.name}`);
7562
+ } else {
7563
+ const updateData = {
7564
+ displayName: config.displayName,
7565
+ description: config.description || null
7566
+ };
7567
+ if (!existing.isBuiltin) {
7568
+ updateData.priority = config.priority ?? existing.priority;
7569
+ }
7570
+ await rolesRepository.updateById(existing.id, updateData);
7463
7571
  }
7464
- await rolesRepository.updateById(existing.id, updateData);
7465
- }
7466
- }
7467
- async function upsertPermission(config) {
7468
- const existing = await permissionsRepository.findByName(config.name);
7469
- if (!existing) {
7470
- await permissionsRepository.create({
7471
- name: config.name,
7472
- displayName: config.displayName,
7473
- description: config.description || null,
7474
- category: config.category || null,
7475
- isSystem: config.isSystem ?? false,
7476
- isBuiltin: config.isBuiltin ?? false,
7477
- isActive: true,
7478
- metadata: null
7479
- });
7480
- authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7481
- } else {
7482
- await permissionsRepository.updateById(existing.id, {
7483
- displayName: config.displayName,
7484
- description: config.description || null,
7485
- category: config.category || null
7486
- });
7487
7572
  }
7488
7573
  }
7489
- async function assignPermissionsToRole(roleName, permissionNames) {
7490
- const role = await rolesRepository.findByName(roleName);
7491
- if (!role) {
7492
- authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7493
- return;
7494
- }
7495
- const perms = await permissionsRepository.findByNames(permissionNames);
7496
- if (perms.length === 0) {
7497
- authLogger.service.warn(` \u26A0\uFE0F No permissions found for role: ${roleName}`);
7498
- return;
7574
+ async function syncPermissions(configs, existingByName) {
7575
+ for (const config of configs) {
7576
+ const existing = existingByName.get(config.name);
7577
+ if (!existing) {
7578
+ await permissionsRepository.create({
7579
+ name: config.name,
7580
+ displayName: config.displayName,
7581
+ description: config.description || null,
7582
+ category: config.category || null,
7583
+ isSystem: config.isSystem ?? false,
7584
+ isBuiltin: config.isBuiltin ?? false,
7585
+ isActive: true,
7586
+ metadata: null
7587
+ });
7588
+ authLogger.service.info(` \u2705 Created permission: ${config.name}`);
7589
+ } else {
7590
+ await permissionsRepository.updateById(existing.id, {
7591
+ displayName: config.displayName,
7592
+ description: config.description || null,
7593
+ category: config.category || null
7594
+ });
7595
+ }
7499
7596
  }
7500
- const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7501
- const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7502
- const newMappings = perms.filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7503
- roleId: role.id,
7504
- permissionId: perm.id
7505
- }));
7506
- if (newMappings.length > 0) {
7507
- await rolePermissionsRepository.createMany(newMappings);
7597
+ }
7598
+ async function syncMappings(allMappings, rolesByName, permsByName) {
7599
+ for (const [roleName, permNames] of Object.entries(allMappings)) {
7600
+ const role = rolesByName.get(roleName);
7601
+ if (!role) {
7602
+ authLogger.service.warn(` \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
7603
+ continue;
7604
+ }
7605
+ const existingMappings = await rolePermissionsRepository.findByRoleId(role.id);
7606
+ const existingPermIds = new Set(existingMappings.map((m) => m.permissionId));
7607
+ const newMappings = permNames.map((name) => permsByName.get(name)).filter((perm) => perm != null).filter((perm) => !existingPermIds.has(perm.id)).map((perm) => ({
7608
+ roleId: role.id,
7609
+ permissionId: perm.id
7610
+ }));
7611
+ if (newMappings.length > 0) {
7612
+ await rolePermissionsRepository.createMany(newMappings);
7613
+ }
7508
7614
  }
7509
7615
  }
7510
7616
 
@@ -7770,6 +7876,7 @@ async function getAuthSessionService(userId) {
7770
7876
  ]);
7771
7877
  return {
7772
7878
  userId: user.userId,
7879
+ publicId: user.publicId,
7773
7880
  email: user.email,
7774
7881
  emailVerified: user.isEmailVerified,
7775
7882
  phoneVerified: user.isPhoneVerified,
@@ -7787,6 +7894,7 @@ async function getUserProfileService(userId) {
7787
7894
  ]);
7788
7895
  return {
7789
7896
  userId: user.userId,
7897
+ publicId: user.publicId,
7790
7898
  email: user.email,
7791
7899
  username: user.username,
7792
7900
  emailVerified: user.isEmailVerified,
@@ -9471,6 +9579,7 @@ function createAuthLifecycle(options = {}) {
9471
9579
  };
9472
9580
  }
9473
9581
  export {
9582
+ AuthMetadataRepository,
9474
9583
  AuthProviderSchema,
9475
9584
  COOKIE_NAMES,
9476
9585
  EmailSchema,
@@ -9498,6 +9607,8 @@ export {
9498
9607
  addPermissionToRole,
9499
9608
  authLogger,
9500
9609
  authLoginEvent,
9610
+ authMetadata,
9611
+ authMetadataRepository,
9501
9612
  authRegisterEvent,
9502
9613
  mainAuthRouter as authRouter,
9503
9614
  authSchema,