@spfn/auth 0.3.0-beta.6 → 0.3.0-beta.8

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
@@ -4473,7 +4473,7 @@ var init_types = __esm({
4473
4473
 
4474
4474
  // src/server/routes/schema.ts
4475
4475
  import { EMAIL_PATTERN, PHONE_PATTERN } from "@spfn/auth";
4476
- var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
4476
+ var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
4477
4477
  var init_schema3 = __esm({
4478
4478
  "src/server/routes/schema.ts"() {
4479
4479
  "use strict";
@@ -4495,6 +4495,39 @@ var init_schema3 = __esm({
4495
4495
  KEY_PLATFORM.map((platform) => Type.Literal(platform)),
4496
4496
  { description: "Platform the key lives on" }
4497
4497
  );
4498
+ PublicKeySchema = Type.String({
4499
+ maxLength: 2048,
4500
+ description: "Client public key, SPKI DER in base64"
4501
+ });
4502
+ KeyIdSchema = Type.String({
4503
+ maxLength: 64,
4504
+ description: "Key identifier"
4505
+ });
4506
+ FingerprintSchema = Type.String({
4507
+ maxLength: 128,
4508
+ description: "SHA-256 hex fingerprint of the public key"
4509
+ });
4510
+ UserCodeSchema = Type.String({
4511
+ minLength: 8,
4512
+ maxLength: 16,
4513
+ description: "Device user code as displayed, e.g. WXYZ-2345. Dashes, spaces and case are ignored."
4514
+ });
4515
+ DeviceAuthPollResponseSchema = Type.Union([
4516
+ Type.Object({
4517
+ status: Type.Literal("pending"),
4518
+ intervalMillis: Type.Integer({ description: "Milliseconds to wait before polling again" })
4519
+ }),
4520
+ Type.Object({
4521
+ status: Type.Literal("approved"),
4522
+ userId: Type.String(),
4523
+ publicId: Type.String(),
4524
+ email: Type.Optional(Type.String()),
4525
+ phone: Type.Optional(Type.String()),
4526
+ passwordChangeRequired: Type.Boolean()
4527
+ })
4528
+ ], {
4529
+ description: "Pending, or the login the approval produced"
4530
+ });
4498
4531
  PasswordSchema = Type.String({
4499
4532
  minLength: 8,
4500
4533
  maxLength: 72,
@@ -5028,9 +5061,72 @@ var init_signup_link_tokens = __esm({
5028
5061
  }
5029
5062
  });
5030
5063
 
5064
+ // src/server/entities/device-authorizations.ts
5065
+ import { text as text8, uniqueIndex as uniqueIndex3 } from "drizzle-orm/pg-core";
5066
+ import { id as id8, timestamps as timestamps7, enumText as enumText5, utcTimestamp as utcTimestamp6, optionalForeignKey } from "@spfn/core/db";
5067
+ var DEVICE_AUTH_STATUSES, deviceAuthorizations;
5068
+ var init_device_authorizations = __esm({
5069
+ "src/server/entities/device-authorizations.ts"() {
5070
+ "use strict";
5071
+ init_types();
5072
+ init_users();
5073
+ init_schema4();
5074
+ DEVICE_AUTH_STATUSES = ["pending", "approved", "denied", "consumed"];
5075
+ deviceAuthorizations = authSchema.table(
5076
+ "device_authorizations",
5077
+ {
5078
+ id: id8(),
5079
+ // SHA-256 of the device code, hex
5080
+ // The code itself (32 random bytes, base64url) is returned to the waiting
5081
+ // device once and never stored: it is the only thing that device holds,
5082
+ // so a dump of this table must not let its reader finish someone's login
5083
+ deviceCodeHash: text8("device_code_hash").notNull().unique(),
5084
+ // The code a person reads off the waiting device's screen
5085
+ // Stored normalized — uppercase, no dash — because that is the only form
5086
+ // a lookup can match; the dash is put back for display
5087
+ // Plaintext on purpose: it authorizes nothing without an approver who is
5088
+ // already authenticated, and it has to be found by exact match
5089
+ userCode: text8("user_code").notNull(),
5090
+ // Key material the waiting device generated, same shapes as user_public_keys
5091
+ // Parked, not registered: it can sign nothing until a poll moves it over
5092
+ publicKey: text8("public_key").notNull(),
5093
+ keyId: text8("key_id").notNull(),
5094
+ fingerprint: text8("fingerprint").notNull(),
5095
+ algorithm: enumText5("algorithm", KEY_ALGORITHM).notNull().default("ES256"),
5096
+ // Device labels the waiting device supplied
5097
+ // null: it sent none
5098
+ // Shown to the approver so they can tell whether the device asking is the
5099
+ // one in front of them — display only, so a client that lies gains nothing
5100
+ // but a wrong line on the approval screen
5101
+ deviceName: text8("device_name"),
5102
+ platform: enumText5("platform", KEY_PLATFORM),
5103
+ status: enumText5("status", DEVICE_AUTH_STATUSES).notNull().default("pending"),
5104
+ // Who approved, written at approve time from the approver's session
5105
+ // null while pending, and on every record that was denied or expired
5106
+ // Never taken from a request body — that would be the whole authorization
5107
+ userId: optionalForeignKey("user", () => users.id, { onDelete: "cascade" }),
5108
+ // Expiry — 10 minutes from creation by default
5109
+ // Judged on read; no job clears the row
5110
+ expiresAt: utcTimestamp6("expires_at").notNull(),
5111
+ // Set when the approver said yes
5112
+ approvedAt: utcTimestamp6("approved_at"),
5113
+ // Set when the poll that registered the key won the race
5114
+ // Non-null means the record is spent, whatever else it says
5115
+ consumedAt: utcTimestamp6("consumed_at"),
5116
+ ...timestamps7()
5117
+ },
5118
+ (table) => [
5119
+ // Lookup path for info/approve/deny
5120
+ // Unique so a typed code can never address two records
5121
+ uniqueIndex3("device_authorization_user_code_idx").on(table.userCode)
5122
+ ]
5123
+ );
5124
+ }
5125
+ });
5126
+
5031
5127
  // src/server/entities/user-invitations.ts
5032
- import { text as text8, index as index8 } from "drizzle-orm/pg-core";
5033
- import { id as id8, timestamps as timestamps7, enumText as enumText5, utcTimestamp as utcTimestamp6, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
5128
+ import { text as text9, index as index8 } from "drizzle-orm/pg-core";
5129
+ import { id as id9, timestamps as timestamps8, enumText as enumText6, utcTimestamp as utcTimestamp7, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
5034
5130
  var userInvitations;
5035
5131
  var init_user_invitations = __esm({
5036
5132
  "src/server/entities/user-invitations.ts"() {
@@ -5043,14 +5139,14 @@ var init_user_invitations = __esm({
5043
5139
  "user_invitations",
5044
5140
  {
5045
5141
  // Primary key
5046
- id: id8(),
5142
+ id: id9(),
5047
5143
  // Target email address for the invitation
5048
5144
  // Will become the user's email upon acceptance
5049
- email: text8("email").notNull(),
5145
+ email: text9("email").notNull(),
5050
5146
  // Unique invitation token (UUID v4)
5051
5147
  // Used in invitation URL: /auth/invite/{token}
5052
5148
  // Single-use token that expires after acceptance
5053
- token: text8("token").notNull().unique(),
5149
+ token: text9("token").notNull().unique(),
5054
5150
  // Role to be assigned when invitation is accepted
5055
5151
  // Foreign key to roles table
5056
5152
  roleId: foreignKey5("role", () => roles.id),
@@ -5063,19 +5159,19 @@ var init_user_invitations = __esm({
5063
5159
  // - accepted: User accepted and account created
5064
5160
  // - expired: Invitation expired (automatic)
5065
5161
  // - cancelled: Invitation cancelled by admin
5066
- status: enumText5("status", INVITATION_STATUSES).default("pending").notNull(),
5162
+ status: enumText6("status", INVITATION_STATUSES).default("pending").notNull(),
5067
5163
  // Expiration timestamp (default: 7 days from creation)
5068
5164
  // Invitation cannot be accepted after this time
5069
5165
  // Background job should update status to 'expired'
5070
- expiresAt: utcTimestamp6("expires_at").notNull(),
5166
+ expiresAt: utcTimestamp7("expires_at").notNull(),
5071
5167
  // Timestamp when invitation was accepted
5072
5168
  // null = not yet accepted
5073
5169
  // Used for: audit trail, analytics
5074
- acceptedAt: utcTimestamp6("accepted_at"),
5170
+ acceptedAt: utcTimestamp7("accepted_at"),
5075
5171
  // Timestamp when invitation was cancelled
5076
5172
  // null = not cancelled
5077
5173
  // Used for: audit trail
5078
- cancelledAt: utcTimestamp6("cancelled_at"),
5174
+ cancelledAt: utcTimestamp7("cancelled_at"),
5079
5175
  // Additional metadata (JSONB)
5080
5176
  // Use cases:
5081
5177
  // - Custom welcome message
@@ -5084,7 +5180,7 @@ var init_user_invitations = __esm({
5084
5180
  // - Custom fields for app-specific data
5085
5181
  // Example: { message: "Welcome!", department: "Engineering" }
5086
5182
  metadata: typedJsonb2("metadata"),
5087
- ...timestamps7()
5183
+ ...timestamps8()
5088
5184
  },
5089
5185
  (table) => [
5090
5186
  // Indexes for query optimization
@@ -5101,9 +5197,9 @@ var init_user_invitations = __esm({
5101
5197
  });
5102
5198
 
5103
5199
  // src/server/entities/account-deletion-requests.ts
5104
- import { text as text9, index as index9, uniqueIndex as uniqueIndex3 } from "drizzle-orm/pg-core";
5200
+ import { text as text10, index as index9, uniqueIndex as uniqueIndex4 } from "drizzle-orm/pg-core";
5105
5201
  import { sql as sql2 } from "drizzle-orm";
5106
- import { id as id9, timestamps as timestamps8, enumText as enumText6, utcTimestamp as utcTimestamp7, optionalForeignKey } from "@spfn/core/db";
5202
+ import { id as id10, timestamps as timestamps9, enumText as enumText7, utcTimestamp as utcTimestamp8, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
5107
5203
  var accountDeletionRequests;
5108
5204
  var init_account_deletion_requests = __esm({
5109
5205
  "src/server/entities/account-deletion-requests.ts"() {
@@ -5114,32 +5210,32 @@ var init_account_deletion_requests = __esm({
5114
5210
  accountDeletionRequests = authSchema.table(
5115
5211
  "account_deletion_requests",
5116
5212
  {
5117
- id: id9(),
5213
+ id: id10(),
5118
5214
  // Foreign key to users table. `set null` (optionalForeignKey default) so this
5119
5215
  // row survives a hard-delete purge of the user it refers to.
5120
- userId: optionalForeignKey("user", () => users.id),
5216
+ userId: optionalForeignKey2("user", () => users.id),
5121
5217
  // Snapshot of the user's public UUID at request time — stays readable even
5122
5218
  // after userId is nulled out or the account is anonymized.
5123
- userPublicId: text9("user_public_id").notNull(),
5219
+ userPublicId: text10("user_public_id").notNull(),
5124
5220
  // When the deletion was requested
5125
- requestedAt: utcTimestamp7("requested_at").notNull().defaultNow(),
5221
+ requestedAt: utcTimestamp8("requested_at").notNull().defaultNow(),
5126
5222
  // When the purge job is allowed to run (requestedAt + grace period; equals
5127
5223
  // requestedAt itself for immediate/zero-grace deletions)
5128
- purgeScheduledAt: utcTimestamp7("purge_scheduled_at").notNull(),
5224
+ purgeScheduledAt: utcTimestamp8("purge_scheduled_at").notNull(),
5129
5225
  // Request lifecycle status
5130
5226
  // - pending: awaiting purgeScheduledAt (or immediate purge)
5131
5227
  // - cancelled: recovered before purge
5132
5228
  // - completed: purge ran
5133
- status: enumText6("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
5229
+ status: enumText7("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
5134
5230
  // Who initiated the request
5135
- requestedBy: enumText6("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
5231
+ requestedBy: enumText7("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
5136
5232
  // Optional free-text reason (self-service UI, admin note, DSR reference, ...)
5137
- reason: text9("reason"),
5138
- cancelledAt: utcTimestamp7("cancelled_at"),
5139
- completedAt: utcTimestamp7("completed_at"),
5233
+ reason: text10("reason"),
5234
+ cancelledAt: utcTimestamp8("cancelled_at"),
5235
+ completedAt: utcTimestamp8("completed_at"),
5140
5236
  // Purge strategy actually executed (set on completion; null while pending)
5141
- purgeStrategy: enumText6("purge_strategy", PURGE_STRATEGIES),
5142
- ...timestamps8()
5237
+ purgeStrategy: enumText7("purge_strategy", PURGE_STRATEGIES),
5238
+ ...timestamps9()
5143
5239
  },
5144
5240
  (table) => [
5145
5241
  index9("account_deletion_requests_user_id_idx").on(table.userId),
@@ -5147,7 +5243,7 @@ var init_account_deletion_requests = __esm({
5147
5243
  index9("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
5148
5244
  index9("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
5149
5245
  // Partial unique index: at most one pending request per user at a time.
5150
- uniqueIndex3("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5246
+ uniqueIndex4("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5151
5247
  ]
5152
5248
  );
5153
5249
  }
@@ -5299,8 +5395,8 @@ var init_rbac = __esm({
5299
5395
  });
5300
5396
 
5301
5397
  // src/server/entities/permissions.ts
5302
- import { text as text10, boolean as boolean4, index as index10 } from "drizzle-orm/pg-core";
5303
- import { id as id10, timestamps as timestamps9, enumText as enumText7, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5398
+ import { text as text11, boolean as boolean4, index as index10 } from "drizzle-orm/pg-core";
5399
+ import { id as id11, timestamps as timestamps10, enumText as enumText8, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5304
5400
  var permissions;
5305
5401
  var init_permissions = __esm({
5306
5402
  "src/server/entities/permissions.ts"() {
@@ -5311,7 +5407,7 @@ var init_permissions = __esm({
5311
5407
  "permissions",
5312
5408
  {
5313
5409
  // Primary key
5314
- id: id10(),
5410
+ id: id11(),
5315
5411
  // Permission identifier
5316
5412
  // Format: resource:action or namespace:resource:action
5317
5413
  // Examples:
@@ -5319,20 +5415,20 @@ var init_permissions = __esm({
5319
5415
  // - Namespaced: 'auth:user:delete', 'cms:post:publish'
5320
5416
  // Must be unique across all permissions
5321
5417
  // Used in: permission checks, role assignments, API guards
5322
- name: text10("name").notNull().unique(),
5418
+ name: text11("name").notNull().unique(),
5323
5419
  // Display name for UI
5324
5420
  // Human-readable name shown in admin panels
5325
5421
  // Example: "Delete Users", "Publish Posts"
5326
- displayName: text10("display_name").notNull(),
5422
+ displayName: text11("display_name").notNull(),
5327
5423
  // Permission description
5328
5424
  // Detailed explanation of what this permission allows
5329
5425
  // Example: "Allows deletion of user accounts from the system"
5330
- description: text10("description"),
5426
+ description: text11("description"),
5331
5427
  // Category for grouping
5332
5428
  // Used for: organizing permissions in UI, filtering
5333
5429
  // Built-in categories: auth, user, rbac, system
5334
5430
  // Custom categories: any app-specific category
5335
- category: enumText7("category", PERMISSION_CATEGORIES),
5431
+ category: enumText8("category", PERMISSION_CATEGORIES),
5336
5432
  // Built-in permission flag
5337
5433
  // true: Core package permissions (auth:*, user:*, rbac:*)
5338
5434
  // - Cannot be deleted or modified
@@ -5363,7 +5459,7 @@ var init_permissions = __esm({
5363
5459
  // - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
5364
5460
  // Example: { icon: 'trash', color: 'red', requiresMfa: true }
5365
5461
  metadata: typedJsonb3("metadata"),
5366
- ...timestamps9()
5462
+ ...timestamps10()
5367
5463
  },
5368
5464
  (table) => [
5369
5465
  index10("permissions_name_idx").on(table.name),
@@ -5378,7 +5474,7 @@ var init_permissions = __esm({
5378
5474
 
5379
5475
  // src/server/entities/role-permissions.ts
5380
5476
  import { index as index11, unique } from "drizzle-orm/pg-core";
5381
- import { id as id11, timestamps as timestamps10, foreignKey as foreignKey6 } from "@spfn/core/db";
5477
+ import { id as id12, timestamps as timestamps11, foreignKey as foreignKey6 } from "@spfn/core/db";
5382
5478
  var rolePermissions;
5383
5479
  var init_role_permissions = __esm({
5384
5480
  "src/server/entities/role-permissions.ts"() {
@@ -5390,7 +5486,7 @@ var init_role_permissions = __esm({
5390
5486
  "role_permissions",
5391
5487
  {
5392
5488
  // Primary key
5393
- id: id11(),
5489
+ id: id12(),
5394
5490
  // Role reference
5395
5491
  // Foreign key to roles table
5396
5492
  // Cascade delete: when role is deleted, all role-permission mappings are removed
@@ -5403,7 +5499,7 @@ var init_role_permissions = __esm({
5403
5499
  // Used for: granting permissions to roles
5404
5500
  // Example: user:delete permission → [Admin, Superadmin]
5405
5501
  permissionId: foreignKey6("permission", () => permissions.id, { onDelete: "cascade" }),
5406
- ...timestamps10()
5502
+ ...timestamps11()
5407
5503
  },
5408
5504
  (table) => [
5409
5505
  // Indexes for query performance
@@ -5417,8 +5513,8 @@ var init_role_permissions = __esm({
5417
5513
  });
5418
5514
 
5419
5515
  // src/server/entities/user-permissions.ts
5420
- import { boolean as boolean5, text as text11, index as index12, unique as unique2 } from "drizzle-orm/pg-core";
5421
- import { id as id12, timestamps as timestamps11, utcTimestamp as utcTimestamp8, foreignKey as foreignKey7 } from "@spfn/core/db";
5516
+ import { boolean as boolean5, text as text12, index as index12, unique as unique2 } from "drizzle-orm/pg-core";
5517
+ import { id as id13, timestamps as timestamps12, utcTimestamp as utcTimestamp9, foreignKey as foreignKey7 } from "@spfn/core/db";
5422
5518
  var userPermissions;
5423
5519
  var init_user_permissions = __esm({
5424
5520
  "src/server/entities/user-permissions.ts"() {
@@ -5430,7 +5526,7 @@ var init_user_permissions = __esm({
5430
5526
  "user_permissions",
5431
5527
  {
5432
5528
  // Primary key
5433
- id: id12(),
5529
+ id: id13(),
5434
5530
  // User reference
5435
5531
  // Foreign key to users table
5436
5532
  // Cascade delete: when user is deleted, all overrides are removed
@@ -5453,13 +5549,13 @@ var init_user_permissions = __esm({
5453
5549
  // Reason for grant/revocation
5454
5550
  // Used for: audit trail, compliance documentation
5455
5551
  // Example: "Temporary access for project X", "Security incident - restricted"
5456
- reason: text11("reason"),
5552
+ reason: text12("reason"),
5457
5553
  // Expiration timestamp (optional)
5458
5554
  // null: Permanent override (remains until manually removed)
5459
5555
  // timestamp: Permission expires at this time (auto-revoked by background job)
5460
5556
  // Use case: Time-limited elevated access, temporary restrictions
5461
- expiresAt: utcTimestamp8("expires_at"),
5462
- ...timestamps11()
5557
+ expiresAt: utcTimestamp9("expires_at"),
5558
+ ...timestamps12()
5463
5559
  },
5464
5560
  (table) => [
5465
5561
  // Indexes for query performance
@@ -5475,7 +5571,7 @@ var init_user_permissions = __esm({
5475
5571
 
5476
5572
  // src/server/entities/auth-metadata.ts
5477
5573
  import { sql as sql3 } from "drizzle-orm";
5478
- import { text as text12, timestamp } from "drizzle-orm/pg-core";
5574
+ import { text as text13, timestamp } from "drizzle-orm/pg-core";
5479
5575
  var authMetadata;
5480
5576
  var init_auth_metadata = __esm({
5481
5577
  "src/server/entities/auth-metadata.ts"() {
@@ -5485,9 +5581,9 @@ var init_auth_metadata = __esm({
5485
5581
  "auth_metadata",
5486
5582
  {
5487
5583
  // Metadata key (primary key)
5488
- key: text12("key").primaryKey(),
5584
+ key: text13("key").primaryKey(),
5489
5585
  // Metadata value
5490
- value: text12("value").notNull(),
5586
+ value: text13("value").notNull(),
5491
5587
  // Last updated timestamp — stamped by the database on insert and on update
5492
5588
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
5493
5589
  }
@@ -5496,8 +5592,8 @@ var init_auth_metadata = __esm({
5496
5592
  });
5497
5593
 
5498
5594
  // src/server/entities/ops-tokens.ts
5499
- import { text as text13 } from "drizzle-orm/pg-core";
5500
- import { id as id13, timestamps as timestamps12, utcTimestamp as utcTimestamp9 } from "@spfn/core/db";
5595
+ import { text as text14 } from "drizzle-orm/pg-core";
5596
+ import { id as id14, timestamps as timestamps13, utcTimestamp as utcTimestamp10 } from "@spfn/core/db";
5501
5597
  var opsTokens;
5502
5598
  var init_ops_tokens = __esm({
5503
5599
  "src/server/entities/ops-tokens.ts"() {
@@ -5506,22 +5602,22 @@ var init_ops_tokens = __esm({
5506
5602
  opsTokens = authSchema.table(
5507
5603
  "ops_tokens",
5508
5604
  {
5509
- id: id13(),
5605
+ id: id14(),
5510
5606
  // Operator-facing label ("ci-deploy", "rayim-laptop")
5511
- name: text13("name").notNull(),
5607
+ name: text14("name").notNull(),
5512
5608
  // SHA-256 hex of the token secret. Lookup key — the secret never lands
5513
5609
  // here, and the unique constraint doubles as the lookup index.
5514
- tokenHash: text13("token_hash").notNull().unique(),
5610
+ tokenHash: text14("token_hash").notNull().unique(),
5515
5611
  // Granted scopes as permission strings ('waitlist:read', ...).
5516
5612
  // '*' grants every scope.
5517
- scopes: text13("scopes").array().notNull(),
5613
+ scopes: text14("scopes").array().notNull(),
5518
5614
  // null = the token does not expire
5519
- expiresAt: utcTimestamp9("expires_at"),
5615
+ expiresAt: utcTimestamp10("expires_at"),
5520
5616
  // null = active; a timestamp revokes the token permanently
5521
- revokedAt: utcTimestamp9("revoked_at"),
5617
+ revokedAt: utcTimestamp10("revoked_at"),
5522
5618
  // Last successful verification, updated fire-and-forget
5523
- lastUsedAt: utcTimestamp9("last_used_at"),
5524
- ...timestamps12()
5619
+ lastUsedAt: utcTimestamp10("last_used_at"),
5620
+ ...timestamps13()
5525
5621
  }
5526
5622
  );
5527
5623
  }
@@ -5538,6 +5634,7 @@ var init_entities = __esm({
5538
5634
  init_user_social_accounts();
5539
5635
  init_verification_codes();
5540
5636
  init_signup_link_tokens();
5637
+ init_device_authorizations();
5541
5638
  init_user_invitations();
5542
5639
  init_account_deletion_requests();
5543
5640
  init_roles();
@@ -5564,8 +5661,8 @@ var init_users_repository = __esm({
5564
5661
  * ID로 사용자 조회
5565
5662
  * Read replica 사용
5566
5663
  */
5567
- async findById(id14) {
5568
- const result = await this.readDb.select().from(users).where(eq(users.id, id14)).limit(1);
5664
+ async findById(id15) {
5665
+ const result = await this.readDb.select().from(users).where(eq(users.id, id15)).limit(1);
5569
5666
  return result[0] ?? null;
5570
5667
  }
5571
5668
  /**
@@ -5575,8 +5672,8 @@ var init_users_repository = __esm({
5575
5672
  * 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
5576
5673
  * 계속 사용할 것.
5577
5674
  */
5578
- async findByIdOnPrimary(id14) {
5579
- const result = await this.db.select().from(users).where(eq(users.id, id14)).limit(1);
5675
+ async findByIdOnPrimary(id15) {
5676
+ const result = await this.db.select().from(users).where(eq(users.id, id15)).limit(1);
5580
5677
  return result[0] ?? null;
5581
5678
  }
5582
5679
  /**
@@ -5647,13 +5744,13 @@ var init_users_repository = __esm({
5647
5744
  *
5648
5745
  * roleId가 null인 유저는 role: null 반환
5649
5746
  */
5650
- async findByIdWithRole(id14) {
5747
+ async findByIdWithRole(id15) {
5651
5748
  const result = await this.readDb.select({
5652
5749
  user: users,
5653
5750
  roleName: roles.name,
5654
5751
  roleDisplayName: roles.displayName,
5655
5752
  rolePriority: roles.priority
5656
- }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id14)).limit(1);
5753
+ }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id15)).limit(1);
5657
5754
  const row = result[0];
5658
5755
  if (!row) {
5659
5756
  return null;
@@ -5728,9 +5825,9 @@ var init_users_repository = __esm({
5728
5825
  * 사용자 정보 업데이트
5729
5826
  * Write primary 사용
5730
5827
  */
5731
- async updateById(id14, data) {
5828
+ async updateById(id15, data) {
5732
5829
  const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
5733
- const result = await this.db.update(users).set(patch).where(eq(users.id, id14)).returning();
5830
+ const result = await this.db.update(users).set(patch).where(eq(users.id, id15)).returning();
5734
5831
  return result[0] ?? null;
5735
5832
  }
5736
5833
  /**
@@ -5742,10 +5839,10 @@ var init_users_repository = __esm({
5742
5839
  * status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
5743
5840
  * Write primary 사용
5744
5841
  */
5745
- async reactivateFromPendingDeletion(id14) {
5842
+ async reactivateFromPendingDeletion(id15) {
5746
5843
  const result = await this.db.update(users).set({ status: "active" }).where(
5747
5844
  and(
5748
- eq(users.id, id14),
5845
+ eq(users.id, id15),
5749
5846
  eq(users.status, "pending_deletion")
5750
5847
  )
5751
5848
  ).returning();
@@ -5755,32 +5852,32 @@ var init_users_repository = __esm({
5755
5852
  * 비밀번호 업데이트
5756
5853
  * Write primary 사용
5757
5854
  */
5758
- async updatePassword(id14, passwordHash, clearPasswordChangeRequired = true) {
5855
+ async updatePassword(id15, passwordHash, clearPasswordChangeRequired = true) {
5759
5856
  const updateData = {
5760
5857
  passwordHash
5761
5858
  };
5762
5859
  if (clearPasswordChangeRequired) {
5763
5860
  updateData.passwordChangeRequired = false;
5764
5861
  }
5765
- const result = await this.db.update(users).set(updateData).where(eq(users.id, id14)).returning();
5862
+ const result = await this.db.update(users).set(updateData).where(eq(users.id, id15)).returning();
5766
5863
  return result[0] ?? null;
5767
5864
  }
5768
5865
  /**
5769
5866
  * 마지막 로그인 시간 업데이트
5770
5867
  * Write primary 사용
5771
5868
  */
5772
- async updateLastLogin(id14) {
5869
+ async updateLastLogin(id15) {
5773
5870
  const result = await this.db.update(users).set({
5774
5871
  lastLoginAt: /* @__PURE__ */ new Date()
5775
- }).where(eq(users.id, id14)).returning();
5872
+ }).where(eq(users.id, id15)).returning();
5776
5873
  return result[0] ?? null;
5777
5874
  }
5778
5875
  /**
5779
5876
  * 사용자 삭제
5780
5877
  * Write primary 사용
5781
5878
  */
5782
- async deleteById(id14) {
5783
- const result = await this.db.delete(users).where(eq(users.id, id14)).returning();
5879
+ async deleteById(id15) {
5880
+ const result = await this.db.delete(users).where(eq(users.id, id15)).returning();
5784
5881
  return result[0] ?? null;
5785
5882
  }
5786
5883
  /**
@@ -6128,14 +6225,14 @@ var init_keys_repository = __esm({
6128
6225
  * stored, so it answers "since when has this device been on this release"
6129
6226
  * rather than "when was it last seen", which lastUsedAt already answers.
6130
6227
  */
6131
- async updateLastUsedById(id14, identity) {
6228
+ async updateLastUsedById(id15, identity) {
6132
6229
  const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
6133
6230
  const lastUsedIsStale = or(
6134
6231
  isNull(userPublicKeys.lastUsedAt),
6135
6232
  lt(userPublicKeys.lastUsedAt, staleBefore)
6136
6233
  );
6137
6234
  if (!identity) {
6138
- await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id14), lastUsedIsStale));
6235
+ await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id15), lastUsedIsStale));
6139
6236
  return;
6140
6237
  }
6141
6238
  const identityChanged = sql5`(
@@ -6152,7 +6249,7 @@ var init_keys_repository = __esm({
6152
6249
  clientContractVersion: identity.contractVersion,
6153
6250
  clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
6154
6251
  }).where(and2(
6155
- eq2(userPublicKeys.id, id14),
6252
+ eq2(userPublicKeys.id, id15),
6156
6253
  or(lastUsedIsStale, identityChanged)
6157
6254
  ));
6158
6255
  }
@@ -6191,8 +6288,8 @@ var init_verification_codes_repository = __esm({
6191
6288
  * ID로 인증 코드 조회
6192
6289
  * Read replica 사용
6193
6290
  */
6194
- async findById(id14) {
6195
- const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id14)).limit(1);
6291
+ async findById(id15) {
6292
+ const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id15)).limit(1);
6196
6293
  return result[0] ?? null;
6197
6294
  }
6198
6295
  /**
@@ -6206,22 +6303,22 @@ var init_verification_codes_repository = __esm({
6206
6303
  * 인증 코드 사용 처리
6207
6304
  * Write primary 사용
6208
6305
  */
6209
- async markAsUsed(id14) {
6306
+ async markAsUsed(id15) {
6210
6307
  const result = await this.db.update(verificationCodes).set({
6211
6308
  usedAt: /* @__PURE__ */ new Date()
6212
- }).where(eq3(verificationCodes.id, id14)).returning();
6309
+ }).where(eq3(verificationCodes.id, id15)).returning();
6213
6310
  return result[0] ?? null;
6214
6311
  }
6215
6312
  /**
6216
6313
  * 시도 횟수 증가
6217
6314
  * Write primary 사용
6218
6315
  */
6219
- async incrementAttempts(id14) {
6220
- const code = await this.findById(id14);
6316
+ async incrementAttempts(id15) {
6317
+ const code = await this.findById(id15);
6221
6318
  if (!code) return null;
6222
6319
  const result = await this.db.update(verificationCodes).set({
6223
6320
  attempts: code.attempts + 1
6224
- }).where(eq3(verificationCodes.id, id14)).returning();
6321
+ }).where(eq3(verificationCodes.id, id15)).returning();
6225
6322
  return result[0] ?? null;
6226
6323
  }
6227
6324
  /**
@@ -6309,14 +6406,14 @@ var init_signup_link_tokens_repository = __esm({
6309
6406
  *
6310
6407
  * @returns the updated row, or null if another request claimed it first
6311
6408
  */
6312
- async claimLink(id14, setupSecretHash, setupExpiresAt) {
6409
+ async claimLink(id15, setupSecretHash, setupExpiresAt) {
6313
6410
  const result = await this.db.update(signupLinkTokens).set({
6314
6411
  consumedAt: /* @__PURE__ */ new Date(),
6315
6412
  setupSecretHash,
6316
6413
  setupExpiresAt
6317
6414
  }).where(
6318
6415
  and4(
6319
- eq4(signupLinkTokens.id, id14),
6416
+ eq4(signupLinkTokens.id, id15),
6320
6417
  isNull3(signupLinkTokens.consumedAt),
6321
6418
  isNull3(signupLinkTokens.supersededAt)
6322
6419
  )
@@ -6329,10 +6426,10 @@ var init_signup_link_tokens_repository = __esm({
6329
6426
  *
6330
6427
  * @returns the updated row, or null if another request completed it first
6331
6428
  */
6332
- async claimSetupSession(id14) {
6429
+ async claimSetupSession(id15) {
6333
6430
  const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
6334
6431
  and4(
6335
- eq4(signupLinkTokens.id, id14),
6432
+ eq4(signupLinkTokens.id, id15),
6336
6433
  isNull3(signupLinkTokens.completedAt),
6337
6434
  isNull3(signupLinkTokens.supersededAt)
6338
6435
  )
@@ -6374,27 +6471,172 @@ var init_signup_link_tokens_repository = __esm({
6374
6471
  }
6375
6472
  });
6376
6473
 
6377
- // src/server/repositories/roles.repository.ts
6474
+ // src/server/repositories/device-authorizations.repository.ts
6378
6475
  import { BaseRepository as BaseRepository5 } from "@spfn/core/db";
6379
- import { eq as eq5, asc } from "drizzle-orm";
6476
+ import { eq as eq5, and as and5, gt as gt2, inArray, sql as sql6 } from "drizzle-orm";
6477
+ var notExpired, DeviceAuthorizationsRepository, deviceAuthorizationsRepository;
6478
+ var init_device_authorizations_repository = __esm({
6479
+ "src/server/repositories/device-authorizations.repository.ts"() {
6480
+ "use strict";
6481
+ init_device_authorizations();
6482
+ notExpired = () => gt2(deviceAuthorizations.expiresAt, sql6`now()`);
6483
+ DeviceAuthorizationsRepository = class extends BaseRepository5 {
6484
+ /**
6485
+ * Insert a pending authorization, unless one of its codes is already taken.
6486
+ *
6487
+ * `onConflictDoNothing` rather than letting the unique index raise: the start
6488
+ * route runs inside a transaction, and a raised unique violation aborts it,
6489
+ * so the retry that a generated-code collision calls for could not run — every
6490
+ * statement after it would fail with "current transaction is aborted" instead.
6491
+ * An empty result is the collision signal, and the caller draws a fresh code.
6492
+ *
6493
+ * Write primary.
6494
+ *
6495
+ * @returns the inserted row, or null if either code collided
6496
+ */
6497
+ async create(data) {
6498
+ const result = await this.db.insert(deviceAuthorizations).values(data).onConflictDoNothing().returning();
6499
+ return result[0] ?? null;
6500
+ }
6501
+ /**
6502
+ * Find a record by its normalized user code, in any state.
6503
+ *
6504
+ * Deliberately unfiltered: which refusal a caller is owed — expired, already
6505
+ * handled, unknown — is the service's decision, and a row filtered out here
6506
+ * would be indistinguishable from a code that was never issued.
6507
+ *
6508
+ * Read replica.
6509
+ */
6510
+ async findByUserCode(userCode) {
6511
+ const result = await this.readDb.select().from(deviceAuthorizations).where(eq5(deviceAuthorizations.userCode, userCode)).limit(1);
6512
+ return result[0] ?? null;
6513
+ }
6514
+ /**
6515
+ * Find a record by the hash of a device code, in any state.
6516
+ * Unfiltered for the same reason as `findByUserCode`.
6517
+ *
6518
+ * Read replica.
6519
+ */
6520
+ async findByDeviceCodeHash(deviceCodeHash) {
6521
+ const result = await this.readDb.select().from(deviceAuthorizations).where(eq5(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
6522
+ return result[0] ?? null;
6523
+ }
6524
+ /**
6525
+ * Bind the approving user and move the record to `approved`, but only from
6526
+ * `pending`.
6527
+ *
6528
+ * `userId` comes from the approver's authenticated session — never from a
6529
+ * request body — so this is the point where the record gains an owner.
6530
+ *
6531
+ * @returns the updated row, or null if it was no longer pending, or expired
6532
+ */
6533
+ async approve(id15, userId) {
6534
+ const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
6535
+ and5(
6536
+ eq5(deviceAuthorizations.id, id15),
6537
+ eq5(deviceAuthorizations.status, "pending"),
6538
+ notExpired()
6539
+ )
6540
+ ).returning();
6541
+ return result[0] ?? null;
6542
+ }
6543
+ /**
6544
+ * Move the record to `denied`, but only from `pending`.
6545
+ *
6546
+ * No user is bound: a refusal should leave no record of who was asked.
6547
+ *
6548
+ * @returns the updated row, or null if it was no longer pending, or expired
6549
+ */
6550
+ async deny(id15) {
6551
+ const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
6552
+ and5(
6553
+ eq5(deviceAuthorizations.id, id15),
6554
+ eq5(deviceAuthorizations.status, "pending"),
6555
+ notExpired()
6556
+ )
6557
+ ).returning();
6558
+ return result[0] ?? null;
6559
+ }
6560
+ /**
6561
+ * Refuse every authorization a user still has in flight.
6562
+ *
6563
+ * This is the device-code half of a global revocation. Revoking
6564
+ * `user_public_keys` alone leaves an approved-but-uncollected record behind,
6565
+ * and the next poll on it registers a brand-new active key — so "sign every
6566
+ * device out" would hand one straight back to whoever was still waiting,
6567
+ * which is exactly the person a revoke-all is usually aimed at.
6568
+ *
6569
+ * `denied` rather than a status of its own: a swept record owes its holder
6570
+ * the answer a refused one owes, on all four operations — the waiting device
6571
+ * is told no and stops polling, and info/approve/deny say the request was
6572
+ * already answered. Adding a fourth live status would add a migration and a
6573
+ * case-table row to record a distinction nothing acts on.
6574
+ *
6575
+ * Expired rows are swept too. A global revoke is not the place to reason
6576
+ * about which dead rows were about to die anyway.
6577
+ *
6578
+ * `pending` alongside `approved` even though a pending row carries no
6579
+ * `userId` today, so only approved rows can match: the state list says which
6580
+ * states this is meant to close, and a later change that binds the user
6581
+ * earlier should not silently reopen the hole.
6582
+ *
6583
+ * @returns the rows this call refused
6584
+ */
6585
+ async denyAllActiveByUserId(userId) {
6586
+ return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
6587
+ and5(
6588
+ eq5(deviceAuthorizations.userId, userId),
6589
+ inArray(deviceAuthorizations.status, ["pending", "approved"])
6590
+ )
6591
+ ).returning();
6592
+ }
6593
+ /**
6594
+ * Spend an approved record, but only from `approved`, and address it by the
6595
+ * device code hash the caller actually presented.
6596
+ *
6597
+ * This is the one-shot: the winner of two concurrent polls registers the key,
6598
+ * and the loser sees no approved record and is answered as if the code were
6599
+ * unknown. Matching on the hash rather than on an id read a moment ago keeps
6600
+ * the whole decision in one statement.
6601
+ *
6602
+ * @returns the spent row, or null if it was not approved (any more), or expired
6603
+ */
6604
+ async consumeApproved(deviceCodeHash) {
6605
+ const result = await this.db.update(deviceAuthorizations).set({ status: "consumed", consumedAt: /* @__PURE__ */ new Date() }).where(
6606
+ and5(
6607
+ eq5(deviceAuthorizations.deviceCodeHash, deviceCodeHash),
6608
+ eq5(deviceAuthorizations.status, "approved"),
6609
+ notExpired()
6610
+ )
6611
+ ).returning();
6612
+ return result[0] ?? null;
6613
+ }
6614
+ };
6615
+ deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
6616
+ }
6617
+ });
6618
+
6619
+ // src/server/repositories/roles.repository.ts
6620
+ import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
6621
+ import { eq as eq6, asc } from "drizzle-orm";
6380
6622
  var RolesRepository, rolesRepository;
6381
6623
  var init_roles_repository = __esm({
6382
6624
  "src/server/repositories/roles.repository.ts"() {
6383
6625
  "use strict";
6384
6626
  init_roles();
6385
- RolesRepository = class extends BaseRepository5 {
6627
+ RolesRepository = class extends BaseRepository6 {
6386
6628
  /**
6387
6629
  * ID로 역할 조회
6388
6630
  */
6389
- async findById(id14) {
6390
- const result = await this.readDb.select().from(roles).where(eq5(roles.id, id14)).limit(1);
6631
+ async findById(id15) {
6632
+ const result = await this.readDb.select().from(roles).where(eq6(roles.id, id15)).limit(1);
6391
6633
  return result[0] ?? null;
6392
6634
  }
6393
6635
  /**
6394
6636
  * Name으로 역할 조회
6395
6637
  */
6396
6638
  async findByName(name) {
6397
- const result = await this.readDb.select().from(roles).where(eq5(roles.name, name)).limit(1);
6639
+ const result = await this.readDb.select().from(roles).where(eq6(roles.name, name)).limit(1);
6398
6640
  return result[0] ?? null;
6399
6641
  }
6400
6642
  /**
@@ -6407,7 +6649,7 @@ var init_roles_repository = __esm({
6407
6649
  * 활성 역할만 조회
6408
6650
  */
6409
6651
  async findActive() {
6410
- return this.readDb.select().from(roles).where(eq5(roles.isActive, true)).orderBy(asc(roles.priority));
6652
+ return this.readDb.select().from(roles).where(eq6(roles.isActive, true)).orderBy(asc(roles.priority));
6411
6653
  }
6412
6654
  /**
6413
6655
  * 역할 생성
@@ -6418,15 +6660,15 @@ var init_roles_repository = __esm({
6418
6660
  /**
6419
6661
  * 역할 업데이트
6420
6662
  */
6421
- async updateById(id14, data) {
6422
- const result = await this.db.update(roles).set(data).where(eq5(roles.id, id14)).returning();
6663
+ async updateById(id15, data) {
6664
+ const result = await this.db.update(roles).set(data).where(eq6(roles.id, id15)).returning();
6423
6665
  return result[0] ?? null;
6424
6666
  }
6425
6667
  /**
6426
6668
  * 역할 삭제
6427
6669
  */
6428
- async deleteById(id14) {
6429
- const result = await this.db.delete(roles).where(eq5(roles.id, id14)).returning();
6670
+ async deleteById(id15) {
6671
+ const result = await this.db.delete(roles).where(eq6(roles.id, id15)).returning();
6430
6672
  return result[0] ?? null;
6431
6673
  }
6432
6674
  };
@@ -6435,26 +6677,26 @@ var init_roles_repository = __esm({
6435
6677
  });
6436
6678
 
6437
6679
  // src/server/repositories/permissions.repository.ts
6438
- import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
6439
- import { asc as asc2, eq as eq6, inArray } from "drizzle-orm";
6680
+ import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
6681
+ import { asc as asc2, eq as eq7, inArray as inArray2 } from "drizzle-orm";
6440
6682
  var PermissionsRepository, permissionsRepository;
6441
6683
  var init_permissions_repository = __esm({
6442
6684
  "src/server/repositories/permissions.repository.ts"() {
6443
6685
  "use strict";
6444
6686
  init_permissions();
6445
- PermissionsRepository = class extends BaseRepository6 {
6687
+ PermissionsRepository = class extends BaseRepository7 {
6446
6688
  /**
6447
6689
  * ID로 권한 조회
6448
6690
  */
6449
- async findById(id14) {
6450
- const result = await this.readDb.select().from(permissions).where(eq6(permissions.id, id14)).limit(1);
6691
+ async findById(id15) {
6692
+ const result = await this.readDb.select().from(permissions).where(eq7(permissions.id, id15)).limit(1);
6451
6693
  return result[0] ?? null;
6452
6694
  }
6453
6695
  /**
6454
6696
  * Name으로 권한 조회
6455
6697
  */
6456
6698
  async findByName(name) {
6457
- const result = await this.readDb.select().from(permissions).where(eq6(permissions.name, name)).limit(1);
6699
+ const result = await this.readDb.select().from(permissions).where(eq7(permissions.name, name)).limit(1);
6458
6700
  return result[0] ?? null;
6459
6701
  }
6460
6702
  /**
@@ -6462,7 +6704,7 @@ var init_permissions_repository = __esm({
6462
6704
  */
6463
6705
  async findByNames(names) {
6464
6706
  if (names.length === 0) return [];
6465
- return this.readDb.select().from(permissions).where(inArray(permissions.name, names));
6707
+ return this.readDb.select().from(permissions).where(inArray2(permissions.name, names));
6466
6708
  }
6467
6709
  /**
6468
6710
  * 모든 권한 조회
@@ -6474,13 +6716,13 @@ var init_permissions_repository = __esm({
6474
6716
  * 활성 권한만 조회
6475
6717
  */
6476
6718
  async findActive() {
6477
- return this.readDb.select().from(permissions).where(eq6(permissions.isActive, true)).orderBy(asc2(permissions.name));
6719
+ return this.readDb.select().from(permissions).where(eq7(permissions.isActive, true)).orderBy(asc2(permissions.name));
6478
6720
  }
6479
6721
  /**
6480
6722
  * 카테고리별 권한 조회
6481
6723
  */
6482
6724
  async findByCategory(category) {
6483
- return this.readDb.select().from(permissions).where(eq6(permissions.category, category)).orderBy(asc2(permissions.name));
6725
+ return this.readDb.select().from(permissions).where(eq7(permissions.category, category)).orderBy(asc2(permissions.name));
6484
6726
  }
6485
6727
  /**
6486
6728
  * 권한 생성
@@ -6498,15 +6740,15 @@ var init_permissions_repository = __esm({
6498
6740
  /**
6499
6741
  * 권한 업데이트
6500
6742
  */
6501
- async updateById(id14, data) {
6502
- const result = await this.db.update(permissions).set(data).where(eq6(permissions.id, id14)).returning();
6743
+ async updateById(id15, data) {
6744
+ const result = await this.db.update(permissions).set(data).where(eq7(permissions.id, id15)).returning();
6503
6745
  return result[0] ?? null;
6504
6746
  }
6505
6747
  /**
6506
6748
  * 권한 삭제
6507
6749
  */
6508
- async deleteById(id14) {
6509
- const result = await this.db.delete(permissions).where(eq6(permissions.id, id14)).returning();
6750
+ async deleteById(id15) {
6751
+ const result = await this.db.delete(permissions).where(eq7(permissions.id, id15)).returning();
6510
6752
  return result[0] ?? null;
6511
6753
  }
6512
6754
  };
@@ -6515,25 +6757,25 @@ var init_permissions_repository = __esm({
6515
6757
  });
6516
6758
 
6517
6759
  // src/server/repositories/role-permissions.repository.ts
6518
- import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
6519
- import { and as and5, eq as eq7 } from "drizzle-orm";
6760
+ import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
6761
+ import { and as and6, eq as eq8 } from "drizzle-orm";
6520
6762
  var RolePermissionsRepository, rolePermissionsRepository;
6521
6763
  var init_role_permissions_repository = __esm({
6522
6764
  "src/server/repositories/role-permissions.repository.ts"() {
6523
6765
  "use strict";
6524
6766
  init_role_permissions();
6525
- RolePermissionsRepository = class extends BaseRepository7 {
6767
+ RolePermissionsRepository = class extends BaseRepository8 {
6526
6768
  /**
6527
6769
  * 역할 ID로 모든 권한 조회
6528
6770
  */
6529
6771
  async findByRoleId(roleId) {
6530
- return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.roleId, roleId));
6772
+ return this.readDb.select().from(rolePermissions).where(eq8(rolePermissions.roleId, roleId));
6531
6773
  }
6532
6774
  /**
6533
6775
  * 권한 ID로 모든 역할 조회
6534
6776
  */
6535
6777
  async findByPermissionId(permissionId) {
6536
- return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.permissionId, permissionId));
6778
+ return this.readDb.select().from(rolePermissions).where(eq8(rolePermissions.permissionId, permissionId));
6537
6779
  }
6538
6780
  /**
6539
6781
  * 역할-권한 매핑 생성
@@ -6553,9 +6795,9 @@ var init_role_permissions_repository = __esm({
6553
6795
  */
6554
6796
  async deleteByRoleIdAndPermissionId(roleId, permissionId) {
6555
6797
  const result = await this.db.delete(rolePermissions).where(
6556
- and5(
6557
- eq7(rolePermissions.roleId, roleId),
6558
- eq7(rolePermissions.permissionId, permissionId)
6798
+ and6(
6799
+ eq8(rolePermissions.roleId, roleId),
6800
+ eq8(rolePermissions.permissionId, permissionId)
6559
6801
  )
6560
6802
  ).returning();
6561
6803
  return result[0] ?? null;
@@ -6564,7 +6806,7 @@ var init_role_permissions_repository = __esm({
6564
6806
  * 역할의 모든 권한 매핑 삭제
6565
6807
  */
6566
6808
  async deleteByRoleId(roleId) {
6567
- const result = await this.db.delete(rolePermissions).where(eq7(rolePermissions.roleId, roleId)).returning();
6809
+ const result = await this.db.delete(rolePermissions).where(eq8(rolePermissions.roleId, roleId)).returning();
6568
6810
  return result.length;
6569
6811
  }
6570
6812
  /**
@@ -6585,19 +6827,19 @@ var init_role_permissions_repository = __esm({
6585
6827
  });
6586
6828
 
6587
6829
  // src/server/repositories/user-permissions.repository.ts
6588
- import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
6589
- import { eq as eq8, and as and6, or as or2, isNull as isNull4, isNotNull, lt as lt3, gt as gt2 } from "drizzle-orm";
6830
+ import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
6831
+ import { eq as eq9, and as and7, or as or2, isNull as isNull4, isNotNull, lt as lt3, gt as gt3 } from "drizzle-orm";
6590
6832
  var UserPermissionsRepository, userPermissionsRepository;
6591
6833
  var init_user_permissions_repository = __esm({
6592
6834
  "src/server/repositories/user-permissions.repository.ts"() {
6593
6835
  "use strict";
6594
6836
  init_user_permissions();
6595
- UserPermissionsRepository = class extends BaseRepository8 {
6837
+ UserPermissionsRepository = class extends BaseRepository9 {
6596
6838
  /**
6597
6839
  * 사용자 ID로 모든 권한 오버라이드 조회
6598
6840
  */
6599
6841
  async findByUserId(userId) {
6600
- return this.readDb.select().from(userPermissions).where(eq8(userPermissions.userId, userId));
6842
+ return this.readDb.select().from(userPermissions).where(eq9(userPermissions.userId, userId));
6601
6843
  }
6602
6844
  /**
6603
6845
  * 사용자 ID로 유효한 권한 오버라이드만 조회
@@ -6606,11 +6848,11 @@ var init_user_permissions_repository = __esm({
6606
6848
  async findValidByUserId(userId) {
6607
6849
  const now = /* @__PURE__ */ new Date();
6608
6850
  return this.readDb.select().from(userPermissions).where(
6609
- and6(
6610
- eq8(userPermissions.userId, userId),
6851
+ and7(
6852
+ eq9(userPermissions.userId, userId),
6611
6853
  or2(
6612
6854
  isNull4(userPermissions.expiresAt),
6613
- gt2(userPermissions.expiresAt, now)
6855
+ gt3(userPermissions.expiresAt, now)
6614
6856
  )
6615
6857
  )
6616
6858
  );
@@ -6620,9 +6862,9 @@ var init_user_permissions_repository = __esm({
6620
6862
  */
6621
6863
  async findByUserIdAndPermissionId(userId, permissionId) {
6622
6864
  const result = await this.readDb.select().from(userPermissions).where(
6623
- and6(
6624
- eq8(userPermissions.userId, userId),
6625
- eq8(userPermissions.permissionId, permissionId)
6865
+ and7(
6866
+ eq9(userPermissions.userId, userId),
6867
+ eq9(userPermissions.permissionId, permissionId)
6626
6868
  )
6627
6869
  ).limit(1);
6628
6870
  return result[0] ?? null;
@@ -6636,8 +6878,8 @@ var init_user_permissions_repository = __esm({
6636
6878
  /**
6637
6879
  * 사용자 권한 오버라이드 업데이트
6638
6880
  */
6639
- async updateById(id14, data) {
6640
- const result = await this.db.update(userPermissions).set(data).where(eq8(userPermissions.id, id14)).returning();
6881
+ async updateById(id15, data) {
6882
+ const result = await this.db.update(userPermissions).set(data).where(eq9(userPermissions.id, id15)).returning();
6641
6883
  return result[0] ?? null;
6642
6884
  }
6643
6885
  /**
@@ -6645,9 +6887,9 @@ var init_user_permissions_repository = __esm({
6645
6887
  */
6646
6888
  async deleteByUserIdAndPermissionId(userId, permissionId) {
6647
6889
  const result = await this.db.delete(userPermissions).where(
6648
- and6(
6649
- eq8(userPermissions.userId, userId),
6650
- eq8(userPermissions.permissionId, permissionId)
6890
+ and7(
6891
+ eq9(userPermissions.userId, userId),
6892
+ eq9(userPermissions.permissionId, permissionId)
6651
6893
  )
6652
6894
  ).returning();
6653
6895
  return result[0] ?? null;
@@ -6656,7 +6898,7 @@ var init_user_permissions_repository = __esm({
6656
6898
  * 사용자의 모든 권한 오버라이드 삭제
6657
6899
  */
6658
6900
  async deleteByUserId(userId) {
6659
- const result = await this.db.delete(userPermissions).where(eq8(userPermissions.userId, userId)).returning();
6901
+ const result = await this.db.delete(userPermissions).where(eq9(userPermissions.userId, userId)).returning();
6660
6902
  return result.length;
6661
6903
  }
6662
6904
  /**
@@ -6665,7 +6907,7 @@ var init_user_permissions_repository = __esm({
6665
6907
  async deleteExpired() {
6666
6908
  const now = /* @__PURE__ */ new Date();
6667
6909
  const result = await this.db.delete(userPermissions).where(
6668
- and6(
6910
+ and7(
6669
6911
  isNotNull(userPermissions.expiresAt),
6670
6912
  lt3(userPermissions.expiresAt, now)
6671
6913
  )
@@ -6678,33 +6920,33 @@ var init_user_permissions_repository = __esm({
6678
6920
  });
6679
6921
 
6680
6922
  // src/server/repositories/user-profiles.repository.ts
6681
- import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
6682
- import { eq as eq9 } from "drizzle-orm";
6923
+ import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
6924
+ import { eq as eq10 } from "drizzle-orm";
6683
6925
  var UserProfilesRepository, userProfilesRepository;
6684
6926
  var init_user_profiles_repository = __esm({
6685
6927
  "src/server/repositories/user-profiles.repository.ts"() {
6686
6928
  "use strict";
6687
6929
  init_user_profiles();
6688
- UserProfilesRepository = class extends BaseRepository9 {
6930
+ UserProfilesRepository = class extends BaseRepository10 {
6689
6931
  /**
6690
6932
  * ID로 프로필 조회
6691
6933
  */
6692
- async findById(id14) {
6693
- const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.id, id14)).limit(1);
6934
+ async findById(id15) {
6935
+ const result = await this.readDb.select().from(userProfiles).where(eq10(userProfiles.id, id15)).limit(1);
6694
6936
  return result[0] ?? null;
6695
6937
  }
6696
6938
  /**
6697
6939
  * User ID로 locale만 조회 (경량)
6698
6940
  */
6699
6941
  async findLocaleByUserId(userId) {
6700
- const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
6942
+ const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1);
6701
6943
  return result[0]?.locale || "en";
6702
6944
  }
6703
6945
  /**
6704
6946
  * User ID로 프로필 조회
6705
6947
  */
6706
6948
  async findByUserId(userId) {
6707
- const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
6949
+ const result = await this.readDb.select().from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1);
6708
6950
  return result[0] ?? null;
6709
6951
  }
6710
6952
  /**
@@ -6716,29 +6958,29 @@ var init_user_profiles_repository = __esm({
6716
6958
  /**
6717
6959
  * 프로필 업데이트 (by ID)
6718
6960
  */
6719
- async updateById(id14, data) {
6720
- const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.id, id14)).returning();
6961
+ async updateById(id15, data) {
6962
+ const result = await this.db.update(userProfiles).set(data).where(eq10(userProfiles.id, id15)).returning();
6721
6963
  return result[0] ?? null;
6722
6964
  }
6723
6965
  /**
6724
6966
  * 프로필 업데이트 (by User ID)
6725
6967
  */
6726
6968
  async updateByUserId(userId, data) {
6727
- const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.userId, userId)).returning();
6969
+ const result = await this.db.update(userProfiles).set(data).where(eq10(userProfiles.userId, userId)).returning();
6728
6970
  return result[0] ?? null;
6729
6971
  }
6730
6972
  /**
6731
6973
  * 프로필 삭제 (by ID)
6732
6974
  */
6733
- async deleteById(id14) {
6734
- const result = await this.db.delete(userProfiles).where(eq9(userProfiles.id, id14)).returning();
6975
+ async deleteById(id15) {
6976
+ const result = await this.db.delete(userProfiles).where(eq10(userProfiles.id, id15)).returning();
6735
6977
  return result[0] ?? null;
6736
6978
  }
6737
6979
  /**
6738
6980
  * 프로필 삭제 (by User ID)
6739
6981
  */
6740
6982
  async deleteByUserId(userId) {
6741
- const result = await this.db.delete(userProfiles).where(eq9(userProfiles.userId, userId)).returning();
6983
+ const result = await this.db.delete(userProfiles).where(eq10(userProfiles.userId, userId)).returning();
6742
6984
  return result[0] ?? null;
6743
6985
  }
6744
6986
  /**
@@ -6780,7 +7022,7 @@ var init_user_profiles_repository = __esm({
6780
7022
  metadata: userProfiles.metadata,
6781
7023
  createdAt: userProfiles.createdAt,
6782
7024
  updatedAt: userProfiles.updatedAt
6783
- }).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
7025
+ }).from(userProfiles).where(eq10(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
6784
7026
  if (!profile) {
6785
7027
  return null;
6786
7028
  }
@@ -6808,8 +7050,8 @@ var init_user_profiles_repository = __esm({
6808
7050
  });
6809
7051
 
6810
7052
  // src/server/repositories/invitations.repository.ts
6811
- import { eq as eq10, and as and7, lt as lt4, desc as desc2, sql as sql6 } from "drizzle-orm";
6812
- import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
7053
+ import { eq as eq11, and as and8, lt as lt4, desc as desc2, sql as sql7 } from "drizzle-orm";
7054
+ import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
6813
7055
  var InvitationsRepository, invitationsRepository;
6814
7056
  var init_invitations_repository = __esm({
6815
7057
  "src/server/repositories/invitations.repository.ts"() {
@@ -6818,19 +7060,19 @@ var init_invitations_repository = __esm({
6818
7060
  init_roles();
6819
7061
  init_user_invitations();
6820
7062
  init_email();
6821
- InvitationsRepository = class extends BaseRepository10 {
7063
+ InvitationsRepository = class extends BaseRepository11 {
6822
7064
  /**
6823
7065
  * ID로 초대 조회
6824
7066
  */
6825
- async findById(id14) {
6826
- const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.id, id14)).limit(1);
7067
+ async findById(id15) {
7068
+ const result = await this.readDb.select().from(userInvitations).where(eq11(userInvitations.id, id15)).limit(1);
6827
7069
  return result[0] ?? null;
6828
7070
  }
6829
7071
  /**
6830
7072
  * Token으로 초대 조회
6831
7073
  */
6832
7074
  async findByToken(token) {
6833
- const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.token, token)).limit(1);
7075
+ const result = await this.readDb.select().from(userInvitations).where(eq11(userInvitations.token, token)).limit(1);
6834
7076
  return result[0] ?? null;
6835
7077
  }
6836
7078
  /**
@@ -6838,9 +7080,9 @@ var init_invitations_repository = __esm({
6838
7080
  */
6839
7081
  async findPendingByEmail(email) {
6840
7082
  const result = await this.readDb.select().from(userInvitations).where(
6841
- and7(
6842
- eq10(userInvitations.email, normalizeEmail(email)),
6843
- eq10(userInvitations.status, "pending")
7083
+ and8(
7084
+ eq11(userInvitations.email, normalizeEmail(email)),
7085
+ eq11(userInvitations.status, "pending")
6844
7086
  )
6845
7087
  ).limit(1);
6846
7088
  return result[0] ?? null;
@@ -6849,13 +7091,13 @@ var init_invitations_repository = __esm({
6849
7091
  * 초대자 ID로 모든 초대 조회
6850
7092
  */
6851
7093
  async findByInvitedBy(invitedBy) {
6852
- return this.readDb.select().from(userInvitations).where(eq10(userInvitations.invitedBy, invitedBy));
7094
+ return this.readDb.select().from(userInvitations).where(eq11(userInvitations.invitedBy, invitedBy));
6853
7095
  }
6854
7096
  /**
6855
7097
  * 상태별 초대 조회
6856
7098
  */
6857
7099
  async findByStatus(status) {
6858
- return this.readDb.select().from(userInvitations).where(eq10(userInvitations.status, status));
7100
+ return this.readDb.select().from(userInvitations).where(eq11(userInvitations.status, status));
6859
7101
  }
6860
7102
  /**
6861
7103
  * 초대 생성
@@ -6866,7 +7108,7 @@ var init_invitations_repository = __esm({
6866
7108
  /**
6867
7109
  * 초대 상태 업데이트
6868
7110
  */
6869
- async updateStatus(id14, status, timestamp2) {
7111
+ async updateStatus(id15, status, timestamp2) {
6870
7112
  const updates = {
6871
7113
  status
6872
7114
  };
@@ -6877,14 +7119,14 @@ var init_invitations_repository = __esm({
6877
7119
  updates.cancelledAt = timestamp2;
6878
7120
  }
6879
7121
  }
6880
- const result = await this.db.update(userInvitations).set(updates).where(eq10(userInvitations.id, id14)).returning();
7122
+ const result = await this.db.update(userInvitations).set(updates).where(eq11(userInvitations.id, id15)).returning();
6881
7123
  return result[0] ?? null;
6882
7124
  }
6883
7125
  /**
6884
7126
  * 초대 삭제
6885
7127
  */
6886
- async deleteById(id14) {
6887
- const result = await this.db.delete(userInvitations).where(eq10(userInvitations.id, id14)).returning();
7128
+ async deleteById(id15) {
7129
+ const result = await this.db.delete(userInvitations).where(eq11(userInvitations.id, id15)).returning();
6888
7130
  return result[0] ?? null;
6889
7131
  }
6890
7132
  /**
@@ -6893,8 +7135,8 @@ var init_invitations_repository = __esm({
6893
7135
  async updateExpiredInvitations() {
6894
7136
  const now = /* @__PURE__ */ new Date();
6895
7137
  const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
6896
- and7(
6897
- eq10(userInvitations.status, "pending"),
7138
+ and8(
7139
+ eq11(userInvitations.status, "pending"),
6898
7140
  lt4(userInvitations.expiresAt, now)
6899
7141
  )
6900
7142
  ).returning();
@@ -6926,7 +7168,7 @@ var init_invitations_repository = __esm({
6926
7168
  id: users.id,
6927
7169
  email: users.email
6928
7170
  }
6929
- }).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(eq10(userInvitations.token, token)).limit(1);
7171
+ }).from(userInvitations).innerJoin(roles, eq11(userInvitations.roleId, roles.id)).innerJoin(users, eq11(userInvitations.invitedBy, users.id)).where(eq11(userInvitations.token, token)).limit(1);
6930
7172
  return result[0] ?? null;
6931
7173
  }
6932
7174
  /**
@@ -6937,13 +7179,13 @@ var init_invitations_repository = __esm({
6937
7179
  const offset = (page - 1) * limit;
6938
7180
  const conditions = [];
6939
7181
  if (status) {
6940
- conditions.push(eq10(userInvitations.status, status));
7182
+ conditions.push(eq11(userInvitations.status, status));
6941
7183
  }
6942
7184
  if (invitedBy) {
6943
- conditions.push(eq10(userInvitations.invitedBy, invitedBy));
7185
+ conditions.push(eq11(userInvitations.invitedBy, invitedBy));
6944
7186
  }
6945
- const whereClause = conditions.length > 0 ? and7(...conditions) : void 0;
6946
- const countResult = await this.readDb.select({ count: sql6`count(*)` }).from(userInvitations).where(whereClause);
7187
+ const whereClause = conditions.length > 0 ? and8(...conditions) : void 0;
7188
+ const countResult = await this.readDb.select({ count: sql7`count(*)` }).from(userInvitations).where(whereClause);
6947
7189
  const total = Number(countResult[0]?.count || 0);
6948
7190
  const results = await this.readDb.select({
6949
7191
  id: userInvitations.id,
@@ -6967,7 +7209,7 @@ var init_invitations_repository = __esm({
6967
7209
  id: users.id,
6968
7210
  email: users.email
6969
7211
  }
6970
- }).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
7212
+ }).from(userInvitations).innerJoin(roles, eq11(userInvitations.roleId, roles.id)).innerJoin(users, eq11(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
6971
7213
  return {
6972
7214
  invitations: results,
6973
7215
  total,
@@ -6979,31 +7221,31 @@ var init_invitations_repository = __esm({
6979
7221
  /**
6980
7222
  * 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
6981
7223
  */
6982
- async updateById(id14, data) {
7224
+ async updateById(id15, data) {
6983
7225
  const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
6984
- const result = await this.db.update(userInvitations).set(patch).where(eq10(userInvitations.id, id14)).returning();
7226
+ const result = await this.db.update(userInvitations).set(patch).where(eq11(userInvitations.id, id15)).returning();
6985
7227
  return result[0] ?? null;
6986
7228
  }
6987
7229
  /**
6988
7230
  * 초대 재전송 (status와 expiresAt 동시 업데이트)
6989
7231
  */
6990
- async resend(id14, newExpiresAt) {
7232
+ async resend(id15, newExpiresAt) {
6991
7233
  const result = await this.db.update(userInvitations).set({
6992
7234
  status: "pending",
6993
7235
  expiresAt: newExpiresAt
6994
- }).where(eq10(userInvitations.id, id14)).returning();
7236
+ }).where(eq11(userInvitations.id, id15)).returning();
6995
7237
  return result[0] ?? null;
6996
7238
  }
6997
7239
  /**
6998
7240
  * 초대 취소 (status, metadata 동시 업데이트)
6999
7241
  */
7000
- async cancel(id14, cancelledBy, reason, currentMetadata) {
7242
+ async cancel(id15, cancelledBy, reason, currentMetadata) {
7001
7243
  const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
7002
7244
  const result = await this.db.update(userInvitations).set({
7003
7245
  status: "cancelled",
7004
7246
  cancelledAt: /* @__PURE__ */ new Date(),
7005
7247
  metadata: newMetadata
7006
- }).where(eq10(userInvitations.id, id14)).returning();
7248
+ }).where(eq11(userInvitations.id, id15)).returning();
7007
7249
  return result[0] ?? null;
7008
7250
  }
7009
7251
  };
@@ -7090,6 +7332,15 @@ var init_schema5 = __esm({
7090
7332
  examples: [true, false]
7091
7333
  })
7092
7334
  },
7335
+ SPFN_AUTH_CSRF: {
7336
+ ...envString({
7337
+ description: 'CSRF protection for cookie-session mutations in the Next.js proxy: off | warn | enforce. Unset behaves as "warn" (log what would be refused, allow it through). configureAuth({ csrf: { mode } }) takes precedence.',
7338
+ required: false,
7339
+ nextjs: true,
7340
+ // The check runs in the Next.js proxy
7341
+ examples: ["enforce", "warn", "off"]
7342
+ })
7343
+ },
7093
7344
  SPFN_AUTH_BCRYPT_SALT_ROUNDS: {
7094
7345
  ...envNumber({
7095
7346
  description: "Bcrypt salt rounds (cost factor, higher = more secure but slower)",
@@ -7650,15 +7901,15 @@ var init_token_cipher = __esm({
7650
7901
  });
7651
7902
 
7652
7903
  // src/server/repositories/social-accounts.repository.ts
7653
- import { eq as eq11, and as and8 } from "drizzle-orm";
7654
- import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
7904
+ import { eq as eq12, and as and9 } from "drizzle-orm";
7905
+ import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
7655
7906
  var SocialAccountsRepository, socialAccountsRepository;
7656
7907
  var init_social_accounts_repository = __esm({
7657
7908
  "src/server/repositories/social-accounts.repository.ts"() {
7658
7909
  "use strict";
7659
7910
  init_entities();
7660
7911
  init_token_cipher();
7661
- SocialAccountsRepository = class extends BaseRepository11 {
7912
+ SocialAccountsRepository = class extends BaseRepository12 {
7662
7913
  /**
7663
7914
  * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
7664
7915
  *
@@ -7686,10 +7937,10 @@ var init_social_accounts_repository = __esm({
7686
7937
  if (refresh?.needsRotation) {
7687
7938
  heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
7688
7939
  }
7689
- await this.db.update(userSocialAccounts).set(heal).where(and8(
7690
- eq11(userSocialAccounts.id, account.id),
7691
- access?.needsRotation && account.accessToken !== null ? eq11(userSocialAccounts.accessToken, account.accessToken) : void 0,
7692
- refresh?.needsRotation && account.refreshToken !== null ? eq11(userSocialAccounts.refreshToken, account.refreshToken) : void 0
7940
+ await this.db.update(userSocialAccounts).set(heal).where(and9(
7941
+ eq12(userSocialAccounts.id, account.id),
7942
+ access?.needsRotation && account.accessToken !== null ? eq12(userSocialAccounts.accessToken, account.accessToken) : void 0,
7943
+ refresh?.needsRotation && account.refreshToken !== null ? eq12(userSocialAccounts.refreshToken, account.refreshToken) : void 0
7693
7944
  ));
7694
7945
  } catch {
7695
7946
  }
@@ -7706,9 +7957,9 @@ var init_social_accounts_repository = __esm({
7706
7957
  */
7707
7958
  async findByProviderAndProviderId(provider, providerUserId) {
7708
7959
  const result = await this.readDb.select().from(userSocialAccounts).where(
7709
- and8(
7710
- eq11(userSocialAccounts.provider, provider),
7711
- eq11(userSocialAccounts.providerUserId, providerUserId)
7960
+ and9(
7961
+ eq12(userSocialAccounts.provider, provider),
7962
+ eq12(userSocialAccounts.providerUserId, providerUserId)
7712
7963
  )
7713
7964
  ).limit(1);
7714
7965
  return this.decryptAccount(result[0] ?? null);
@@ -7718,7 +7969,7 @@ var init_social_accounts_repository = __esm({
7718
7969
  * Read replica 사용
7719
7970
  */
7720
7971
  async findByUserId(userId) {
7721
- const result = await this.readDb.select().from(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId));
7972
+ const result = await this.readDb.select().from(userSocialAccounts).where(eq12(userSocialAccounts.userId, userId));
7722
7973
  return Promise.all(result.map((account) => this.decryptAccount(account)));
7723
7974
  }
7724
7975
  /**
@@ -7727,9 +7978,9 @@ var init_social_accounts_repository = __esm({
7727
7978
  */
7728
7979
  async findByUserIdAndProvider(userId, provider) {
7729
7980
  const result = await this.readDb.select().from(userSocialAccounts).where(
7730
- and8(
7731
- eq11(userSocialAccounts.userId, userId),
7732
- eq11(userSocialAccounts.provider, provider)
7981
+ and9(
7982
+ eq12(userSocialAccounts.userId, userId),
7983
+ eq12(userSocialAccounts.provider, provider)
7733
7984
  )
7734
7985
  ).limit(1);
7735
7986
  return this.decryptAccount(result[0] ?? null);
@@ -7755,11 +8006,11 @@ var init_social_accounts_repository = __esm({
7755
8006
  * 토큰 정보 업데이트
7756
8007
  * Write primary 사용
7757
8008
  */
7758
- async updateTokens(id14, data) {
8009
+ async updateTokens(id15, data) {
7759
8010
  const accounts = await this.db.select({
7760
8011
  provider: userSocialAccounts.provider,
7761
8012
  providerUserId: userSocialAccounts.providerUserId
7762
- }).from(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).limit(1);
8013
+ }).from(userSocialAccounts).where(eq12(userSocialAccounts.id, id15)).limit(1);
7763
8014
  const account = accounts[0];
7764
8015
  if (!account) {
7765
8016
  return null;
@@ -7773,15 +8024,15 @@ var init_social_accounts_repository = __esm({
7773
8024
  ...data,
7774
8025
  accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
7775
8026
  refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
7776
- }).where(eq11(userSocialAccounts.id, id14)).returning();
8027
+ }).where(eq12(userSocialAccounts.id, id15)).returning();
7777
8028
  return this.decryptAccount(result[0] ?? null);
7778
8029
  }
7779
8030
  /**
7780
8031
  * 소셜 계정 삭제
7781
8032
  * Write primary 사용
7782
8033
  */
7783
- async deleteById(id14) {
7784
- const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).returning();
8034
+ async deleteById(id15) {
8035
+ const result = await this.db.delete(userSocialAccounts).where(eq12(userSocialAccounts.id, id15)).returning();
7785
8036
  return result[0] ?? null;
7786
8037
  }
7787
8038
  /**
@@ -7790,9 +8041,9 @@ var init_social_accounts_repository = __esm({
7790
8041
  */
7791
8042
  async deleteByUserIdAndProvider(userId, provider) {
7792
8043
  const result = await this.db.delete(userSocialAccounts).where(
7793
- and8(
7794
- eq11(userSocialAccounts.userId, userId),
7795
- eq11(userSocialAccounts.provider, provider)
8044
+ and9(
8045
+ eq12(userSocialAccounts.userId, userId),
8046
+ eq12(userSocialAccounts.provider, provider)
7796
8047
  )
7797
8048
  ).returning();
7798
8049
  return result[0] ?? null;
@@ -7805,7 +8056,7 @@ var init_social_accounts_repository = __esm({
7805
8056
  * Write primary 사용
7806
8057
  */
7807
8058
  async deleteAllByUserId(userId) {
7808
- const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId)).returning();
8059
+ const result = await this.db.delete(userSocialAccounts).where(eq12(userSocialAccounts.userId, userId)).returning();
7809
8060
  return result.length;
7810
8061
  }
7811
8062
  };
@@ -7814,19 +8065,19 @@ var init_social_accounts_repository = __esm({
7814
8065
  });
7815
8066
 
7816
8067
  // src/server/repositories/auth-metadata.repository.ts
7817
- import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
7818
- import { eq as eq12 } from "drizzle-orm";
8068
+ import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
8069
+ import { eq as eq13 } from "drizzle-orm";
7819
8070
  var AuthMetadataRepository, authMetadataRepository;
7820
8071
  var init_auth_metadata_repository = __esm({
7821
8072
  "src/server/repositories/auth-metadata.repository.ts"() {
7822
8073
  "use strict";
7823
8074
  init_auth_metadata();
7824
- AuthMetadataRepository = class extends BaseRepository12 {
8075
+ AuthMetadataRepository = class extends BaseRepository13 {
7825
8076
  /**
7826
8077
  * 키로 값 조회
7827
8078
  */
7828
8079
  async get(key) {
7829
- const result = await this.readDb.select().from(authMetadata).where(eq12(authMetadata.key, key)).limit(1);
8080
+ const result = await this.readDb.select().from(authMetadata).where(eq13(authMetadata.key, key)).limit(1);
7830
8081
  return result[0]?.value ?? null;
7831
8082
  }
7832
8083
  /**
@@ -7849,20 +8100,20 @@ var init_auth_metadata_repository = __esm({
7849
8100
  });
7850
8101
 
7851
8102
  // src/server/repositories/account-deletion-requests.repository.ts
7852
- import { eq as eq13, and as and9, lte } from "drizzle-orm";
7853
- import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
8103
+ import { eq as eq14, and as and10, lte } from "drizzle-orm";
8104
+ import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
7854
8105
  var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
7855
8106
  var init_account_deletion_requests_repository = __esm({
7856
8107
  "src/server/repositories/account-deletion-requests.repository.ts"() {
7857
8108
  "use strict";
7858
8109
  init_account_deletion_requests();
7859
- AccountDeletionRequestsRepository = class extends BaseRepository13 {
8110
+ AccountDeletionRequestsRepository = class extends BaseRepository14 {
7860
8111
  /**
7861
8112
  * ID로 요청 조회
7862
8113
  * Read replica 사용
7863
8114
  */
7864
- async findById(id14) {
7865
- const result = await this.readDb.select().from(accountDeletionRequests).where(eq13(accountDeletionRequests.id, id14)).limit(1);
8115
+ async findById(id15) {
8116
+ const result = await this.readDb.select().from(accountDeletionRequests).where(eq14(accountDeletionRequests.id, id15)).limit(1);
7866
8117
  return result[0] ?? null;
7867
8118
  }
7868
8119
  /**
@@ -7871,9 +8122,9 @@ var init_account_deletion_requests_repository = __esm({
7871
8122
  */
7872
8123
  async findPendingByUserId(userId) {
7873
8124
  const result = await this.readDb.select().from(accountDeletionRequests).where(
7874
- and9(
7875
- eq13(accountDeletionRequests.userId, userId),
7876
- eq13(accountDeletionRequests.status, "pending")
8125
+ and10(
8126
+ eq14(accountDeletionRequests.userId, userId),
8127
+ eq14(accountDeletionRequests.status, "pending")
7877
8128
  )
7878
8129
  ).limit(1);
7879
8130
  return result[0] ?? null;
@@ -7887,9 +8138,9 @@ var init_account_deletion_requests_repository = __esm({
7887
8138
  */
7888
8139
  async findPendingByUserIdOnPrimary(userId) {
7889
8140
  const result = await this.db.select().from(accountDeletionRequests).where(
7890
- and9(
7891
- eq13(accountDeletionRequests.userId, userId),
7892
- eq13(accountDeletionRequests.status, "pending")
8141
+ and10(
8142
+ eq14(accountDeletionRequests.userId, userId),
8143
+ eq14(accountDeletionRequests.status, "pending")
7893
8144
  )
7894
8145
  ).limit(1);
7895
8146
  return result[0] ?? null;
@@ -7900,8 +8151,8 @@ var init_account_deletion_requests_repository = __esm({
7900
8151
  */
7901
8152
  async findDueForPurge(now) {
7902
8153
  return this.readDb.select().from(accountDeletionRequests).where(
7903
- and9(
7904
- eq13(accountDeletionRequests.status, "pending"),
8154
+ and10(
8155
+ eq14(accountDeletionRequests.status, "pending"),
7905
8156
  lte(accountDeletionRequests.purgeScheduledAt, now)
7906
8157
  )
7907
8158
  );
@@ -7921,14 +8172,14 @@ var init_account_deletion_requests_repository = __esm({
7921
8172
  * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
7922
8173
  * Write primary 사용
7923
8174
  */
7924
- async markCancelled(id14) {
8175
+ async markCancelled(id15) {
7925
8176
  const result = await this.db.update(accountDeletionRequests).set({
7926
8177
  status: "cancelled",
7927
8178
  cancelledAt: /* @__PURE__ */ new Date()
7928
8179
  }).where(
7929
- and9(
7930
- eq13(accountDeletionRequests.id, id14),
7931
- eq13(accountDeletionRequests.status, "pending")
8180
+ and10(
8181
+ eq14(accountDeletionRequests.id, id15),
8182
+ eq14(accountDeletionRequests.status, "pending")
7932
8183
  )
7933
8184
  ).returning();
7934
8185
  return result[0] ?? null;
@@ -7943,15 +8194,15 @@ var init_account_deletion_requests_repository = __esm({
7943
8194
  * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
7944
8195
  * Write primary 사용
7945
8196
  */
7946
- async markCompleted(id14, purgeStrategy) {
8197
+ async markCompleted(id15, purgeStrategy) {
7947
8198
  const result = await this.db.update(accountDeletionRequests).set({
7948
8199
  status: "completed",
7949
8200
  completedAt: /* @__PURE__ */ new Date(),
7950
8201
  purgeStrategy
7951
8202
  }).where(
7952
- and9(
7953
- eq13(accountDeletionRequests.id, id14),
7954
- eq13(accountDeletionRequests.status, "pending")
8203
+ and10(
8204
+ eq14(accountDeletionRequests.id, id15),
8205
+ eq14(accountDeletionRequests.status, "pending")
7955
8206
  )
7956
8207
  ).returning();
7957
8208
  return result[0] ?? null;
@@ -7962,14 +8213,14 @@ var init_account_deletion_requests_repository = __esm({
7962
8213
  });
7963
8214
 
7964
8215
  // src/server/repositories/ops-tokens.repository.ts
7965
- import { and as and10, desc as desc3, eq as eq14, isNull as isNull5 } from "drizzle-orm";
7966
- import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
8216
+ import { and as and11, desc as desc3, eq as eq15, isNull as isNull5 } from "drizzle-orm";
8217
+ import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
7967
8218
  var OpsTokensRepository, opsTokensRepository;
7968
8219
  var init_ops_tokens_repository = __esm({
7969
8220
  "src/server/repositories/ops-tokens.repository.ts"() {
7970
8221
  "use strict";
7971
8222
  init_ops_tokens();
7972
- OpsTokensRepository = class extends BaseRepository14 {
8223
+ OpsTokensRepository = class extends BaseRepository15 {
7973
8224
  /**
7974
8225
  * Lookup by the secret's hash — the verification path.
7975
8226
  *
@@ -7979,7 +8230,7 @@ var init_ops_tokens_repository = __esm({
7979
8230
  * and revocation is documented as taking effect immediately.
7980
8231
  */
7981
8232
  async findByTokenHash(tokenHash) {
7982
- const result = await this.db.select().from(opsTokens).where(eq14(opsTokens.tokenHash, tokenHash)).limit(1);
8233
+ const result = await this.db.select().from(opsTokens).where(eq15(opsTokens.tokenHash, tokenHash)).limit(1);
7983
8234
  return result[0] ?? null;
7984
8235
  }
7985
8236
  async create(data) {
@@ -7994,13 +8245,13 @@ var init_ops_tokens_repository = __esm({
7994
8245
  * token is already revoked — the first revocation's timestamp is never
7995
8246
  * overwritten.
7996
8247
  */
7997
- async revokeById(id14) {
7998
- const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and10(eq14(opsTokens.id, id14), isNull5(opsTokens.revokedAt))).returning();
8248
+ async revokeById(id15) {
8249
+ const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and11(eq15(opsTokens.id, id15), isNull5(opsTokens.revokedAt))).returning();
7999
8250
  return result[0] ?? null;
8000
8251
  }
8001
8252
  /** Fire-and-forget from the verification path. */
8002
- async updateLastUsedById(id14) {
8003
- await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq14(opsTokens.id, id14));
8253
+ async updateLastUsedById(id15) {
8254
+ await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq15(opsTokens.id, id15));
8004
8255
  }
8005
8256
  };
8006
8257
  opsTokensRepository = new OpsTokensRepository();
@@ -8015,6 +8266,7 @@ var init_repositories = __esm({
8015
8266
  init_keys_repository();
8016
8267
  init_verification_codes_repository();
8017
8268
  init_signup_link_tokens_repository();
8269
+ init_device_authorizations_repository();
8018
8270
  init_roles_repository();
8019
8271
  init_permissions_repository();
8020
8272
  init_role_permissions_repository();
@@ -8116,7 +8368,7 @@ async function removePermissionFromRole(roleId, permissionId) {
8116
8368
  }
8117
8369
  async function setRolePermissions(roleId, permissionIds) {
8118
8370
  const roleIdNum = Number(roleId);
8119
- const permissionIdNums = permissionIds.map((id14) => Number(id14));
8371
+ const permissionIdNums = permissionIds.map((id15) => Number(id15));
8120
8372
  await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
8121
8373
  }
8122
8374
  async function getAllRoles(includeInactive = false) {
@@ -8136,7 +8388,7 @@ async function getRolePermissions(roleId) {
8136
8388
  }
8137
8389
  const permissionIds = mappings.map((m) => m.permissionId);
8138
8390
  const perms = await Promise.all(
8139
- permissionIds.map((id14) => permissionsRepository.findById(id14))
8391
+ permissionIds.map((id15) => permissionsRepository.findById(id15))
8140
8392
  );
8141
8393
  return perms.filter((p) => p !== null).map((p) => p.name);
8142
8394
  }
@@ -8353,6 +8605,27 @@ import {
8353
8605
  // src/server/lib/config.ts
8354
8606
  init_email();
8355
8607
  import { env as env4 } from "@spfn/auth/config";
8608
+
8609
+ // src/server/logger.ts
8610
+ import { logger as rootLogger } from "@spfn/core/logger";
8611
+ var authLogger = {
8612
+ plugin: rootLogger.child("@spfn/auth:plugin"),
8613
+ middleware: rootLogger.child("@spfn/auth:middleware"),
8614
+ interceptor: {
8615
+ general: rootLogger.child("@spfn/auth:interceptor:general"),
8616
+ login: rootLogger.child("@spfn/auth:interceptor:login"),
8617
+ keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
8618
+ oauth: rootLogger.child("@spfn/auth:interceptor:oauth"),
8619
+ csrf: rootLogger.child("@spfn/auth:interceptor:csrf")
8620
+ },
8621
+ session: rootLogger.child("@spfn/auth:session"),
8622
+ service: rootLogger.child("@spfn/auth:service"),
8623
+ setup: rootLogger.child("@spfn/auth:setup"),
8624
+ email: rootLogger.child("@spfn/auth:email"),
8625
+ sms: rootLogger.child("@spfn/auth:sms")
8626
+ };
8627
+
8628
+ // src/server/lib/config.ts
8356
8629
  function getCookieSuffix() {
8357
8630
  const port = process.env.SPFN_PORT;
8358
8631
  return port ? `_${port}` : "";
@@ -8377,6 +8650,10 @@ var COOKIE_NAMES = {
8377
8650
  /** Password-setup session for verified-email signup — temporary, single-purpose */
8378
8651
  get SIGNUP_SETUP() {
8379
8652
  return `spfn_signup_setup${getCookieSuffix()}`;
8653
+ },
8654
+ /** CSRF token — the only cookie here the browser can read */
8655
+ get CSRF() {
8656
+ return `spfn_csrf${getCookieSuffix()}`;
8380
8657
  }
8381
8658
  };
8382
8659
  function matchOAuthCsrfCookies(cookies) {
@@ -8409,10 +8686,10 @@ var globalConfig = {
8409
8686
  sessionTtl: "7d"
8410
8687
  // Default: 7 days
8411
8688
  };
8412
- function configureAuth(config2) {
8689
+ function configureAuth(config3) {
8413
8690
  globalConfig = {
8414
8691
  ...globalConfig,
8415
- ...config2
8692
+ ...config3
8416
8693
  };
8417
8694
  }
8418
8695
  function getAuthConfig() {
@@ -8437,6 +8714,28 @@ function getSessionTtl(override) {
8437
8714
  }
8438
8715
  return 7 * 24 * 60 * 60;
8439
8716
  }
8717
+ var CSRF_MODES = ["off", "warn", "enforce"];
8718
+ var unrecognizedCsrfModeReported = false;
8719
+ function getCsrfMode() {
8720
+ const configured2 = globalConfig.csrf?.mode ?? env4.SPFN_AUTH_CSRF;
8721
+ if (!configured2) {
8722
+ return "warn";
8723
+ }
8724
+ const normalized = String(configured2).trim().toLowerCase();
8725
+ if (!CSRF_MODES.includes(normalized)) {
8726
+ if (!unrecognizedCsrfModeReported) {
8727
+ unrecognizedCsrfModeReported = true;
8728
+ authLogger.interceptor.csrf.error(
8729
+ `Unrecognized CSRF mode "${configured2}" \u2014 expected off | warn | enforce. Enforcing.`
8730
+ );
8731
+ }
8732
+ return "enforce";
8733
+ }
8734
+ return normalized;
8735
+ }
8736
+ function getCsrfExemptPaths() {
8737
+ return globalConfig.csrf?.exemptPaths ?? [];
8738
+ }
8440
8739
 
8441
8740
  // src/server/services/verification.service.ts
8442
8741
  import crypto4 from "crypto";
@@ -8444,26 +8743,6 @@ import { env as env5 } from "@spfn/auth/config";
8444
8743
  import { InvalidVerificationCodeError } from "@spfn/auth/errors";
8445
8744
  import jwt2 from "jsonwebtoken";
8446
8745
  import { sendEmail, sendSMS } from "@spfn/notification/server";
8447
-
8448
- // src/server/logger.ts
8449
- import { logger as rootLogger } from "@spfn/core/logger";
8450
- var authLogger = {
8451
- plugin: rootLogger.child("@spfn/auth:plugin"),
8452
- middleware: rootLogger.child("@spfn/auth:middleware"),
8453
- interceptor: {
8454
- general: rootLogger.child("@spfn/auth:interceptor:general"),
8455
- login: rootLogger.child("@spfn/auth:interceptor:login"),
8456
- keyRotation: rootLogger.child("@spfn/auth:interceptor:key-rotation"),
8457
- oauth: rootLogger.child("@spfn/auth:interceptor:oauth")
8458
- },
8459
- session: rootLogger.child("@spfn/auth:session"),
8460
- service: rootLogger.child("@spfn/auth:service"),
8461
- setup: rootLogger.child("@spfn/auth:setup"),
8462
- email: rootLogger.child("@spfn/auth:email"),
8463
- sms: rootLogger.child("@spfn/auth:sms")
8464
- };
8465
-
8466
- // src/server/services/verification.service.ts
8467
8746
  init_email();
8468
8747
  init_repositories();
8469
8748
  var ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES = 60;
@@ -8646,6 +8925,7 @@ var KEY_TTL_DAYS = 90;
8646
8925
  init_repositories();
8647
8926
  import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/auth/errors";
8648
8927
  var KEY_FINGERPRINT_PREFIX_LENGTH = 8;
8928
+ var DEFAULT_KEY_ALGORITHM = "ES256";
8649
8929
  function getKeyExpiryDate() {
8650
8930
  const expiresAt = /* @__PURE__ */ new Date();
8651
8931
  expiresAt.setDate(expiresAt.getDate() + KEY_TTL_DAYS);
@@ -8655,7 +8935,7 @@ function isExpired(expiresAt) {
8655
8935
  return expiresAt !== null && /* @__PURE__ */ new Date() > expiresAt;
8656
8936
  }
8657
8937
  async function registerPublicKeyService(params) {
8658
- const { userId, keyId, publicKey, fingerprint, algorithm = "ES256", deviceName, platform } = params;
8938
+ const { userId, keyId, publicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM, deviceName, platform } = params;
8659
8939
  const existing = await keysRepository.findByKeyId(keyId);
8660
8940
  if (existing) {
8661
8941
  if (existing.userId === userId && existing.isActive) {
@@ -8683,7 +8963,7 @@ async function registerPublicKeyService(params) {
8683
8963
  });
8684
8964
  }
8685
8965
  async function rotateKeyService(params) {
8686
- const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = "ES256" } = params;
8966
+ const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM } = params;
8687
8967
  const isValidFingerprint = verifyKeyFingerprint(newPublicKey, fingerprint);
8688
8968
  if (!isValidFingerprint) {
8689
8969
  throw new InvalidKeyFingerprintError();
@@ -8734,6 +9014,7 @@ async function listKeysService(params) {
8734
9014
  async function revokeAllKeysService(params) {
8735
9015
  const { userId, currentKeyId, includeCurrent = false, reason } = params;
8736
9016
  const revoked = includeCurrent ? await keysRepository.revokeAllActiveByUserId(userId, reason) : await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason);
9017
+ await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
8737
9018
  return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
8738
9019
  }
8739
9020
 
@@ -8859,11 +9140,17 @@ var AuthProviderSchema = Type.Union([
8859
9140
  Type.Literal("phone"),
8860
9141
  ...SOCIAL_PROVIDERS.map((p) => Type.Literal(p))
8861
9142
  ]);
9143
+ var AuthLoginProviderSchema = Type.Union([
9144
+ Type.Literal("email"),
9145
+ Type.Literal("phone"),
9146
+ Type.Literal("device"),
9147
+ ...SOCIAL_PROVIDERS.map((p) => Type.Literal(p))
9148
+ ]);
8862
9149
  var authLoginEvent = defineEvent(
8863
9150
  "auth.login",
8864
9151
  Type.Object({
8865
9152
  userId: Type.String(),
8866
- provider: AuthProviderSchema,
9153
+ provider: AuthLoginProviderSchema,
8867
9154
  email: Type.Optional(Type.String()),
8868
9155
  phone: Type.Optional(Type.String())
8869
9156
  })
@@ -8971,8 +9258,8 @@ async function verifyReauthCredential(user, params) {
8971
9258
  throw new VerificationTokenTargetMismatchError();
8972
9259
  }
8973
9260
  }
8974
- async function sendDeletionEmail(to, subject, text14) {
8975
- const result = await sendEmail2({ to, subject, text: text14 });
9261
+ async function sendDeletionEmail(to, subject, text15) {
9262
+ const result = await sendEmail2({ to, subject, text: text15 });
8976
9263
  if (!result.success) {
8977
9264
  authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
8978
9265
  }
@@ -9024,12 +9311,12 @@ async function requestAccountDeletionService(userId, params) {
9024
9311
  if (requestedBy === "self") {
9025
9312
  await verifyReauthCredential(user, { password, verificationToken });
9026
9313
  }
9027
- const config2 = getDeletionConfig();
9314
+ const config3 = getDeletionConfig();
9028
9315
  const wantsImmediate = immediate === true;
9029
- if (wantsImmediate && requestedBy === "self" && !config2.allowSelfImmediate) {
9316
+ if (wantsImmediate && requestedBy === "self" && !config3.allowSelfImmediate) {
9030
9317
  throw new ImmediateDeletionNotAllowedError();
9031
9318
  }
9032
- const gracePeriodDays = wantsImmediate ? 0 : config2.gracePeriodDays;
9319
+ const gracePeriodDays = wantsImmediate ? 0 : config3.gracePeriodDays;
9033
9320
  const requestedAt = /* @__PURE__ */ new Date();
9034
9321
  const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
9035
9322
  await usersRepository.updateById(user.id, { status: "pending_deletion" });
@@ -9051,6 +9338,7 @@ async function requestAccountDeletionService(userId, params) {
9051
9338
  throw error;
9052
9339
  }
9053
9340
  await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
9341
+ await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
9054
9342
  onAfterCommit(() => authDeletionRequestedEvent.emit({
9055
9343
  userId: String(user.id),
9056
9344
  userPublicId: user.publicId,
@@ -9133,10 +9421,10 @@ async function purgePendingRequest(request) {
9133
9421
  if (!precheckUser || precheckUser.status !== "pending_deletion") {
9134
9422
  return { outcome: "skipped" };
9135
9423
  }
9136
- const config2 = getDeletionConfig();
9137
- if (config2.onBeforePurge) {
9424
+ const config3 = getDeletionConfig();
9425
+ if (config3.onBeforePurge) {
9138
9426
  try {
9139
- await config2.onBeforePurge({
9427
+ await config3.onBeforePurge({
9140
9428
  id: precheckUser.id,
9141
9429
  publicId: precheckUser.publicId,
9142
9430
  email: precheckUser.email,
@@ -9150,7 +9438,7 @@ async function purgePendingRequest(request) {
9150
9438
  return { outcome: "skipped" };
9151
9439
  }
9152
9440
  }
9153
- const purgeStrategy = config2.purgeStrategy;
9441
+ const purgeStrategy = config3.purgeStrategy;
9154
9442
  let purgedUser = null;
9155
9443
  await runInTransaction(async () => {
9156
9444
  const user = await usersRepository.findById(userId);
@@ -9371,6 +9659,7 @@ async function changePasswordService(params) {
9371
9659
  }
9372
9660
  const newPasswordHash = await hashPassword(newPassword);
9373
9661
  await usersRepository.updatePassword(userId, newPasswordHash, true);
9662
+ await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
9374
9663
  await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
9375
9664
  }
9376
9665
 
@@ -9526,10 +9815,238 @@ async function completeSignupService(params) {
9526
9815
  });
9527
9816
  }
9528
9817
 
9818
+ // src/server/services/device-auth.service.ts
9819
+ init_repositories();
9820
+ import {
9821
+ AccountDisabledError as AccountDisabledError2,
9822
+ AccountPendingDeletionError as AccountPendingDeletionError2,
9823
+ DeviceAuthNotFoundError,
9824
+ DeviceAuthExpiredError,
9825
+ DeviceAuthAlreadyHandledError,
9826
+ DeviceAuthDeniedError,
9827
+ InvalidKeyFingerprintError as InvalidKeyFingerprintError2
9828
+ } from "@spfn/auth/errors";
9829
+ import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
9830
+
9831
+ // src/server/lib/device-auth-config.ts
9832
+ var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
9833
+ var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
9834
+ var config2 = {
9835
+ ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
9836
+ intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
9837
+ };
9838
+ function configureDeviceAuth(options) {
9839
+ const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
9840
+ const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
9841
+ assertWholeMillis("ttlMs", ttlMs);
9842
+ assertWholeMillis("intervalMs", intervalMs);
9843
+ config2 = { ttlMs, intervalMs };
9844
+ }
9845
+ function assertWholeMillis(name, value) {
9846
+ if (!Number.isInteger(value) || value <= 0) {
9847
+ throw new Error(
9848
+ `deviceAuth.${name} must be a positive whole number of milliseconds, received ${value}.`
9849
+ );
9850
+ }
9851
+ }
9852
+ function getDeviceAuthConfig() {
9853
+ return config2;
9854
+ }
9855
+
9856
+ // src/server/lib/device-code.ts
9857
+ import { createHash, randomBytes, randomInt } from "crypto";
9858
+ var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
9859
+ var USER_CODE_LENGTH = 8;
9860
+ var USER_CODE_GROUP_SIZE = 4;
9861
+ var DEVICE_CODE_BYTES = 32;
9862
+ function generateUserCode() {
9863
+ let code = "";
9864
+ for (let position = 0; position < USER_CODE_LENGTH; position++) {
9865
+ code += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)];
9866
+ }
9867
+ return code;
9868
+ }
9869
+ function formatUserCode(userCode) {
9870
+ return `${userCode.slice(0, USER_CODE_GROUP_SIZE)}-${userCode.slice(USER_CODE_GROUP_SIZE)}`;
9871
+ }
9872
+ function normalizeUserCode(input) {
9873
+ return input.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
9874
+ }
9875
+ function generateDeviceCode() {
9876
+ return randomBytes(DEVICE_CODE_BYTES).toString("base64url");
9877
+ }
9878
+ function hashDeviceCode(deviceCode) {
9879
+ return createHash("sha256").update(deviceCode).digest("hex");
9880
+ }
9881
+
9882
+ // src/server/services/device-auth.service.ts
9883
+ var USER_CODE_ATTEMPTS = 3;
9884
+ function assertActionable(record) {
9885
+ if (!record || record.status === "consumed") {
9886
+ throw new DeviceAuthNotFoundError();
9887
+ }
9888
+ if (record.expiresAt.getTime() < Date.now()) {
9889
+ throw new DeviceAuthExpiredError();
9890
+ }
9891
+ return record;
9892
+ }
9893
+ function refuseMissedTransition(record, from, moved) {
9894
+ if (!record || record.status === "consumed") {
9895
+ throw new DeviceAuthNotFoundError();
9896
+ }
9897
+ if (record.status === from) {
9898
+ throw new DeviceAuthExpiredError();
9899
+ }
9900
+ throw moved();
9901
+ }
9902
+ function describeDevice(record) {
9903
+ return {
9904
+ deviceName: record.deviceName ?? void 0,
9905
+ platform: record.platform ?? void 0,
9906
+ fingerprintPrefix: record.fingerprint.slice(0, KEY_FINGERPRINT_PREFIX_LENGTH),
9907
+ requestedAtMillis: record.createdAt.getTime(),
9908
+ expiresAtMillis: record.expiresAt.getTime()
9909
+ };
9910
+ }
9911
+ async function startDeviceAuthService(params) {
9912
+ if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
9913
+ throw new InvalidKeyFingerprintError2();
9914
+ }
9915
+ const { ttlMs, intervalMs } = getDeviceAuthConfig();
9916
+ const expiresAt = new Date(Date.now() + ttlMs);
9917
+ for (let attempt = 0; attempt < USER_CODE_ATTEMPTS; attempt++) {
9918
+ const deviceCode = generateDeviceCode();
9919
+ const userCode = generateUserCode();
9920
+ const record = await deviceAuthorizationsRepository.create({
9921
+ deviceCodeHash: hashDeviceCode(deviceCode),
9922
+ userCode,
9923
+ publicKey: params.publicKey,
9924
+ keyId: params.keyId,
9925
+ fingerprint: params.fingerprint,
9926
+ algorithm: params.algorithm,
9927
+ deviceName: params.deviceName,
9928
+ platform: params.platform,
9929
+ expiresAt
9930
+ });
9931
+ if (record) {
9932
+ return {
9933
+ deviceCode,
9934
+ userCode: formatUserCode(userCode),
9935
+ expiresAtMillis: expiresAt.getTime(),
9936
+ intervalMillis: intervalMs
9937
+ };
9938
+ }
9939
+ }
9940
+ throw new Error(
9941
+ `Could not allocate a unique device user code in ${USER_CODE_ATTEMPTS} attempts. Check the code generator and the user_code unique index.`
9942
+ );
9943
+ }
9944
+ async function getDeviceAuthInfoService(params) {
9945
+ const record = assertActionable(
9946
+ await deviceAuthorizationsRepository.findByUserCode(normalizeUserCode(params.userCode))
9947
+ );
9948
+ if (record.status !== "pending") {
9949
+ throw new DeviceAuthAlreadyHandledError();
9950
+ }
9951
+ return describeDevice(record);
9952
+ }
9953
+ async function approveDeviceAuthService(params) {
9954
+ const userCode = normalizeUserCode(params.userCode);
9955
+ const record = assertActionable(
9956
+ await deviceAuthorizationsRepository.findByUserCode(userCode)
9957
+ );
9958
+ const approved = await deviceAuthorizationsRepository.approve(record.id, params.userId);
9959
+ if (!approved) {
9960
+ refuseMissedTransition(
9961
+ await deviceAuthorizationsRepository.findByUserCode(userCode),
9962
+ "pending",
9963
+ () => new DeviceAuthAlreadyHandledError()
9964
+ );
9965
+ }
9966
+ return describeDevice(approved);
9967
+ }
9968
+ async function denyDeviceAuthService(params) {
9969
+ const userCode = normalizeUserCode(params.userCode);
9970
+ const record = assertActionable(
9971
+ await deviceAuthorizationsRepository.findByUserCode(userCode)
9972
+ );
9973
+ const denied = await deviceAuthorizationsRepository.deny(record.id);
9974
+ if (!denied) {
9975
+ refuseMissedTransition(
9976
+ await deviceAuthorizationsRepository.findByUserCode(userCode),
9977
+ "pending",
9978
+ () => new DeviceAuthAlreadyHandledError()
9979
+ );
9980
+ }
9981
+ }
9982
+ async function pollDeviceAuthService(params) {
9983
+ const deviceCodeHash = hashDeviceCode(params.deviceCode);
9984
+ const record = assertActionable(
9985
+ await deviceAuthorizationsRepository.findByDeviceCodeHash(deviceCodeHash)
9986
+ );
9987
+ if (record.status === "denied") {
9988
+ throw new DeviceAuthDeniedError();
9989
+ }
9990
+ if (record.status === "pending") {
9991
+ return { status: "pending", intervalMillis: getDeviceAuthConfig().intervalMs };
9992
+ }
9993
+ const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
9994
+ if (!consumed) {
9995
+ refuseMissedTransition(
9996
+ await deviceAuthorizationsRepository.findByDeviceCodeHash(deviceCodeHash),
9997
+ "approved",
9998
+ () => new DeviceAuthNotFoundError()
9999
+ );
10000
+ }
10001
+ return { status: "approved", ...await completeDeviceLogin(consumed) };
10002
+ }
10003
+ async function completeDeviceLogin(record) {
10004
+ if (record.userId === null) {
10005
+ throw new DeviceAuthNotFoundError();
10006
+ }
10007
+ const user = await usersRepository.findById(record.userId);
10008
+ if (!user) {
10009
+ throw new DeviceAuthNotFoundError();
10010
+ }
10011
+ if (user.status !== "active") {
10012
+ if (user.status === "pending_deletion") {
10013
+ const pending = await getPendingDeletionInfo(user.id);
10014
+ throw new AccountPendingDeletionError2({
10015
+ purgeScheduledAt: pending?.purgeScheduledAt.toISOString()
10016
+ });
10017
+ }
10018
+ throw new AccountDisabledError2({ status: user.status });
10019
+ }
10020
+ await registerPublicKeyService({
10021
+ userId: user.id,
10022
+ keyId: record.keyId,
10023
+ publicKey: record.publicKey,
10024
+ fingerprint: record.fingerprint,
10025
+ algorithm: record.algorithm,
10026
+ deviceName: record.deviceName ?? void 0,
10027
+ platform: record.platform ?? void 0
10028
+ });
10029
+ await updateLastLoginService(user.id);
10030
+ const result = {
10031
+ userId: String(user.id),
10032
+ publicId: user.publicId,
10033
+ email: user.email || void 0,
10034
+ phone: user.phone || void 0,
10035
+ passwordChangeRequired: user.passwordChangeRequired
10036
+ };
10037
+ onAfterCommit2(() => authLoginEvent.emit({
10038
+ userId: result.userId,
10039
+ provider: "device",
10040
+ email: result.email,
10041
+ phone: result.phone
10042
+ }));
10043
+ return result;
10044
+ }
10045
+
9529
10046
  // src/server/services/rbac.service.ts
9530
10047
  init_repositories();
9531
10048
  init_rbac();
9532
- import { createHash } from "crypto";
10049
+ import { createHash as createHash2 } from "crypto";
9533
10050
  var RBAC_HASH_KEY = "rbac_config_hash";
9534
10051
  function computeConfigHash(allRoles, allPermissions, allMappings) {
9535
10052
  const payload = JSON.stringify({
@@ -9540,7 +10057,7 @@ function computeConfigHash(allRoles, allPermissions, allMappings) {
9540
10057
  return acc;
9541
10058
  }, {})
9542
10059
  });
9543
- return createHash("sha256").update(payload).digest("hex");
10060
+ return createHash2("sha256").update(payload).digest("hex");
9544
10061
  }
9545
10062
  function collectMappings(options) {
9546
10063
  const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
@@ -9598,51 +10115,51 @@ async function initializeAuth(options = {}) {
9598
10115
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
9599
10116
  }
9600
10117
  async function syncRoles(configs, existingByName) {
9601
- for (const config2 of configs) {
9602
- const existing = existingByName.get(config2.name);
10118
+ for (const config3 of configs) {
10119
+ const existing = existingByName.get(config3.name);
9603
10120
  if (!existing) {
9604
10121
  await rolesRepository.create({
9605
- name: config2.name,
9606
- displayName: config2.displayName,
9607
- description: config2.description || null,
9608
- priority: config2.priority ?? 10,
9609
- isSystem: config2.isSystem ?? false,
9610
- isBuiltin: config2.isBuiltin ?? false,
10122
+ name: config3.name,
10123
+ displayName: config3.displayName,
10124
+ description: config3.description || null,
10125
+ priority: config3.priority ?? 10,
10126
+ isSystem: config3.isSystem ?? false,
10127
+ isBuiltin: config3.isBuiltin ?? false,
9611
10128
  isActive: true
9612
10129
  });
9613
- authLogger.service.info(` \u2705 Created role: ${config2.name}`);
10130
+ authLogger.service.info(` \u2705 Created role: ${config3.name}`);
9614
10131
  } else {
9615
10132
  const updateData = {
9616
- displayName: config2.displayName,
9617
- description: config2.description || null
10133
+ displayName: config3.displayName,
10134
+ description: config3.description || null
9618
10135
  };
9619
10136
  if (!existing.isBuiltin) {
9620
- updateData.priority = config2.priority ?? existing.priority;
10137
+ updateData.priority = config3.priority ?? existing.priority;
9621
10138
  }
9622
10139
  await rolesRepository.updateById(existing.id, updateData);
9623
10140
  }
9624
10141
  }
9625
10142
  }
9626
10143
  async function syncPermissions(configs, existingByName) {
9627
- for (const config2 of configs) {
9628
- const existing = existingByName.get(config2.name);
10144
+ for (const config3 of configs) {
10145
+ const existing = existingByName.get(config3.name);
9629
10146
  if (!existing) {
9630
10147
  await permissionsRepository.create({
9631
- name: config2.name,
9632
- displayName: config2.displayName,
9633
- description: config2.description || null,
9634
- category: config2.category || null,
9635
- isSystem: config2.isSystem ?? false,
9636
- isBuiltin: config2.isBuiltin ?? false,
10148
+ name: config3.name,
10149
+ displayName: config3.displayName,
10150
+ description: config3.description || null,
10151
+ category: config3.category || null,
10152
+ isSystem: config3.isSystem ?? false,
10153
+ isBuiltin: config3.isBuiltin ?? false,
9637
10154
  isActive: true,
9638
10155
  metadata: null
9639
10156
  });
9640
- authLogger.service.info(` \u2705 Created permission: ${config2.name}`);
10157
+ authLogger.service.info(` \u2705 Created permission: ${config3.name}`);
9641
10158
  } else {
9642
10159
  await permissionsRepository.updateById(existing.id, {
9643
- displayName: config2.displayName,
9644
- description: config2.description || null,
9645
- category: config2.category || null
10160
+ displayName: config3.displayName,
10161
+ description: config3.description || null,
10162
+ category: config3.category || null
9646
10163
  });
9647
10164
  }
9648
10165
  }
@@ -9703,7 +10220,7 @@ async function getUserPermissions(userId) {
9703
10220
  const permIds = rolePermMappings.map((rp) => rp.permissionId);
9704
10221
  if (permIds.length > 0) {
9705
10222
  const rolePerms = await Promise.all(
9706
- permIds.map((id14) => permissionsRepository.findById(id14))
10223
+ permIds.map((id15) => permissionsRepository.findById(id15))
9707
10224
  );
9708
10225
  for (const perm of rolePerms) {
9709
10226
  if (perm && perm.isActive) {
@@ -9916,20 +10433,20 @@ async function acceptInvitation(params) {
9916
10433
  async function listInvitations(params) {
9917
10434
  return await invitationsRepository.list(params);
9918
10435
  }
9919
- async function cancelInvitation(id14, cancelledBy, reason) {
9920
- const invitation = await invitationsRepository.findById(id14);
10436
+ async function cancelInvitation(id15, cancelledBy, reason) {
10437
+ const invitation = await invitationsRepository.findById(id15);
9921
10438
  if (!invitation) {
9922
10439
  throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
9923
10440
  }
9924
10441
  if (invitation.status !== "pending") {
9925
10442
  throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
9926
10443
  }
9927
- await invitationsRepository.cancel(id14, cancelledBy, reason, invitation.metadata);
10444
+ await invitationsRepository.cancel(id15, cancelledBy, reason, invitation.metadata);
9928
10445
  console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
9929
10446
  }
9930
- async function deleteInvitation(id14) {
9931
- await invitationsRepository.deleteById(id14);
9932
- console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id14}`);
10447
+ async function deleteInvitation(id15) {
10448
+ await invitationsRepository.deleteById(id15);
10449
+ console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id15}`);
9933
10450
  }
9934
10451
  async function expireOldInvitations() {
9935
10452
  const count = await invitationsRepository.updateExpiredInvitations();
@@ -9938,8 +10455,8 @@ async function expireOldInvitations() {
9938
10455
  }
9939
10456
  return count;
9940
10457
  }
9941
- async function resendInvitation(id14, expiresInDays = 7) {
9942
- const invitation = await invitationsRepository.findById(id14);
10458
+ async function resendInvitation(id15, expiresInDays = 7) {
10459
+ const invitation = await invitationsRepository.findById(id15);
9943
10460
  if (!invitation) {
9944
10461
  throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
9945
10462
  }
@@ -9947,7 +10464,7 @@ async function resendInvitation(id14, expiresInDays = 7) {
9947
10464
  throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
9948
10465
  }
9949
10466
  const newExpiresAt = calculateExpiresAt(expiresInDays);
9950
- const updated = await invitationsRepository.resend(id14, newExpiresAt);
10467
+ const updated = await invitationsRepository.resend(id15, newExpiresAt);
9951
10468
  if (!updated) {
9952
10469
  throw new Error("Failed to update invitation");
9953
10470
  }
@@ -9986,13 +10503,13 @@ async function getAuthSessionService(userId) {
9986
10503
  // src/server/lib/one-time-token.ts
9987
10504
  import { SSETokenManager } from "@spfn/core/event/sse";
9988
10505
  var manager = null;
9989
- function initOneTimeTokenManager(config2) {
10506
+ function initOneTimeTokenManager(config3) {
9990
10507
  if (manager) {
9991
10508
  manager.destroy();
9992
10509
  }
9993
10510
  manager = new SSETokenManager({
9994
- ttl: config2?.ttl,
9995
- store: config2?.store
10511
+ ttl: config3?.ttl,
10512
+ store: config3?.store
9996
10513
  });
9997
10514
  }
9998
10515
  function getOneTimeTokenManager() {
@@ -10104,8 +10621,8 @@ init_repositories();
10104
10621
  import { env as env12 } from "@spfn/auth/config";
10105
10622
  import { ValidationError as ValidationError8 } from "@spfn/core/errors";
10106
10623
  import {
10107
- AccountDisabledError as AccountDisabledError2,
10108
- AccountPendingDeletionError as AccountPendingDeletionError2,
10624
+ AccountDisabledError as AccountDisabledError3,
10625
+ AccountPendingDeletionError as AccountPendingDeletionError3,
10109
10626
  UnverifiedEmailLinkError
10110
10627
  } from "@spfn/auth/errors";
10111
10628
 
@@ -10140,10 +10657,10 @@ function getDefaultScopes() {
10140
10657
  }
10141
10658
  function getGoogleAuthUrl(state, scopes) {
10142
10659
  const resolvedScopes = scopes ?? getDefaultScopes();
10143
- const config2 = getGoogleOAuthConfig();
10660
+ const config3 = getGoogleOAuthConfig();
10144
10661
  const params = new URLSearchParams({
10145
- client_id: config2.clientId,
10146
- redirect_uri: config2.redirectUri,
10662
+ client_id: config3.clientId,
10663
+ redirect_uri: config3.redirectUri,
10147
10664
  response_type: "code",
10148
10665
  scope: resolvedScopes.join(" "),
10149
10666
  state,
@@ -10155,16 +10672,16 @@ function getGoogleAuthUrl(state, scopes) {
10155
10672
  return `${GOOGLE_AUTH_URL}?${params.toString()}`;
10156
10673
  }
10157
10674
  async function exchangeCodeForTokens(code) {
10158
- const config2 = getGoogleOAuthConfig();
10675
+ const config3 = getGoogleOAuthConfig();
10159
10676
  const response = await fetch(GOOGLE_TOKEN_URL, {
10160
10677
  method: "POST",
10161
10678
  headers: {
10162
10679
  "Content-Type": "application/x-www-form-urlencoded"
10163
10680
  },
10164
10681
  body: new URLSearchParams({
10165
- client_id: config2.clientId,
10166
- client_secret: config2.clientSecret,
10167
- redirect_uri: config2.redirectUri,
10682
+ client_id: config3.clientId,
10683
+ client_secret: config3.clientSecret,
10684
+ redirect_uri: config3.redirectUri,
10168
10685
  grant_type: "authorization_code",
10169
10686
  code
10170
10687
  })
@@ -10188,15 +10705,15 @@ async function getGoogleUserInfo(accessToken) {
10188
10705
  return response.json();
10189
10706
  }
10190
10707
  async function refreshAccessToken(refreshToken) {
10191
- const config2 = getGoogleOAuthConfig();
10708
+ const config3 = getGoogleOAuthConfig();
10192
10709
  const response = await fetch(GOOGLE_TOKEN_URL, {
10193
10710
  method: "POST",
10194
10711
  headers: {
10195
10712
  "Content-Type": "application/x-www-form-urlencoded"
10196
10713
  },
10197
10714
  body: new URLSearchParams({
10198
- client_id: config2.clientId,
10199
- client_secret: config2.clientSecret,
10715
+ client_id: config3.clientId,
10716
+ client_secret: config3.clientSecret,
10200
10717
  refresh_token: refreshToken,
10201
10718
  grant_type: "refresh_token"
10202
10719
  })
@@ -10261,8 +10778,8 @@ var registry2 = /* @__PURE__ */ new Map();
10261
10778
  function registerOAuthProvider(provider) {
10262
10779
  registry2.set(provider.id, provider);
10263
10780
  }
10264
- function getOAuthProvider(id14) {
10265
- return registry2.get(id14);
10781
+ function getOAuthProvider(id15) {
10782
+ return registry2.get(id15);
10266
10783
  }
10267
10784
  function getRegisteredProviders() {
10268
10785
  return [...registry2.values()];
@@ -10383,7 +10900,7 @@ var googleProvider = {
10383
10900
  registerOAuthProvider(googleProvider);
10384
10901
 
10385
10902
  // src/server/lib/oauth/apple-provider.ts
10386
- import { createHash as createHash2 } from "crypto";
10903
+ import { createHash as createHash3 } from "crypto";
10387
10904
  import { env as env11 } from "@spfn/auth/config";
10388
10905
  import { ValidationError as ValidationError4 } from "@spfn/core/errors";
10389
10906
  import { NativeSignInUnsupportedError as NativeSignInUnsupportedError2 } from "@spfn/auth/errors";
@@ -10393,7 +10910,7 @@ function getAppleClientIds() {
10393
10910
  return (env11.SPFN_AUTH_APPLE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
10394
10911
  }
10395
10912
  function hashNonce(rawNonce) {
10396
- return createHash2("sha256").update(rawNonce).digest("hex");
10913
+ return createHash3("sha256").update(rawNonce).digest("hex");
10397
10914
  }
10398
10915
  function unsupportedWebFlow() {
10399
10916
  throw new ValidationError4({
@@ -10516,21 +11033,21 @@ var githubProvider = {
10516
11033
  return !!(env3.SPFN_AUTH_GITHUB_CLIENT_ID && env3.SPFN_AUTH_GITHUB_CLIENT_SECRET);
10517
11034
  },
10518
11035
  getAuthUrl(state, scopes) {
10519
- const config2 = getGithubConfig();
11036
+ const config3 = getGithubConfig();
10520
11037
  const params = new URLSearchParams({
10521
- client_id: config2.clientId,
10522
- redirect_uri: config2.redirectUri,
11038
+ client_id: config3.clientId,
11039
+ redirect_uri: config3.redirectUri,
10523
11040
  state,
10524
11041
  scope: (scopes ?? getGithubScopes()).join(" ")
10525
11042
  });
10526
11043
  return `${GITHUB_AUTH_URL}?${params.toString()}`;
10527
11044
  },
10528
11045
  async exchangeCodeForTokens(code) {
10529
- const config2 = getGithubConfig();
11046
+ const config3 = getGithubConfig();
10530
11047
  return requestGithubTokens(new URLSearchParams({
10531
- client_id: config2.clientId,
10532
- client_secret: config2.clientSecret,
10533
- redirect_uri: config2.redirectUri,
11048
+ client_id: config3.clientId,
11049
+ client_secret: config3.clientSecret,
11050
+ redirect_uri: config3.redirectUri,
10534
11051
  code
10535
11052
  }));
10536
11053
  },
@@ -10560,11 +11077,11 @@ var githubProvider = {
10560
11077
  };
10561
11078
  },
10562
11079
  async refreshTokens(refreshToken) {
10563
- const config2 = getGithubConfig();
11080
+ const config3 = getGithubConfig();
10564
11081
  return requestGithubTokens(new URLSearchParams({
10565
11082
  grant_type: "refresh_token",
10566
- client_id: config2.clientId,
10567
- client_secret: config2.clientSecret,
11083
+ client_id: config3.clientId,
11084
+ client_secret: config3.clientSecret,
10568
11085
  refresh_token: refreshToken
10569
11086
  }));
10570
11087
  }
@@ -10687,26 +11204,26 @@ var kakaoProvider = {
10687
11204
  return !!env3.SPFN_AUTH_KAKAO_CLIENT_ID;
10688
11205
  },
10689
11206
  getAuthUrl(state, scopes) {
10690
- const config2 = getKakaoConfig();
11207
+ const config3 = getKakaoConfig();
10691
11208
  const params = new URLSearchParams({
10692
11209
  response_type: "code",
10693
- client_id: config2.clientId,
10694
- redirect_uri: config2.redirectUri,
11210
+ client_id: config3.clientId,
11211
+ redirect_uri: config3.redirectUri,
10695
11212
  state,
10696
11213
  scope: (scopes ?? getKakaoScopes()).join(",")
10697
11214
  });
10698
11215
  return `${KAKAO_AUTH_URL}?${params.toString()}`;
10699
11216
  },
10700
11217
  async exchangeCodeForTokens(code) {
10701
- const config2 = getKakaoConfig();
11218
+ const config3 = getKakaoConfig();
10702
11219
  const params = new URLSearchParams({
10703
11220
  grant_type: "authorization_code",
10704
- client_id: config2.clientId,
10705
- redirect_uri: config2.redirectUri,
11221
+ client_id: config3.clientId,
11222
+ redirect_uri: config3.redirectUri,
10706
11223
  code
10707
11224
  });
10708
- if (config2.clientSecret) {
10709
- params.set("client_secret", config2.clientSecret);
11225
+ if (config3.clientSecret) {
11226
+ params.set("client_secret", config3.clientSecret);
10710
11227
  }
10711
11228
  return requestKakaoTokens(params);
10712
11229
  },
@@ -10748,14 +11265,14 @@ var kakaoProvider = {
10748
11265
  return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
10749
11266
  },
10750
11267
  async refreshTokens(refreshToken) {
10751
- const config2 = getKakaoConfig();
11268
+ const config3 = getKakaoConfig();
10752
11269
  const params = new URLSearchParams({
10753
11270
  grant_type: "refresh_token",
10754
- client_id: config2.clientId,
11271
+ client_id: config3.clientId,
10755
11272
  refresh_token: refreshToken
10756
11273
  });
10757
- if (config2.clientSecret) {
10758
- params.set("client_secret", config2.clientSecret);
11274
+ if (config3.clientSecret) {
11275
+ params.set("client_secret", config3.clientSecret);
10759
11276
  }
10760
11277
  return requestKakaoTokens(params);
10761
11278
  },
@@ -10792,7 +11309,7 @@ registerOAuthProvider(kakaoProvider);
10792
11309
  init_config();
10793
11310
  import { ValidationError as ValidationError7 } from "@spfn/core/errors";
10794
11311
  import { NativeSignInUnsupportedError as NativeSignInUnsupportedError4 } from "@spfn/auth/errors";
10795
- import { createDecipheriv, createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
11312
+ import { createDecipheriv, createHash as createHash4, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
10796
11313
  var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
10797
11314
  var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
10798
11315
  var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
@@ -10842,7 +11359,7 @@ async function requestNaverTokens(params) {
10842
11359
  };
10843
11360
  }
10844
11361
  function deriveNaverUnlinkKey(clientSecret) {
10845
- return createHash3("md5").update(clientSecret).digest().subarray(0, 16);
11362
+ return createHash4("md5").update(clientSecret).digest().subarray(0, 16);
10846
11363
  }
10847
11364
  function decodeBase64Url(value) {
10848
11365
  return Buffer.from(value.replace(/-/g, "+").replace(/_/g, "/"), "base64");
@@ -10913,22 +11430,22 @@ var naverProvider = {
10913
11430
  return !!(env3.SPFN_AUTH_NAVER_CLIENT_ID && env3.SPFN_AUTH_NAVER_CLIENT_SECRET);
10914
11431
  },
10915
11432
  getAuthUrl(state) {
10916
- const config2 = getNaverConfig();
11433
+ const config3 = getNaverConfig();
10917
11434
  const params = new URLSearchParams({
10918
11435
  response_type: "code",
10919
- client_id: config2.clientId,
10920
- redirect_uri: config2.redirectUri,
11436
+ client_id: config3.clientId,
11437
+ redirect_uri: config3.redirectUri,
10921
11438
  state
10922
11439
  });
10923
11440
  return `${NAVER_AUTH_URL}?${params.toString()}`;
10924
11441
  },
10925
11442
  async exchangeCodeForTokens(code, options) {
10926
- const config2 = getNaverConfig();
11443
+ const config3 = getNaverConfig();
10927
11444
  return requestNaverTokens(new URLSearchParams({
10928
11445
  grant_type: "authorization_code",
10929
- client_id: config2.clientId,
10930
- client_secret: config2.clientSecret,
10931
- redirect_uri: config2.redirectUri,
11446
+ client_id: config3.clientId,
11447
+ client_secret: config3.clientSecret,
11448
+ redirect_uri: config3.redirectUri,
10932
11449
  code,
10933
11450
  state: options.state
10934
11451
  }));
@@ -10969,11 +11486,11 @@ var naverProvider = {
10969
11486
  return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
10970
11487
  },
10971
11488
  async refreshTokens(refreshToken) {
10972
- const config2 = getNaverConfig();
11489
+ const config3 = getNaverConfig();
10973
11490
  return requestNaverTokens(new URLSearchParams({
10974
11491
  grant_type: "refresh_token",
10975
- client_id: config2.clientId,
10976
- client_secret: config2.clientSecret,
11492
+ client_id: config3.clientId,
11493
+ client_secret: config3.clientSecret,
10977
11494
  refresh_token: refreshToken
10978
11495
  }));
10979
11496
  },
@@ -11142,9 +11659,9 @@ async function assertActiveForOAuthSession(userId) {
11142
11659
  }
11143
11660
  if (user.status === "pending_deletion") {
11144
11661
  const pending = await getPendingDeletionInfo(user.id);
11145
- throw new AccountPendingDeletionError2({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
11662
+ throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
11146
11663
  }
11147
- throw new AccountDisabledError2({ status: user.status });
11664
+ throw new AccountDisabledError3({ status: user.status });
11148
11665
  }
11149
11666
  async function backfillVerifiedEmail(userId, identity) {
11150
11667
  if (!identity.email || !identity.emailVerified) {
@@ -11287,9 +11804,9 @@ async function oauthUnlinkNotifyService(provider, notification) {
11287
11804
  }
11288
11805
 
11289
11806
  // src/server/services/oauth-native.service.ts
11290
- import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
11807
+ import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit3 } from "@spfn/core/db";
11291
11808
  import {
11292
- InvalidKeyFingerprintError as InvalidKeyFingerprintError2,
11809
+ InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
11293
11810
  NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
11294
11811
  NonceKeyBindingError
11295
11812
  } from "@spfn/auth/errors";
@@ -11316,7 +11833,7 @@ function assertNonceBindsPublicKey(params) {
11316
11833
  throw new NonceKeyBindingError();
11317
11834
  }
11318
11835
  if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
11319
- throw new InvalidKeyFingerprintError2();
11836
+ throw new InvalidKeyFingerprintError3();
11320
11837
  }
11321
11838
  }
11322
11839
  async function persistNativeLogin(identity, params) {
@@ -11352,23 +11869,23 @@ async function persistNativeLogin(identity, params) {
11352
11869
  email: identity.email || void 0,
11353
11870
  metadata: params.metadata
11354
11871
  };
11355
- onAfterCommit2(() => (isNewUser ? authRegisterEvent : authLoginEvent).emit(eventPayload));
11872
+ onAfterCommit3(() => (isNewUser ? authRegisterEvent : authLoginEvent).emit(eventPayload));
11356
11873
  return { userId: String(userId), keyId: params.keyId, isNewUser };
11357
11874
  }, { context: "auth:oauth-native" });
11358
11875
  }
11359
11876
 
11360
11877
  // src/server/services/ops-token.service.ts
11361
11878
  init_ops_tokens_repository();
11362
- import { createHash as createHash4, randomBytes } from "crypto";
11879
+ import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
11363
11880
  var OPS_TOKEN_PREFIX = "spfn_ops_";
11364
11881
  function hashOpsToken(token) {
11365
- return createHash4("sha256").update(token).digest("hex");
11882
+ return createHash5("sha256").update(token).digest("hex");
11366
11883
  }
11367
11884
  async function issueOpsTokenService(name, scopes, expiresAt) {
11368
11885
  if (scopes.length === 0) {
11369
11886
  throw new Error("An ops token needs at least one scope ('*' grants all).");
11370
11887
  }
11371
- const token = OPS_TOKEN_PREFIX + randomBytes(32).toString("hex");
11888
+ const token = OPS_TOKEN_PREFIX + randomBytes2(32).toString("hex");
11372
11889
  const record = await opsTokensRepository.create({
11373
11890
  name,
11374
11891
  tokenHash: hashOpsToken(token),
@@ -11398,8 +11915,8 @@ async function verifyOpsTokenService(token) {
11398
11915
  scopes: record.scopes
11399
11916
  };
11400
11917
  }
11401
- async function revokeOpsTokenService(id14) {
11402
- return await opsTokensRepository.revokeById(id14);
11918
+ async function revokeOpsTokenService(id15) {
11919
+ return await opsTokensRepository.revokeById(id15);
11403
11920
  }
11404
11921
  async function listOpsTokensService() {
11405
11922
  return await opsTokensRepository.list();
@@ -11412,7 +11929,7 @@ import { rateLimitPolicy } from "@spfn/core/middleware";
11412
11929
 
11413
11930
  // src/server/lib/rate-limit-keys.ts
11414
11931
  init_email();
11415
- import { createHash as createHash5 } from "crypto";
11932
+ import { createHash as createHash6 } from "crypto";
11416
11933
  import { getClientIp } from "@spfn/core/middleware";
11417
11934
  async function readJsonBody(c) {
11418
11935
  try {
@@ -11448,11 +11965,20 @@ function byIpAndAccount(options = {}) {
11448
11965
  ];
11449
11966
  };
11450
11967
  }
11968
+ function byIpAndCaller(options = {}) {
11969
+ return async (c) => {
11970
+ const auth = getOptionalAuth(c);
11971
+ return [
11972
+ { key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
11973
+ auth ? `caller:${auth.userId}` : void 0
11974
+ ];
11975
+ };
11976
+ }
11451
11977
  function idTokenKey(body) {
11452
11978
  if (typeof body.idToken !== "string" || !body.idToken) {
11453
11979
  return void 0;
11454
11980
  }
11455
- return `tok:${createHash5("sha256").update(body.idToken).digest("hex")}`;
11981
+ return `tok:${createHash6("sha256").update(body.idToken).digest("hex")}`;
11456
11982
  }
11457
11983
  function byIpAndIdToken(options = {}) {
11458
11984
  return async (c) => {
@@ -11610,6 +12136,69 @@ var login = route.post("/_auth/login").input({
11610
12136
  const { body } = await c.data();
11611
12137
  return await loginService(body);
11612
12138
  });
12139
+ var startDeviceAuth = route.post("/_auth/device/start").input({
12140
+ // Bounded, unlike the interceptor bodies the authenticated enrolment
12141
+ // routes use: this is the one place key material arrives from a caller
12142
+ // with nothing to authenticate and is stored before anyone has agreed to
12143
+ // it. Validation refuses an oversize payload before it reaches a row.
12144
+ body: Type.Object({
12145
+ publicKey: PublicKeySchema,
12146
+ keyId: KeyIdSchema,
12147
+ fingerprint: FingerprintSchema,
12148
+ algorithm: Type.Optional(Type.Union(
12149
+ KEY_ALGORITHM.map((algo) => Type.Literal(algo)),
12150
+ { description: "Signature algorithm" }
12151
+ )),
12152
+ deviceName: Type.Optional(DeviceNameSchema),
12153
+ platform: Type.Optional(PlatformSchema)
12154
+ })
12155
+ }).use([rateLimitPolicy("auth-device-start", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
12156
+ const { body } = await c.data();
12157
+ return await startDeviceAuthService(body);
12158
+ });
12159
+ var pollDeviceAuth = route.post("/_auth/device/poll").input({
12160
+ body: Type.Object({
12161
+ deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
12162
+ })
12163
+ }).use([rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
12164
+ const { body } = await c.data();
12165
+ return await pollDeviceAuthService(body);
12166
+ });
12167
+ var getDeviceAuthInfo = route.post("/_auth/device/info").input({
12168
+ body: Type.Object({
12169
+ userCode: UserCodeSchema
12170
+ })
12171
+ }).use([
12172
+ rateLimitPolicy("auth-device-info", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
12173
+ Transactional()
12174
+ ]).handler(async (c) => {
12175
+ const { body } = await c.data();
12176
+ return await getDeviceAuthInfoService(body);
12177
+ });
12178
+ var approveDeviceAuth = route.post("/_auth/device/approve").input({
12179
+ body: Type.Object({
12180
+ userCode: UserCodeSchema
12181
+ })
12182
+ }).use([
12183
+ rateLimitPolicy("auth-device-approve", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
12184
+ Transactional()
12185
+ ]).handler(async (c) => {
12186
+ const { body } = await c.data();
12187
+ const { userId } = getAuth(c);
12188
+ return await approveDeviceAuthService({ userCode: body.userCode, userId: Number(userId) });
12189
+ });
12190
+ var denyDeviceAuth = route.post("/_auth/device/deny").input({
12191
+ body: Type.Object({
12192
+ userCode: UserCodeSchema
12193
+ })
12194
+ }).use([
12195
+ rateLimitPolicy("auth-device-deny", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
12196
+ Transactional()
12197
+ ]).handler(async (c) => {
12198
+ const { body } = await c.data();
12199
+ await denyDeviceAuthService(body);
12200
+ return c.noContent();
12201
+ });
11613
12202
  var logout = route.post("/_auth/logout").handler(async (c) => {
11614
12203
  const auth = getAuth(c);
11615
12204
  if (!auth) {
@@ -11718,6 +12307,11 @@ var authRouter = defineRouter({
11718
12307
  verifyCode,
11719
12308
  register,
11720
12309
  login,
12310
+ startDeviceAuth,
12311
+ pollDeviceAuth,
12312
+ getDeviceAuthInfo,
12313
+ approveDeviceAuth,
12314
+ denyDeviceAuth,
11721
12315
  logout,
11722
12316
  rotateKey,
11723
12317
  listKeys,
@@ -11732,8 +12326,8 @@ var authRouter = defineRouter({
11732
12326
  import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
11733
12327
 
11734
12328
  // src/server/middleware/authenticate.ts
11735
- import { defineMiddleware } from "@spfn/core/route";
11736
- import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
12329
+ import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
12330
+ import { UnauthorizedError as UnauthorizedError3 } from "@spfn/core/errors";
11737
12331
  import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger3, keysRepository as keysRepository3, usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
11738
12332
  import {
11739
12333
  InvalidTokenError,
@@ -11742,7 +12336,7 @@ import {
11742
12336
  } from "@spfn/auth/errors";
11743
12337
 
11744
12338
  // src/server/client-proof/refusal.ts
11745
- import { randomBytes as randomBytes2 } from "crypto";
12339
+ import { randomBytes as randomBytes3 } from "crypto";
11746
12340
 
11747
12341
  // src/server/client-proof/canonical-json.ts
11748
12342
  var CanonicalJsonError = class extends Error {
@@ -11755,13 +12349,13 @@ var CanonicalJsonError = class extends Error {
11755
12349
  var INT64_MIN = -(2n ** 63n);
11756
12350
  var INT64_MAX = 2n ** 63n - 1n;
11757
12351
  function parseCanonicalJson(bytes) {
11758
- let text14;
12352
+ let text15;
11759
12353
  try {
11760
- text14 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
12354
+ text15 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
11761
12355
  } catch {
11762
12356
  throw new CanonicalJsonError("INVALID_UTF8");
11763
12357
  }
11764
- const parser = new Parser(text14);
12358
+ const parser = new Parser(text15);
11765
12359
  const value = parser.parseValue();
11766
12360
  parser.skipWhitespace();
11767
12361
  if (!parser.atEnd()) {
@@ -11782,8 +12376,8 @@ function isCanonicalBytes(bytes, value) {
11782
12376
  return true;
11783
12377
  }
11784
12378
  var Parser = class {
11785
- constructor(text14) {
11786
- this.text = text14;
12379
+ constructor(text15) {
12380
+ this.text = text15;
11787
12381
  }
11788
12382
  pos = 0;
11789
12383
  atEnd() {
@@ -12100,7 +12694,7 @@ var HTTP_STATUS = {
12100
12694
  CONTRACT_UNSUPPORTED: 409
12101
12695
  };
12102
12696
  function newHexId() {
12103
- return randomBytes2(16).toString("hex");
12697
+ return randomBytes3(16).toString("hex");
12104
12698
  }
12105
12699
  var ClientProofRefusal = class _ClientProofRefusal {
12106
12700
  constructor(code, message) {
@@ -12202,7 +12796,7 @@ function contractViolation(message) {
12202
12796
  }
12203
12797
 
12204
12798
  // src/server/client-proof/contract-bundle.ts
12205
- import { createHash as createHash7 } from "crypto";
12799
+ import { createHash as createHash8 } from "crypto";
12206
12800
  import {
12207
12801
  CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
12208
12802
  ServerTimeResponseSchema
@@ -12210,7 +12804,7 @@ import {
12210
12804
  init_types();
12211
12805
 
12212
12806
  // src/server/client-proof/proof.ts
12213
- import { createHash as createHash6, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
12807
+ import { createHash as createHash7, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
12214
12808
  var CLIENT_PROOF_PROFILE = "clientProofV1";
12215
12809
  var ABSENT_BODY_SHA256 = "0".repeat(64);
12216
12810
  var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
@@ -12280,7 +12874,7 @@ function verifyClientProof(input, presentedProof, publicKey) {
12280
12874
  );
12281
12875
  }
12282
12876
  function sha256Hex(bytes) {
12283
- return createHash6("sha256").update(bytes).digest("hex");
12877
+ return createHash7("sha256").update(bytes).digest("hex");
12284
12878
  }
12285
12879
 
12286
12880
  // src/server/client-proof/admission.ts
@@ -12372,8 +12966,8 @@ var CORE_PREREQUISITE_OPERATIONS = [
12372
12966
 
12373
12967
  // src/server/client-proof/contract-bundle.ts
12374
12968
  init_wire_headers();
12375
- var CONTRACT_VERSION = "0.9.0";
12376
- var CONTRACT_SUPPORTED_RANGE = ">=0.9.0 <0.10.0";
12969
+ var CONTRACT_VERSION = "0.10.0";
12970
+ var CONTRACT_SUPPORTED_RANGE = ">=0.10.0 <0.11.0";
12377
12971
  function required(name, type) {
12378
12972
  return { name, type, optional: false };
12379
12973
  }
@@ -12544,7 +13138,7 @@ var CONTRACT_TYPES = [
12544
13138
  fields: [
12545
13139
  required("keyId", "string"),
12546
13140
  optional("deviceName", "string"),
12547
- optional("platform", "string"),
13141
+ optional("platform", "KeyPlatform"),
12548
13142
  required("algorithm", "KeyAlgorithm"),
12549
13143
  required("fingerprintPrefix", "string"),
12550
13144
  required("createdAtMillis", "integer"),
@@ -12586,10 +13180,94 @@ var CONTRACT_TYPES = [
12586
13180
  required("revokedCount", "integer"),
12587
13181
  required("currentKeyRevoked", "boolean")
12588
13182
  ]
13183
+ },
13184
+ {
13185
+ name: "StartDeviceAuthRequest",
13186
+ fields: [
13187
+ required("publicKey", "string"),
13188
+ required("keyId", "string"),
13189
+ required("fingerprint", "string"),
13190
+ optional("algorithm", "KeyAlgorithm"),
13191
+ optional("deviceName", "string"),
13192
+ optional("platform", "KeyPlatform")
13193
+ ]
13194
+ },
13195
+ {
13196
+ name: "StartDeviceAuthResponse",
13197
+ fields: [
13198
+ required("deviceCode", "string"),
13199
+ required("userCode", "string"),
13200
+ required("expiresAtMillis", "integer"),
13201
+ required("intervalMillis", "integer")
13202
+ ]
13203
+ },
13204
+ {
13205
+ name: "PollDeviceAuthRequest",
13206
+ fields: [
13207
+ required("deviceCode", "string")
13208
+ ]
13209
+ },
13210
+ /**
13211
+ * The poll union, flattened into the one shape this grammar can carry.
13212
+ *
13213
+ * `status` is the discriminant and the only required field; everything else
13214
+ * belongs to one branch and is therefore optional. `intervalMillis` is the
13215
+ * pending branch, and the five after it are the approved branch — the same
13216
+ * fields `LoginResponse` carries, because an approved poll is the login the
13217
+ * approval produced. `deviceAuthorization.pollStatusRule` states the pairing
13218
+ * the grammar cannot.
13219
+ */
13220
+ {
13221
+ name: "PollDeviceAuthResponse",
13222
+ fields: [
13223
+ required("status", "DeviceAuthPollStatus"),
13224
+ optional("intervalMillis", "integer"),
13225
+ optional("userId", "string"),
13226
+ optional("publicId", "string"),
13227
+ optional("email", "string"),
13228
+ optional("phone", "string"),
13229
+ optional("passwordChangeRequired", "boolean")
13230
+ ]
13231
+ },
13232
+ /**
13233
+ * Info, approve and deny each declare their own request type although all
13234
+ * three carry nothing but `userCode`. An operation's request shape is its
13235
+ * own: a field added to one of them later must not appear on the other two
13236
+ * by accident, which is what a shared type would do.
13237
+ */
13238
+ {
13239
+ name: "DeviceAuthInfoRequest",
13240
+ fields: [
13241
+ required("userCode", "string")
13242
+ ]
13243
+ },
13244
+ {
13245
+ name: "DeviceAuthInfoResponse",
13246
+ fields: [
13247
+ optional("deviceName", "string"),
13248
+ optional("platform", "KeyPlatform"),
13249
+ required("fingerprintPrefix", "string"),
13250
+ required("requestedAtMillis", "integer"),
13251
+ required("expiresAtMillis", "integer")
13252
+ ]
13253
+ },
13254
+ {
13255
+ name: "ApproveDeviceAuthRequest",
13256
+ fields: [
13257
+ required("userCode", "string")
13258
+ ]
13259
+ },
13260
+ {
13261
+ name: "DenyDeviceAuthRequest",
13262
+ fields: [
13263
+ required("userCode", "string")
13264
+ ]
12589
13265
  }
12590
13266
  ];
12591
13267
  var CONTRACT_ENUMS = [
12592
- { name: "KeyAlgorithm", values: [...KEY_ALGORITHM] }
13268
+ { name: "KeyAlgorithm", values: [...KEY_ALGORITHM] },
13269
+ { name: "KeyPlatform", values: [...KEY_PLATFORM] },
13270
+ { name: "DeviceAuthPollStatus", values: ["pending", "approved"] }
12593
13271
  ];
12594
13272
  var BUNDLE_FILENAME = "spfn-mobile-contract.json";
12595
13273
  var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
@@ -12623,7 +13301,7 @@ import {
12623
13301
  userProfilesRepository as userProfilesRepository2,
12624
13302
  getPendingDeletionInfo as getPendingDeletionInfo2
12625
13303
  } from "@spfn/auth/server";
12626
- import { AccountDisabledError as AccountDisabledError3, AccountPendingDeletionError as AccountPendingDeletionError3 } from "@spfn/auth/errors";
13304
+ import { AccountDisabledError as AccountDisabledError4, AccountPendingDeletionError as AccountPendingDeletionError4 } from "@spfn/auth/errors";
12627
13305
 
12628
13306
  // src/server/client-proof/refusal-response.ts
12629
13307
  function clientProofRefusalResponse(c, refusal) {
@@ -12718,9 +13396,9 @@ async function resolveAuthenticatedUser(userId) {
12718
13396
  if (user.status !== "active") {
12719
13397
  if (user.status === "pending_deletion") {
12720
13398
  const pending = await getPendingDeletionInfo2(user.id);
12721
- throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
13399
+ throw new AccountPendingDeletionError4({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
12722
13400
  }
12723
- throw new AccountDisabledError3({ status: user.status });
13401
+ throw new AccountDisabledError4({ status: user.status });
12724
13402
  }
12725
13403
  return { user, role: role?.name ?? null, locale };
12726
13404
  }
@@ -12739,13 +13417,20 @@ var ClientProofRefusalError = class extends SerializableError {
12739
13417
  function refusalError(refusal) {
12740
13418
  return new ClientProofRefusalError(refusal);
12741
13419
  }
13420
+ function clientProofRefusalOf(err) {
13421
+ return err instanceof ClientProofRefusalError ? err.refusal : null;
13422
+ }
12742
13423
  async function runAuthProfile(c) {
12743
13424
  try {
12744
13425
  const verifier = selectAuthProfile(c);
12745
13426
  if (verifier === null) {
12746
13427
  return { kind: "none" };
12747
13428
  }
12748
- return { kind: "authenticated", auth: await verifier.verify(c) };
13429
+ const auth = await verifier.verify(c);
13430
+ if (!auth?.userId) {
13431
+ throw new UnauthorizedError({ message: "Auth profile verifier returned no principal" });
13432
+ }
13433
+ return { kind: "authenticated", auth };
12749
13434
  } catch (err) {
12750
13435
  if (err instanceof ClientProofRefusalError) {
12751
13436
  return { kind: "refused", response: clientProofRefusalResponse(c, err.refusal) };
@@ -12867,9 +13552,185 @@ function verifyProofOrThrow(proofInput, credentials, publicKeySpkiDerBase64) {
12867
13552
  var AUTH_PROFILE_VERIFIERS = /* @__PURE__ */ new Map([
12868
13553
  [CLIENT_PROOF_PROFILE, { verify: verifyClientProofProfile }]
12869
13554
  ]);
13555
+ function registerAuthProfile(profileId, verifier) {
13556
+ if (typeof profileId !== "string" || profileId.length === 0) {
13557
+ throw new Error("registerAuthProfile: profileId must be a non-empty string");
13558
+ }
13559
+ if (typeof verifier?.verify !== "function") {
13560
+ throw new Error(`registerAuthProfile: auth profile '${profileId}' needs a verifier with a callable verify(c)`);
13561
+ }
13562
+ if (AUTH_PROFILE_VERIFIERS.has(profileId)) {
13563
+ throw new Error(`registerAuthProfile: auth profile '${profileId}' is already registered`);
13564
+ }
13565
+ AUTH_PROFILE_VERIFIERS.set(profileId, { verify: verifier.verify.bind(verifier) });
13566
+ }
13567
+
13568
+ // src/server/middleware/machine-principals.ts
13569
+ import { decodeProtectedHeader } from "jose";
13570
+ import { defineMiddleware } from "@spfn/core/route";
13571
+ import { ForbiddenError as ForbiddenError2, UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
13572
+ function getMachinePrincipal(c) {
13573
+ return c.get("machinePrincipal") ?? null;
13574
+ }
13575
+ var TOKEN_PREFIX_VERIFIERS = [];
13576
+ var KID_PREFIX_VERIFIERS = [];
13577
+ var REGISTERED_IDS = /* @__PURE__ */ new Set();
13578
+ function readDiscriminator(match) {
13579
+ const tokenPrefix = match?.tokenPrefix;
13580
+ const kidPrefix = match?.kidPrefix;
13581
+ if (tokenPrefix !== void 0 && kidPrefix !== void 0) {
13582
+ return null;
13583
+ }
13584
+ if (typeof tokenPrefix === "string" && tokenPrefix.length > 0) {
13585
+ return { kind: "tokenPrefix", prefix: tokenPrefix };
13586
+ }
13587
+ if (typeof kidPrefix === "string" && kidPrefix.length > 0) {
13588
+ return { kind: "kidPrefix", prefix: kidPrefix };
13589
+ }
13590
+ return null;
13591
+ }
13592
+ function prefixesCollide(a, b) {
13593
+ return a.startsWith(b) || b.startsWith(a);
13594
+ }
13595
+ function registerMachineVerifier(reg) {
13596
+ if (typeof reg?.id !== "string" || reg.id.length === 0) {
13597
+ throw new Error("registerMachineVerifier: id must be a non-empty string");
13598
+ }
13599
+ if (typeof reg.verify !== "function") {
13600
+ throw new Error(`registerMachineVerifier: '${reg.id}' needs a verifier with a callable verify(token, c)`);
13601
+ }
13602
+ const discriminator = readDiscriminator(reg.match);
13603
+ if (discriminator === null) {
13604
+ throw new Error(`registerMachineVerifier: '${reg.id}' needs exactly one non-empty discriminator \u2014 { tokenPrefix } or { kidPrefix }`);
13605
+ }
13606
+ if (REGISTERED_IDS.has(reg.id)) {
13607
+ throw new Error(`registerMachineVerifier: '${reg.id}' is already registered`);
13608
+ }
13609
+ const peers = discriminator.kind === "tokenPrefix" ? TOKEN_PREFIX_VERIFIERS : KID_PREFIX_VERIFIERS;
13610
+ const shadowed = peers.find((peer) => prefixesCollide(peer.prefix, discriminator.prefix));
13611
+ if (shadowed !== void 0) {
13612
+ throw new Error(
13613
+ `registerMachineVerifier: '${reg.id}' ${discriminator.kind} '${discriminator.prefix}' collides with '${shadowed.id}' \u2014 one prefix would shadow the other`
13614
+ );
13615
+ }
13616
+ peers.push({ id: reg.id, prefix: discriminator.prefix, verify: reg.verify.bind(reg) });
13617
+ REGISTERED_IDS.add(reg.id);
13618
+ }
13619
+ function findMachineVerifier(token) {
13620
+ const byTokenPrefix = TOKEN_PREFIX_VERIFIERS.find((entry) => token.startsWith(entry.prefix));
13621
+ if (byTokenPrefix !== void 0) {
13622
+ return byTokenPrefix;
13623
+ }
13624
+ if (KID_PREFIX_VERIFIERS.length === 0) {
13625
+ return null;
13626
+ }
13627
+ const kid = readProtectedKid(token);
13628
+ if (kid === null) {
13629
+ return null;
13630
+ }
13631
+ return KID_PREFIX_VERIFIERS.find((entry) => kid.startsWith(entry.prefix)) ?? null;
13632
+ }
13633
+ function matchesMachineDiscriminator(token) {
13634
+ return findMachineVerifier(token) !== null;
13635
+ }
13636
+ function readProtectedKid(token) {
13637
+ try {
13638
+ const { kid } = decodeProtectedHeader(token);
13639
+ return typeof kid === "string" ? kid : null;
13640
+ } catch {
13641
+ return null;
13642
+ }
13643
+ }
13644
+ var MACHINE_REFUSAL_MESSAGE = "Machine authentication required: Authorization: Bearer <token>";
13645
+ function machineRefusal() {
13646
+ return new UnauthorizedError2({ message: MACHINE_REFUSAL_MESSAGE });
13647
+ }
13648
+ var machineAuth = defineMiddleware("machineAuth", async (c, next) => {
13649
+ const refused = profileChannelRefusal(c);
13650
+ if (refused !== null) {
13651
+ return refused;
13652
+ }
13653
+ const token = extractBearer(c.req.header("Authorization"));
13654
+ if (token === null) {
13655
+ throw machineRefusal();
13656
+ }
13657
+ const entry = findMachineVerifier(token);
13658
+ if (entry === null) {
13659
+ throw machineRefusal();
13660
+ }
13661
+ const principal = await verifiedPrincipal(entry, token, c);
13662
+ if (principal === null) {
13663
+ throw machineRefusal();
13664
+ }
13665
+ c.set("machinePrincipal", principal);
13666
+ await next();
13667
+ return void 0;
13668
+ }, { skips: ["auth"] });
13669
+ function profileChannelRefusal(c) {
13670
+ try {
13671
+ selectAuthProfile(c);
13672
+ return null;
13673
+ } catch (err) {
13674
+ const refusal = clientProofRefusalOf(err);
13675
+ if (refusal === null) {
13676
+ throw err;
13677
+ }
13678
+ return clientProofRefusalResponse(c, refusal);
13679
+ }
13680
+ }
13681
+ async function verifiedPrincipal(entry, token, c) {
13682
+ try {
13683
+ const principal = copyPrincipal(await entry.verify(token, c), entry.id);
13684
+ if (principal === null) {
13685
+ authLogger.middleware.error(`machine verifier '${entry.id}' resolved no principal`);
13686
+ }
13687
+ return principal;
13688
+ } catch (err) {
13689
+ authLogger.middleware.error(`machine verifier '${entry.id}' refused or failed`, err);
13690
+ return null;
13691
+ }
13692
+ }
13693
+ function copyPrincipal(resolved, scheme) {
13694
+ const subjectType = resolved?.subjectType;
13695
+ const subjectId = resolved?.subjectId;
13696
+ const scopes = resolved?.scopes;
13697
+ const claims = resolved?.claims;
13698
+ if (typeof subjectType !== "string" || subjectType.length === 0 || typeof subjectId !== "string" || subjectId.length === 0 || !Array.isArray(scopes)) {
13699
+ return null;
13700
+ }
13701
+ return {
13702
+ subjectType,
13703
+ subjectId,
13704
+ scopes: [...scopes],
13705
+ claims: claims === void 0 ? void 0 : structuredClone(claims),
13706
+ scheme
13707
+ };
13708
+ }
13709
+ var requireMachineScope = defineMiddleware(
13710
+ "machineScope",
13711
+ (...scopes) => async (c, next) => {
13712
+ const principal = getMachinePrincipal(c);
13713
+ if (!principal) {
13714
+ throw machineRefusal();
13715
+ }
13716
+ const granted = new Set(principal.scopes);
13717
+ const missing = scopes.filter((scope) => !granted.has(scope));
13718
+ if (missing.length > 0) {
13719
+ throw new ForbiddenError2({ message: `Machine principal lacks scope: ${missing.join(", ")}` });
13720
+ }
13721
+ await next();
13722
+ }
13723
+ );
13724
+ function extractBearer(header) {
13725
+ if (!header || !header.startsWith("Bearer ")) {
13726
+ return null;
13727
+ }
13728
+ return header.substring(7);
13729
+ }
12870
13730
 
12871
13731
  // src/server/middleware/authenticate.ts
12872
- var authenticate = defineMiddleware("auth", async (c, next) => {
13732
+ var INVALID_TOKEN_MESSAGE = "Invalid token: missing keyId";
13733
+ var authenticate = defineMiddleware2("auth", async (c, next) => {
12873
13734
  const profile = await runAuthProfile(c);
12874
13735
  if (profile.kind === "refused") {
12875
13736
  return profile.response;
@@ -12885,17 +13746,21 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
12885
13746
  headers: c.req.header(),
12886
13747
  path: c.req.path
12887
13748
  });
12888
- throw new UnauthorizedError2({ message: "Authentication header missing or invalid: Bearer {token}" });
13749
+ throw new UnauthorizedError3({ message: "Authentication header missing or invalid: Bearer {token}" });
12889
13750
  }
12890
13751
  const token = authHeader.substring(7);
13752
+ if (matchesMachineDiscriminator(token)) {
13753
+ authLogger3.middleware.warn("Machine credential presented to the user path \u2014 refused", { path: c.req.path });
13754
+ throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
13755
+ }
12891
13756
  const decoded = decodeToken2(token);
12892
13757
  if (!decoded || !decoded.keyId) {
12893
- throw new UnauthorizedError2({ message: "Invalid token: missing keyId" });
13758
+ throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
12894
13759
  }
12895
13760
  const keyId = decoded.keyId;
12896
13761
  const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
12897
13762
  if (!keyRecord) {
12898
- throw new UnauthorizedError2({ message: "Invalid or revoked key" });
13763
+ throw new UnauthorizedError3({ message: "Invalid or revoked key" });
12899
13764
  }
12900
13765
  if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
12901
13766
  throw new KeyExpiredError();
@@ -12916,7 +13781,7 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
12916
13781
  throw new InvalidTokenError({ message: "Invalid token signature" });
12917
13782
  }
12918
13783
  }
12919
- throw new UnauthorizedError2({ message: "Authentication failed" });
13784
+ throw new UnauthorizedError3({ message: "Authentication failed" });
12920
13785
  }
12921
13786
  const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
12922
13787
  keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
@@ -12942,7 +13807,7 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
12942
13807
  await next();
12943
13808
  return void 0;
12944
13809
  });
12945
- var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
13810
+ var optionalAuth = defineMiddleware2("optionalAuth", async (c, next) => {
12946
13811
  const profile = await runAuthProfile(c);
12947
13812
  if (profile.kind === "refused") {
12948
13813
  return profile.response;
@@ -12958,6 +13823,10 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
12958
13823
  return void 0;
12959
13824
  }
12960
13825
  const token = authHeader.substring(7);
13826
+ if (matchesMachineDiscriminator(token)) {
13827
+ authLogger3.middleware.warn("Machine credential presented to the user path \u2014 refused", { path: c.req.path });
13828
+ throw new UnauthorizedError3({ message: INVALID_TOKEN_MESSAGE });
13829
+ }
12961
13830
  try {
12962
13831
  const decoded = decodeToken2(token);
12963
13832
  if (!decoded || !decoded.keyId) {
@@ -13004,11 +13873,11 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
13004
13873
  }, { skips: ["auth"] });
13005
13874
 
13006
13875
  // src/server/middleware/require-permission.ts
13007
- import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
13008
- import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
13876
+ import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
13877
+ import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
13009
13878
  import { InsufficientPermissionsError } from "@spfn/auth/errors";
13010
13879
  import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as authLogger4 } from "@spfn/auth/server";
13011
- var requirePermissions = defineMiddleware2(
13880
+ var requirePermissions = defineMiddleware3(
13012
13881
  "permission",
13013
13882
  (...permissionNames) => async (c, next) => {
13014
13883
  const auth = getAuth2(c);
@@ -13017,7 +13886,7 @@ var requirePermissions = defineMiddleware2(
13017
13886
  permissions: permissionNames,
13018
13887
  path: c.req.path
13019
13888
  });
13020
- throw new ForbiddenError2({ message: "Authentication required" });
13889
+ throw new ForbiddenError3({ message: "Authentication required" });
13021
13890
  }
13022
13891
  const { userId } = auth;
13023
13892
  const allowed = await hasAllPermissions2(userId, permissionNames);
@@ -13036,7 +13905,7 @@ var requirePermissions = defineMiddleware2(
13036
13905
  await next();
13037
13906
  }
13038
13907
  );
13039
- var requireAnyPermission = defineMiddleware2(
13908
+ var requireAnyPermission = defineMiddleware3(
13040
13909
  "anyPermission",
13041
13910
  (...permissionNames) => async (c, next) => {
13042
13911
  const auth = getAuth2(c);
@@ -13045,7 +13914,7 @@ var requireAnyPermission = defineMiddleware2(
13045
13914
  permissions: permissionNames,
13046
13915
  path: c.req.path
13047
13916
  });
13048
- throw new ForbiddenError2({ message: "Authentication required" });
13917
+ throw new ForbiddenError3({ message: "Authentication required" });
13049
13918
  }
13050
13919
  const { userId } = auth;
13051
13920
  const allowed = await hasAnyPermission2(userId, permissionNames);
@@ -13066,11 +13935,11 @@ var requireAnyPermission = defineMiddleware2(
13066
13935
  );
13067
13936
 
13068
13937
  // src/server/middleware/require-role.ts
13069
- import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
13938
+ import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
13070
13939
  import { getAuth as getAuth3, authLogger as authLogger5 } from "@spfn/auth/server";
13071
- import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
13940
+ import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
13072
13941
  import { InsufficientRoleError } from "@spfn/auth/errors";
13073
- var requireRole = defineMiddleware3(
13942
+ var requireRole = defineMiddleware4(
13074
13943
  "role",
13075
13944
  (...roleNames) => async (c, next) => {
13076
13945
  const auth = getAuth3(c);
@@ -13079,7 +13948,7 @@ var requireRole = defineMiddleware3(
13079
13948
  roles: roleNames,
13080
13949
  path: c.req.path
13081
13950
  });
13082
- throw new ForbiddenError3({ message: "Authentication required" });
13951
+ throw new ForbiddenError4({ message: "Authentication required" });
13083
13952
  }
13084
13953
  const { userId, role: userRole } = auth;
13085
13954
  if (!userRole || !roleNames.includes(userRole)) {
@@ -13101,11 +13970,11 @@ var requireRole = defineMiddleware3(
13101
13970
  );
13102
13971
 
13103
13972
  // src/server/middleware/role-guard.ts
13104
- import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
13973
+ import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
13105
13974
  import { getAuth as getAuth4, authLogger as authLogger6 } from "@spfn/auth/server";
13106
- import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
13975
+ import { ForbiddenError as ForbiddenError5 } from "@spfn/core/errors";
13107
13976
  import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
13108
- var roleGuard = defineMiddleware4(
13977
+ var roleGuard = defineMiddleware5(
13109
13978
  "roleGuard",
13110
13979
  (options) => async (c, next) => {
13111
13980
  const { allow, deny } = options;
@@ -13117,7 +13986,7 @@ var roleGuard = defineMiddleware4(
13117
13986
  authLogger6.middleware.warn("Role guard failed: not authenticated", {
13118
13987
  path: c.req.path
13119
13988
  });
13120
- throw new ForbiddenError4({ message: "Authentication required" });
13989
+ throw new ForbiddenError5({ message: "Authentication required" });
13121
13990
  }
13122
13991
  const { userId, role: userRole } = auth;
13123
13992
  if (deny && deny.length > 0) {
@@ -13153,28 +14022,28 @@ var roleGuard = defineMiddleware4(
13153
14022
  );
13154
14023
 
13155
14024
  // src/server/middleware/one-time-token-auth.ts
13156
- import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
13157
- import { UnauthorizedError as UnauthorizedError3 } from "@spfn/core/errors";
14025
+ import { defineMiddleware as defineMiddleware6 } from "@spfn/core/route";
14026
+ import { UnauthorizedError as UnauthorizedError4 } from "@spfn/core/errors";
13158
14027
  import { usersRepository as usersRepository4, userProfilesRepository as userProfilesRepository4 } from "@spfn/auth/server";
13159
- var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) => {
14028
+ var oneTimeTokenAuth = defineMiddleware6("oneTimeTokenAuth", async (c, next) => {
13160
14029
  const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
13161
14030
  if (!token) {
13162
- throw new UnauthorizedError3({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
14031
+ throw new UnauthorizedError4({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
13163
14032
  }
13164
14033
  const userId = await verifyOneTimeTokenService(token);
13165
14034
  if (!userId) {
13166
- throw new UnauthorizedError3({ message: "Invalid or expired one-time token" });
14035
+ throw new UnauthorizedError4({ message: "Invalid or expired one-time token" });
13167
14036
  }
13168
14037
  const [result, locale] = await Promise.all([
13169
14038
  usersRepository4.findByIdWithRole(Number(userId)),
13170
14039
  userProfilesRepository4.findLocaleByUserId(Number(userId))
13171
14040
  ]);
13172
14041
  if (!result) {
13173
- throw new UnauthorizedError3({ message: "User not found" });
14042
+ throw new UnauthorizedError4({ message: "User not found" });
13174
14043
  }
13175
14044
  const { user, role } = result;
13176
14045
  if (user.status !== "active") {
13177
- throw new UnauthorizedError3({ message: "Account is not active" });
14046
+ throw new UnauthorizedError4({ message: "Account is not active" });
13178
14047
  }
13179
14048
  c.set("auth", {
13180
14049
  user,
@@ -13195,39 +14064,39 @@ function extractOTTHeader(header) {
13195
14064
  }
13196
14065
 
13197
14066
  // src/server/middleware/ops-token-auth.ts
13198
- import { defineMiddleware as defineMiddleware6 } from "@spfn/core/route";
13199
- import { ForbiddenError as ForbiddenError5, UnauthorizedError as UnauthorizedError4 } from "@spfn/core/errors";
14067
+ import { defineMiddleware as defineMiddleware7 } from "@spfn/core/route";
14068
+ import { ForbiddenError as ForbiddenError6, UnauthorizedError as UnauthorizedError5 } from "@spfn/core/errors";
13200
14069
  function getOpsToken(c) {
13201
14070
  return c.get("opsToken") ?? null;
13202
14071
  }
13203
- var opsTokenAuth = defineMiddleware6("opsTokenAuth", async (c, next) => {
13204
- const token = extractBearer(c.req.header("Authorization"));
14072
+ var opsTokenAuth = defineMiddleware7("opsTokenAuth", async (c, next) => {
14073
+ const token = extractBearer2(c.req.header("Authorization"));
13205
14074
  if (!token) {
13206
- throw new UnauthorizedError4({ message: "Ops token required: Authorization: Bearer <token>" });
14075
+ throw new UnauthorizedError5({ message: "Ops token required: Authorization: Bearer <token>" });
13207
14076
  }
13208
14077
  const verified = await verifyOpsTokenService(token);
13209
14078
  if (!verified) {
13210
- throw new UnauthorizedError4({ message: "Invalid ops token" });
14079
+ throw new UnauthorizedError5({ message: "Invalid ops token" });
13211
14080
  }
13212
14081
  c.set("opsToken", verified);
13213
14082
  await next();
13214
14083
  }, { skips: ["auth"] });
13215
- var requireOpsScope = defineMiddleware6(
14084
+ var requireOpsScope = defineMiddleware7(
13216
14085
  "opsScope",
13217
14086
  (...scopes) => async (c, next) => {
13218
14087
  const token = getOpsToken(c);
13219
14088
  if (!token) {
13220
- throw new UnauthorizedError4({ message: "Ops token required: Authorization: Bearer <token>" });
14089
+ throw new UnauthorizedError5({ message: "Ops token required: Authorization: Bearer <token>" });
13221
14090
  }
13222
14091
  const granted = new Set(token.scopes);
13223
14092
  const missing = scopes.filter((scope) => !granted.has(scope) && !granted.has("*"));
13224
14093
  if (missing.length > 0) {
13225
- throw new ForbiddenError5({ message: `Ops token lacks scope: ${missing.join(", ")}` });
14094
+ throw new ForbiddenError6({ message: `Ops token lacks scope: ${missing.join(", ")}` });
13226
14095
  }
13227
14096
  await next();
13228
14097
  }
13229
14098
  );
13230
- function extractBearer(header) {
14099
+ function extractBearer2(header) {
13231
14100
  if (!header || !header.startsWith("Bearer ")) {
13232
14101
  return null;
13233
14102
  }
@@ -14022,7 +14891,7 @@ var oauthRouter = defineRouter4({
14022
14891
 
14023
14892
  // src/server/routes/admin/index.ts
14024
14893
  init_esm();
14025
- import { ForbiddenError as ForbiddenError6 } from "@spfn/core/errors";
14894
+ import { ForbiddenError as ForbiddenError7 } from "@spfn/core/errors";
14026
14895
  import { route as route5 } from "@spfn/core/route";
14027
14896
  var listRoles = route5.get("/_auth/admin/roles").input({
14028
14897
  query: Type.Object({
@@ -14092,11 +14961,11 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
14092
14961
  const { params, body } = await c.data();
14093
14962
  const auth = getAuth(c);
14094
14963
  if (params.userId === Number(auth.userId)) {
14095
- throw new ForbiddenError6({ message: "Cannot change your own role" });
14964
+ throw new ForbiddenError7({ message: "Cannot change your own role" });
14096
14965
  }
14097
14966
  const targetRole = await getUserRole(params.userId);
14098
14967
  if (targetRole === "superadmin") {
14099
- throw new ForbiddenError6({ message: "Cannot modify superadmin role" });
14968
+ throw new ForbiddenError7({ message: "Cannot modify superadmin role" });
14100
14969
  }
14101
14970
  await assertCanAssignRole(auth.userId, body.roleId);
14102
14971
  await updateUserService(params.userId, { roleId: body.roleId });
@@ -14220,6 +15089,12 @@ var mainAuthRouter = defineRouter6({
14220
15089
  confirmSignupLink,
14221
15090
  completeSignup,
14222
15091
  login,
15092
+ // Device-code login routes
15093
+ startDeviceAuth,
15094
+ pollDeviceAuth,
15095
+ getDeviceAuthInfo,
15096
+ approveDeviceAuth,
15097
+ denyDeviceAuth,
14223
15098
  logout,
14224
15099
  rotateKey,
14225
15100
  listKeys,
@@ -14463,15 +15338,64 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
14463
15338
  return hoursRemaining < thresholdHours;
14464
15339
  }
14465
15340
 
14466
- // src/server/setup.ts
15341
+ // src/server/lib/csrf.ts
14467
15342
  import { env as env14 } from "@spfn/auth/config";
15343
+ var CSRF_HEADER = "x-spfn-csrf";
15344
+ var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
15345
+ var MAX_CANDIDATES = 32;
15346
+ function sessionSecret() {
15347
+ const secret = env14.SPFN_AUTH_SESSION_SECRET;
15348
+ if (!secret) {
15349
+ throw new Error(
15350
+ "SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
15351
+ );
15352
+ }
15353
+ return secret;
15354
+ }
15355
+ async function hmacSha256(key, message) {
15356
+ const cryptoKey = await crypto.subtle.importKey(
15357
+ "raw",
15358
+ key.buffer,
15359
+ { name: "HMAC", hash: "SHA-256" },
15360
+ false,
15361
+ ["sign"]
15362
+ );
15363
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
15364
+ return new Uint8Array(signature);
15365
+ }
15366
+ function toHex(bytes) {
15367
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
15368
+ }
15369
+ async function deriveCsrfToken(keyId) {
15370
+ const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
15371
+ return toHex(await hmacSha256(subkey, keyId));
15372
+ }
15373
+ function timingSafeEqualString(a, b) {
15374
+ if (a.length !== b.length) {
15375
+ return false;
15376
+ }
15377
+ let difference = 0;
15378
+ for (let i = 0; i < a.length; i++) {
15379
+ difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
15380
+ }
15381
+ return difference === 0;
15382
+ }
15383
+ function matchesCsrfToken(expected, presented) {
15384
+ if (!presented) {
15385
+ return false;
15386
+ }
15387
+ return presented.split(",", MAX_CANDIDATES).some((candidate) => timingSafeEqualString(expected, candidate.trim()));
15388
+ }
15389
+
15390
+ // src/server/setup.ts
15391
+ import { env as env15 } from "@spfn/auth/config";
14468
15392
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
14469
15393
  init_repositories();
14470
15394
  function parseAdminAccounts() {
14471
15395
  const accounts = [];
14472
- if (env14.SPFN_AUTH_ADMIN_ACCOUNTS) {
15396
+ if (env15.SPFN_AUTH_ADMIN_ACCOUNTS) {
14473
15397
  try {
14474
- const accountsJson = env14.SPFN_AUTH_ADMIN_ACCOUNTS;
15398
+ const accountsJson = env15.SPFN_AUTH_ADMIN_ACCOUNTS;
14475
15399
  const parsed = JSON.parse(accountsJson);
14476
15400
  if (!Array.isArray(parsed)) {
14477
15401
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -14498,11 +15422,11 @@ function parseAdminAccounts() {
14498
15422
  return accounts;
14499
15423
  }
14500
15424
  }
14501
- const adminEmails = env14.SPFN_AUTH_ADMIN_EMAILS;
15425
+ const adminEmails = env15.SPFN_AUTH_ADMIN_EMAILS;
14502
15426
  if (adminEmails) {
14503
15427
  const emails = adminEmails.split(",").map((s) => s.trim());
14504
- const passwords = (env14.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
14505
- const roles2 = (env14.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
15428
+ const passwords = (env15.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
15429
+ const roles2 = (env15.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
14506
15430
  if (passwords.length !== emails.length) {
14507
15431
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
14508
15432
  return accounts;
@@ -14524,8 +15448,8 @@ function parseAdminAccounts() {
14524
15448
  }
14525
15449
  return accounts;
14526
15450
  }
14527
- const adminEmail = env14.SPFN_AUTH_ADMIN_EMAIL;
14528
- const adminPassword = env14.SPFN_AUTH_ADMIN_PASSWORD;
15451
+ const adminEmail = env15.SPFN_AUTH_ADMIN_EMAIL;
15452
+ const adminPassword = env15.SPFN_AUTH_ADMIN_PASSWORD;
14529
15453
  if (adminEmail && adminPassword) {
14530
15454
  accounts.push({
14531
15455
  email: adminEmail,
@@ -14589,6 +15513,7 @@ async function ensureAdminExists() {
14589
15513
  // src/server/lifecycle.ts
14590
15514
  function createAuthLifecycle(options = {}) {
14591
15515
  configureDeletion(options.deletion);
15516
+ configureDeviceAuth(options.deviceAuth);
14592
15517
  return {
14593
15518
  /**
14594
15519
  * Initialize auth system after database is ready
@@ -14637,20 +15562,28 @@ export {
14637
15562
  AuthMetadataRepository,
14638
15563
  AuthProviderSchema,
14639
15564
  COOKIE_NAMES,
15565
+ CSRF_HEADER,
14640
15566
  DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
14641
15567
  DEFAULT_DELETION_GRACE_PERIOD_DAYS,
14642
15568
  DEFAULT_DELETION_PURGE_CRON,
14643
15569
  DEFAULT_DELETION_PURGE_STRATEGY,
14644
15570
  DEFAULT_DELETION_SEND_NOTIFICATIONS,
15571
+ DEFAULT_DEVICE_AUTH_INTERVAL_MS,
15572
+ DEFAULT_DEVICE_AUTH_TTL_MS,
15573
+ DEVICE_AUTH_STATUSES,
15574
+ DeviceAuthPollResponseSchema,
15575
+ DeviceAuthorizationsRepository,
14645
15576
  DeviceNameSchema,
14646
15577
  EmailSchema,
14647
15578
  EnvironmentKeyringTokenCipher,
15579
+ FingerprintSchema,
14648
15580
  INVITATION_STATUSES,
14649
15581
  InvitationsRepository,
14650
15582
  KEY_ALGORITHM,
14651
15583
  KEY_DEVICE_NAME_MAX_LENGTH,
14652
15584
  KEY_FINGERPRINT_PREFIX_LENGTH,
14653
15585
  KEY_PLATFORM,
15586
+ KeyIdSchema,
14654
15587
  KeysRepository,
14655
15588
  OpsTokensRepository,
14656
15589
  PURGE_STRATEGIES,
@@ -14658,14 +15591,18 @@ export {
14658
15591
  PermissionsRepository,
14659
15592
  PhoneSchema,
14660
15593
  PlatformSchema,
15594
+ PublicKeySchema,
14661
15595
  RolePermissionsRepository,
14662
15596
  RolesRepository,
14663
15597
  SOCIAL_PROVIDERS,
14664
15598
  SignupLinkTokensRepository,
14665
15599
  SocialAccountsRepository,
14666
15600
  TargetTypeSchema,
15601
+ USER_CODE_ALPHABET,
15602
+ USER_CODE_LENGTH,
14667
15603
  USER_STATUSES,
14668
15604
  UnlinkNotifyRejection,
15605
+ UserCodeSchema,
14669
15606
  UserPermissionsRepository,
14670
15607
  UserProfilesRepository,
14671
15608
  UsersRepository,
@@ -14678,6 +15615,7 @@ export {
14678
15615
  accountDeletionRequestsRepository,
14679
15616
  addPermissionToRole,
14680
15617
  appleProvider,
15618
+ approveDeviceAuthService,
14681
15619
  assertCanAssignRole,
14682
15620
  authDeletionCancelledEvent,
14683
15621
  authDeletionCompletedEvent,
@@ -14699,6 +15637,7 @@ export {
14699
15637
  completeSignupService,
14700
15638
  configureAuth,
14701
15639
  configureDeletion,
15640
+ configureDeviceAuth,
14702
15641
  configureOAuthTokenCipher,
14703
15642
  confirmSignupLinkService,
14704
15643
  createAuthDeletionJobRouter,
@@ -14711,20 +15650,31 @@ export {
14711
15650
  decryptToken,
14712
15651
  deleteInvitation,
14713
15652
  deleteRole,
15653
+ denyDeviceAuthService,
15654
+ deriveCsrfToken,
15655
+ deviceAuthorizations,
15656
+ deviceAuthorizationsRepository,
14714
15657
  encryptToken,
14715
15658
  exchangeCodeForTokens,
14716
15659
  expireOldInvitations,
15660
+ formatUserCode,
14717
15661
  generateClientToken,
15662
+ generateDeviceCode,
14718
15663
  generateKeyPair,
14719
15664
  generateKeyPairES256,
14720
15665
  generateKeyPairRS256,
14721
15666
  generateOAuthNonce,
14722
15667
  generateToken,
15668
+ generateUserCode,
14723
15669
  getAllRoles,
14724
15670
  getAuth,
14725
15671
  getAuthConfig,
14726
15672
  getAuthSessionService,
15673
+ getCsrfExemptPaths,
15674
+ getCsrfMode,
14727
15675
  getDeletionConfig,
15676
+ getDeviceAuthConfig,
15677
+ getDeviceAuthInfoService,
14728
15678
  getDummyPasswordHash,
14729
15679
  getEnabledOAuthProviders,
14730
15680
  getGoogleAccessToken,
@@ -14736,6 +15686,7 @@ export {
14736
15686
  getKeyId,
14737
15687
  getKeySize,
14738
15688
  getLocale,
15689
+ getMachinePrincipal,
14739
15690
  getOAuthProvider,
14740
15691
  getOneTimeTokenManager,
14741
15692
  getOpsToken,
@@ -14762,6 +15713,7 @@ export {
14762
15713
  hasAnyRole,
14763
15714
  hasPermission,
14764
15715
  hasRole,
15716
+ hashDeviceCode,
14765
15717
  hashPassword,
14766
15718
  initOneTimeTokenManager,
14767
15719
  initializeAuth,
@@ -14781,11 +15733,14 @@ export {
14781
15733
  listOpsTokensService,
14782
15734
  loginService,
14783
15735
  logoutService,
15736
+ machineAuth,
14784
15737
  matchOAuthCsrfCookies,
15738
+ matchesCsrfToken,
14785
15739
  naverProvider,
14786
15740
  normalizeEmail,
14787
15741
  normalizeOptionalEmail,
14788
15742
  normalizeStoredEmails,
15743
+ normalizeUserCode,
14789
15744
  oauthCallbackService,
14790
15745
  oauthNativeService,
14791
15746
  oauthStartService,
@@ -14799,8 +15754,11 @@ export {
14799
15754
  parseDuration,
14800
15755
  permissions,
14801
15756
  permissionsRepository,
15757
+ pollDeviceAuthService,
14802
15758
  purgeUserService,
14803
15759
  refreshAccessToken,
15760
+ registerAuthProfile,
15761
+ registerMachineVerifier,
14804
15762
  registerOAuthProvider,
14805
15763
  registerPublicKeyService,
14806
15764
  registerService,
@@ -14809,6 +15767,7 @@ export {
14809
15767
  requestSignupLinkService,
14810
15768
  requireAnyPermission,
14811
15769
  requireEnabledProvider,
15770
+ requireMachineScope,
14812
15771
  requireOpsScope,
14813
15772
  requirePermissions,
14814
15773
  requireRole,
@@ -14834,6 +15793,7 @@ export {
14834
15793
  signupLinkTokens,
14835
15794
  signupLinkTokensRepository,
14836
15795
  socialAccountsRepository,
15796
+ startDeviceAuthService,
14837
15797
  sweepDuePurges,
14838
15798
  unsealSession,
14839
15799
  updateLastLoginService,