@spfn/auth 0.3.0-beta.26 → 0.3.0-beta.28

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
@@ -897,8 +897,8 @@ var init_array2 = __esm({
897
897
  });
898
898
 
899
899
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs
900
- function Argument(index21) {
901
- return CreateType({ [Kind]: "Argument", index: index21 });
900
+ function Argument(index22) {
901
+ return CreateType({ [Kind]: "Argument", index: index22 });
902
902
  }
903
903
  var init_argument = __esm({
904
904
  "../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs"() {
@@ -1136,28 +1136,28 @@ var init_union2 = __esm({
1136
1136
  function Unescape(pattern) {
1137
1137
  return pattern.replace(/\\\$/g, "$").replace(/\\\*/g, "*").replace(/\\\^/g, "^").replace(/\\\|/g, "|").replace(/\\\(/g, "(").replace(/\\\)/g, ")");
1138
1138
  }
1139
- function IsNonEscaped(pattern, index21, char) {
1140
- return pattern[index21] === char && pattern.charCodeAt(index21 - 1) !== 92;
1139
+ function IsNonEscaped(pattern, index22, char) {
1140
+ return pattern[index22] === char && pattern.charCodeAt(index22 - 1) !== 92;
1141
1141
  }
1142
- function IsOpenParen(pattern, index21) {
1143
- return IsNonEscaped(pattern, index21, "(");
1142
+ function IsOpenParen(pattern, index22) {
1143
+ return IsNonEscaped(pattern, index22, "(");
1144
1144
  }
1145
- function IsCloseParen(pattern, index21) {
1146
- return IsNonEscaped(pattern, index21, ")");
1145
+ function IsCloseParen(pattern, index22) {
1146
+ return IsNonEscaped(pattern, index22, ")");
1147
1147
  }
1148
- function IsSeparator(pattern, index21) {
1149
- return IsNonEscaped(pattern, index21, "|");
1148
+ function IsSeparator(pattern, index22) {
1149
+ return IsNonEscaped(pattern, index22, "|");
1150
1150
  }
1151
1151
  function IsGroup(pattern) {
1152
1152
  if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1)))
1153
1153
  return false;
1154
1154
  let count2 = 0;
1155
- for (let index21 = 0; index21 < pattern.length; index21++) {
1156
- if (IsOpenParen(pattern, index21))
1155
+ for (let index22 = 0; index22 < pattern.length; index22++) {
1156
+ if (IsOpenParen(pattern, index22))
1157
1157
  count2 += 1;
1158
- if (IsCloseParen(pattern, index21))
1158
+ if (IsCloseParen(pattern, index22))
1159
1159
  count2 -= 1;
1160
- if (count2 === 0 && index21 !== pattern.length - 1)
1160
+ if (count2 === 0 && index22 !== pattern.length - 1)
1161
1161
  return false;
1162
1162
  }
1163
1163
  return true;
@@ -1167,19 +1167,19 @@ function InGroup(pattern) {
1167
1167
  }
1168
1168
  function IsPrecedenceOr(pattern) {
1169
1169
  let count2 = 0;
1170
- for (let index21 = 0; index21 < pattern.length; index21++) {
1171
- if (IsOpenParen(pattern, index21))
1170
+ for (let index22 = 0; index22 < pattern.length; index22++) {
1171
+ if (IsOpenParen(pattern, index22))
1172
1172
  count2 += 1;
1173
- if (IsCloseParen(pattern, index21))
1173
+ if (IsCloseParen(pattern, index22))
1174
1174
  count2 -= 1;
1175
- if (IsSeparator(pattern, index21) && count2 === 0)
1175
+ if (IsSeparator(pattern, index22) && count2 === 0)
1176
1176
  return true;
1177
1177
  }
1178
1178
  return false;
1179
1179
  }
1180
1180
  function IsPrecedenceAnd(pattern) {
1181
- for (let index21 = 0; index21 < pattern.length; index21++) {
1182
- if (IsOpenParen(pattern, index21))
1181
+ for (let index22 = 0; index22 < pattern.length; index22++) {
1182
+ if (IsOpenParen(pattern, index22))
1183
1183
  return true;
1184
1184
  }
1185
1185
  return false;
@@ -1187,16 +1187,16 @@ function IsPrecedenceAnd(pattern) {
1187
1187
  function Or(pattern) {
1188
1188
  let [count2, start] = [0, 0];
1189
1189
  const expressions = [];
1190
- for (let index21 = 0; index21 < pattern.length; index21++) {
1191
- if (IsOpenParen(pattern, index21))
1190
+ for (let index22 = 0; index22 < pattern.length; index22++) {
1191
+ if (IsOpenParen(pattern, index22))
1192
1192
  count2 += 1;
1193
- if (IsCloseParen(pattern, index21))
1193
+ if (IsCloseParen(pattern, index22))
1194
1194
  count2 -= 1;
1195
- if (IsSeparator(pattern, index21) && count2 === 0) {
1196
- const range2 = pattern.slice(start, index21);
1195
+ if (IsSeparator(pattern, index22) && count2 === 0) {
1196
+ const range2 = pattern.slice(start, index22);
1197
1197
  if (range2.length > 0)
1198
1198
  expressions.push(TemplateLiteralParse(range2));
1199
- start = index21 + 1;
1199
+ start = index22 + 1;
1200
1200
  }
1201
1201
  }
1202
1202
  const range = pattern.slice(start);
@@ -1209,40 +1209,40 @@ function Or(pattern) {
1209
1209
  return { type: "or", expr: expressions };
1210
1210
  }
1211
1211
  function And(pattern) {
1212
- function Group(value, index21) {
1213
- if (!IsOpenParen(value, index21))
1212
+ function Group(value, index22) {
1213
+ if (!IsOpenParen(value, index22))
1214
1214
  throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
1215
1215
  let count2 = 0;
1216
- for (let scan = index21; scan < value.length; scan++) {
1216
+ for (let scan = index22; scan < value.length; scan++) {
1217
1217
  if (IsOpenParen(value, scan))
1218
1218
  count2 += 1;
1219
1219
  if (IsCloseParen(value, scan))
1220
1220
  count2 -= 1;
1221
1221
  if (count2 === 0)
1222
- return [index21, scan];
1222
+ return [index22, scan];
1223
1223
  }
1224
1224
  throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
1225
1225
  }
1226
- function Range(pattern2, index21) {
1227
- for (let scan = index21; scan < pattern2.length; scan++) {
1226
+ function Range(pattern2, index22) {
1227
+ for (let scan = index22; scan < pattern2.length; scan++) {
1228
1228
  if (IsOpenParen(pattern2, scan))
1229
- return [index21, scan];
1229
+ return [index22, scan];
1230
1230
  }
1231
- return [index21, pattern2.length];
1231
+ return [index22, pattern2.length];
1232
1232
  }
1233
1233
  const expressions = [];
1234
- for (let index21 = 0; index21 < pattern.length; index21++) {
1235
- if (IsOpenParen(pattern, index21)) {
1236
- const [start, end] = Group(pattern, index21);
1234
+ for (let index22 = 0; index22 < pattern.length; index22++) {
1235
+ if (IsOpenParen(pattern, index22)) {
1236
+ const [start, end] = Group(pattern, index22);
1237
1237
  const range = pattern.slice(start, end + 1);
1238
1238
  expressions.push(TemplateLiteralParse(range));
1239
- index21 = end;
1239
+ index22 = end;
1240
1240
  } else {
1241
- const [start, end] = Range(pattern, index21);
1241
+ const [start, end] = Range(pattern, index22);
1242
1242
  const range = pattern.slice(start, end);
1243
1243
  if (range.length > 0)
1244
1244
  expressions.push(TemplateLiteralParse(range));
1245
- index21 = end - 1;
1245
+ index22 = end - 1;
1246
1246
  }
1247
1247
  }
1248
1248
  return expressions.length === 0 ? { type: "const", const: "" } : expressions.length === 1 ? expressions[0] : { type: "and", expr: expressions };
@@ -2577,13 +2577,13 @@ function FromBoolean(left, right) {
2577
2577
  return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsBoolean(right) ? ExtendsResult.True : ExtendsResult.False;
2578
2578
  }
2579
2579
  function FromConstructor(left, right) {
2580
- return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index21) => IntoBooleanResult(Visit3(right.parameters[index21], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
2580
+ return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index22) => IntoBooleanResult(Visit3(right.parameters[index22], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
2581
2581
  }
2582
2582
  function FromDate(left, right) {
2583
2583
  return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsDate(right) ? ExtendsResult.True : ExtendsResult.False;
2584
2584
  }
2585
2585
  function FromFunction(left, right) {
2586
- return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index21) => IntoBooleanResult(Visit3(right.parameters[index21], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
2586
+ return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index22) => IntoBooleanResult(Visit3(right.parameters[index22], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
2587
2587
  }
2588
2588
  function FromIntegerRight(left, right) {
2589
2589
  return type_exports.IsLiteral(left) && value_exports.IsNumber(left.const) ? ExtendsResult.True : type_exports.IsNumber(left) || type_exports.IsInteger(left) ? ExtendsResult.True : ExtendsResult.False;
@@ -2743,7 +2743,7 @@ function FromTupleRight(left, right) {
2743
2743
  return type_exports.IsNever(left) ? ExtendsResult.True : type_exports.IsUnknown(left) ? ExtendsResult.False : type_exports.IsAny(left) ? ExtendsResult.Union : ExtendsResult.False;
2744
2744
  }
2745
2745
  function FromTuple3(left, right) {
2746
- return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index21) => Visit3(schema, right.items[index21]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
2746
+ return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index22) => Visit3(schema, right.items[index22]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
2747
2747
  }
2748
2748
  function FromUint8Array(left, right) {
2749
2749
  return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsUint8Array(right) ? ExtendsResult.True : ExtendsResult.False;
@@ -4474,7 +4474,7 @@ var init_types = __esm({
4474
4474
 
4475
4475
  // src/server/routes/schema.ts
4476
4476
  import { EMAIL_PATTERN, PHONE_PATTERN } from "@spfn/auth";
4477
- var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
4477
+ var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, LinkIdSchema, MatchChoiceSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
4478
4478
  var init_schema3 = __esm({
4479
4479
  "src/server/routes/schema.ts"() {
4480
4480
  "use strict";
@@ -4513,6 +4513,16 @@ var init_schema3 = __esm({
4513
4513
  maxLength: 16,
4514
4514
  description: "Device user code as displayed, e.g. WXYZ-2345. Dashes, spaces and case are ignored."
4515
4515
  });
4516
+ LinkIdSchema = Type.String({
4517
+ minLength: 1,
4518
+ maxLength: 64,
4519
+ description: "Device link handle returned by /_auth/device/link/issue"
4520
+ });
4521
+ MatchChoiceSchema = Type.Integer({
4522
+ minimum: 10,
4523
+ maximum: 99,
4524
+ description: "The number the issuer picked \u2014 the one the new device shows"
4525
+ });
4516
4526
  DeviceAuthPollResponseSchema = Type.Union([
4517
4527
  Type.Object({
4518
4528
  status: Type.Literal("pending"),
@@ -5615,9 +5625,86 @@ var init_device_authorizations = __esm({
5615
5625
  }
5616
5626
  });
5617
5627
 
5628
+ // src/server/entities/device-links.ts
5629
+ import { integer as integer9, text as text17, uniqueIndex as uniqueIndex9, index as index14 } from "drizzle-orm/pg-core";
5630
+ import { id as id16, timestamps as timestamps14, enumText as enumText10, utcTimestamp as utcTimestamp15, foreignKey as foreignKey12 } from "@spfn/core/db";
5631
+ var DEVICE_LINK_STATUSES, deviceLinks;
5632
+ var init_device_links = __esm({
5633
+ "src/server/entities/device-links.ts"() {
5634
+ "use strict";
5635
+ init_types();
5636
+ init_users();
5637
+ init_schema4();
5638
+ DEVICE_LINK_STATUSES = ["issued", "redeemed", "approved", "denied", "consumed", "expired"];
5639
+ deviceLinks = authSchema.table(
5640
+ "device_links",
5641
+ {
5642
+ id: id16(),
5643
+ // The issuer's handle on the record, returned by issue and named by
5644
+ // status, confirm, deny and cancel
5645
+ // Random rather than the row id so a handle says nothing about how many
5646
+ // links exist; it authorizes nothing without the issuing key beside it
5647
+ linkId: text17("link_id").notNull().unique(),
5648
+ // The code the issuer shows and the new device sends back
5649
+ // Stored normalized — uppercase, no dash — as device_authorizations does
5650
+ // Plaintext on purpose: redeeming it registers nothing until the issuer
5651
+ // picks the match number the redeeming device shows
5652
+ userCode: text17("user_code").notNull(),
5653
+ // The account the new device joins, written at issue time from the
5654
+ // issuer's session. Never taken from a request body
5655
+ issuerUserId: foreignKey12("issuer_user", () => users.id, { onDelete: "cascade" }),
5656
+ // The key that signed the issue request
5657
+ // Only requests signed by this key can see or answer the link, and the
5658
+ // link is judged expired once this key is revoked or runs out
5659
+ issuerKeyId: text17("issuer_key_id").notNull(),
5660
+ // SHA-256 of the device code, hex — set at redeem, null before
5661
+ // The code itself is returned to the redeeming device once and never stored
5662
+ deviceCodeHash: text17("device_code_hash").unique(),
5663
+ // Key material the redeeming device generated, same shapes as user_public_keys
5664
+ // null until redeem; parked, not registered, until a poll moves it over
5665
+ publicKey: text17("public_key"),
5666
+ keyId: text17("key_id"),
5667
+ fingerprint: text17("fingerprint"),
5668
+ algorithm: enumText10("algorithm", KEY_ALGORITHM),
5669
+ // Labels the redeeming device supplied, shown to the issuer
5670
+ // Display only — the match number is what the decision rests on
5671
+ deviceName: text17("device_name"),
5672
+ platform: enumText10("platform", KEY_PLATFORM),
5673
+ // The number the redeeming device shows, 10–99, drawn at redeem
5674
+ // Never sent to the issuer on its own: it is one of `choices`
5675
+ matchNumber: integer9("match_number"),
5676
+ // The three numbers the issuer picks from — the match and two distinct
5677
+ // decoys, shuffled once at redeem so every status answer shows the same
5678
+ // order
5679
+ choices: integer9("choices").array(),
5680
+ status: enumText10("status", DEVICE_LINK_STATUSES).notNull().default("issued"),
5681
+ // Expiry — 5 minutes from issue by default
5682
+ // Judged on read and in every transition; no job clears the row
5683
+ expiresAt: utcTimestamp15("expires_at").notNull(),
5684
+ // Set when a device redeemed the code
5685
+ redeemedAt: utcTimestamp15("redeemed_at"),
5686
+ // Set when the issuer picked the right number
5687
+ approvedAt: utcTimestamp15("approved_at"),
5688
+ // Set when the poll that registered the key won the race
5689
+ consumedAt: utcTimestamp15("consumed_at"),
5690
+ ...timestamps14()
5691
+ },
5692
+ (table) => [
5693
+ // Lookup path for redeem
5694
+ // Unique so a typed code can never address two records
5695
+ uniqueIndex9("device_link_user_code_idx").on(table.userCode),
5696
+ // Issue expires the issuing key's live link before it creates the next one
5697
+ index14("device_link_issuer_key_idx").on(table.issuerKeyId),
5698
+ // A global revocation expires every live link of the account
5699
+ index14("device_link_issuer_user_idx").on(table.issuerUserId)
5700
+ ]
5701
+ );
5702
+ }
5703
+ });
5704
+
5618
5705
  // src/server/entities/user-invitations.ts
5619
- import { text as text17, index as index14 } from "drizzle-orm/pg-core";
5620
- import { id as id16, timestamps as timestamps14, enumText as enumText10, utcTimestamp as utcTimestamp15, typedJsonb as typedJsonb2, foreignKey as foreignKey12 } from "@spfn/core/db";
5706
+ import { text as text18, index as index15 } from "drizzle-orm/pg-core";
5707
+ import { id as id17, timestamps as timestamps15, enumText as enumText11, utcTimestamp as utcTimestamp16, typedJsonb as typedJsonb2, foreignKey as foreignKey13 } from "@spfn/core/db";
5621
5708
  var userInvitations;
5622
5709
  var init_user_invitations = __esm({
5623
5710
  "src/server/entities/user-invitations.ts"() {
@@ -5630,39 +5717,39 @@ var init_user_invitations = __esm({
5630
5717
  "user_invitations",
5631
5718
  {
5632
5719
  // Primary key
5633
- id: id16(),
5720
+ id: id17(),
5634
5721
  // Target email address for the invitation
5635
5722
  // Will become the user's email upon acceptance
5636
- email: text17("email").notNull(),
5723
+ email: text18("email").notNull(),
5637
5724
  // Unique invitation token (UUID v4)
5638
5725
  // Used in invitation URL: /auth/invite/{token}
5639
5726
  // Single-use token that expires after acceptance
5640
- token: text17("token").notNull().unique(),
5727
+ token: text18("token").notNull().unique(),
5641
5728
  // Role to be assigned when invitation is accepted
5642
5729
  // Foreign key to roles table
5643
- roleId: foreignKey12("role", () => roles.id),
5730
+ roleId: foreignKey13("role", () => roles.id),
5644
5731
  // User who created this invitation
5645
5732
  // Foreign key to users table
5646
5733
  // Used for: audit trail, permission checks
5647
- invitedBy: foreignKey12("invited_by", () => users.id, { onDelete: "cascade" }),
5734
+ invitedBy: foreignKey13("invited_by", () => users.id, { onDelete: "cascade" }),
5648
5735
  // Invitation status
5649
5736
  // - pending: Invitation sent, awaiting acceptance
5650
5737
  // - accepted: User accepted and account created
5651
5738
  // - expired: Invitation expired (automatic)
5652
5739
  // - cancelled: Invitation cancelled by admin
5653
- status: enumText10("status", INVITATION_STATUSES).default("pending").notNull(),
5740
+ status: enumText11("status", INVITATION_STATUSES).default("pending").notNull(),
5654
5741
  // Expiration timestamp (default: 7 days from creation)
5655
5742
  // Invitation cannot be accepted after this time
5656
5743
  // Background job should update status to 'expired'
5657
- expiresAt: utcTimestamp15("expires_at").notNull(),
5744
+ expiresAt: utcTimestamp16("expires_at").notNull(),
5658
5745
  // Timestamp when invitation was accepted
5659
5746
  // null = not yet accepted
5660
5747
  // Used for: audit trail, analytics
5661
- acceptedAt: utcTimestamp15("accepted_at"),
5748
+ acceptedAt: utcTimestamp16("accepted_at"),
5662
5749
  // Timestamp when invitation was cancelled
5663
5750
  // null = not cancelled
5664
5751
  // Used for: audit trail
5665
- cancelledAt: utcTimestamp15("cancelled_at"),
5752
+ cancelledAt: utcTimestamp16("cancelled_at"),
5666
5753
  // Additional metadata (JSONB)
5667
5754
  // Use cases:
5668
5755
  // - Custom welcome message
@@ -5671,26 +5758,26 @@ var init_user_invitations = __esm({
5671
5758
  // - Custom fields for app-specific data
5672
5759
  // Example: { message: "Welcome!", department: "Engineering" }
5673
5760
  metadata: typedJsonb2("metadata"),
5674
- ...timestamps14()
5761
+ ...timestamps15()
5675
5762
  },
5676
5763
  (table) => [
5677
5764
  // Indexes for query optimization
5678
- index14("invitations_token_idx").on(table.token),
5679
- index14("invitations_email_idx").on(table.email),
5680
- index14("invitations_status_idx").on(table.status),
5681
- index14("invitations_invited_by_idx").on(table.invitedBy),
5682
- index14("invitations_expires_at_idx").on(table.expiresAt),
5765
+ index15("invitations_token_idx").on(table.token),
5766
+ index15("invitations_email_idx").on(table.email),
5767
+ index15("invitations_status_idx").on(table.status),
5768
+ index15("invitations_invited_by_idx").on(table.invitedBy),
5769
+ index15("invitations_expires_at_idx").on(table.expiresAt),
5683
5770
  // For cleanup jobs
5684
- index14("invitations_role_id_idx").on(table.roleId)
5771
+ index15("invitations_role_id_idx").on(table.roleId)
5685
5772
  ]
5686
5773
  );
5687
5774
  }
5688
5775
  });
5689
5776
 
5690
5777
  // src/server/entities/account-deletion-requests.ts
5691
- import { text as text18, index as index15, uniqueIndex as uniqueIndex9 } from "drizzle-orm/pg-core";
5778
+ import { text as text19, index as index16, uniqueIndex as uniqueIndex10 } from "drizzle-orm/pg-core";
5692
5779
  import { sql as sql2 } from "drizzle-orm";
5693
- import { id as id17, timestamps as timestamps15, enumText as enumText11, utcTimestamp as utcTimestamp16, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
5780
+ import { id as id18, timestamps as timestamps16, enumText as enumText12, utcTimestamp as utcTimestamp17, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
5694
5781
  var accountDeletionRequests;
5695
5782
  var init_account_deletion_requests = __esm({
5696
5783
  "src/server/entities/account-deletion-requests.ts"() {
@@ -5701,40 +5788,40 @@ var init_account_deletion_requests = __esm({
5701
5788
  accountDeletionRequests = authSchema.table(
5702
5789
  "account_deletion_requests",
5703
5790
  {
5704
- id: id17(),
5791
+ id: id18(),
5705
5792
  // Foreign key to users table. `set null` (optionalForeignKey default) so this
5706
5793
  // row survives a hard-delete purge of the user it refers to.
5707
5794
  userId: optionalForeignKey2("user", () => users.id),
5708
5795
  // Snapshot of the user's public UUID at request time — stays readable even
5709
5796
  // after userId is nulled out or the account is anonymized.
5710
- userPublicId: text18("user_public_id").notNull(),
5797
+ userPublicId: text19("user_public_id").notNull(),
5711
5798
  // When the deletion was requested
5712
- requestedAt: utcTimestamp16("requested_at").notNull().defaultNow(),
5799
+ requestedAt: utcTimestamp17("requested_at").notNull().defaultNow(),
5713
5800
  // When the purge job is allowed to run (requestedAt + grace period; equals
5714
5801
  // requestedAt itself for immediate/zero-grace deletions)
5715
- purgeScheduledAt: utcTimestamp16("purge_scheduled_at").notNull(),
5802
+ purgeScheduledAt: utcTimestamp17("purge_scheduled_at").notNull(),
5716
5803
  // Request lifecycle status
5717
5804
  // - pending: awaiting purgeScheduledAt (or immediate purge)
5718
5805
  // - cancelled: recovered before purge
5719
5806
  // - completed: purge ran
5720
- status: enumText11("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
5807
+ status: enumText12("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
5721
5808
  // Who initiated the request
5722
- requestedBy: enumText11("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
5809
+ requestedBy: enumText12("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
5723
5810
  // Optional free-text reason (self-service UI, admin note, DSR reference, ...)
5724
- reason: text18("reason"),
5725
- cancelledAt: utcTimestamp16("cancelled_at"),
5726
- completedAt: utcTimestamp16("completed_at"),
5811
+ reason: text19("reason"),
5812
+ cancelledAt: utcTimestamp17("cancelled_at"),
5813
+ completedAt: utcTimestamp17("completed_at"),
5727
5814
  // Purge strategy actually executed (set on completion; null while pending)
5728
- purgeStrategy: enumText11("purge_strategy", PURGE_STRATEGIES),
5729
- ...timestamps15()
5815
+ purgeStrategy: enumText12("purge_strategy", PURGE_STRATEGIES),
5816
+ ...timestamps16()
5730
5817
  },
5731
5818
  (table) => [
5732
- index15("account_deletion_requests_user_id_idx").on(table.userId),
5733
- index15("account_deletion_requests_status_idx").on(table.status),
5734
- index15("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
5735
- index15("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
5819
+ index16("account_deletion_requests_user_id_idx").on(table.userId),
5820
+ index16("account_deletion_requests_status_idx").on(table.status),
5821
+ index16("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
5822
+ index16("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
5736
5823
  // Partial unique index: at most one pending request per user at a time.
5737
- uniqueIndex9("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5824
+ uniqueIndex10("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5738
5825
  ]
5739
5826
  );
5740
5827
  }
@@ -5886,8 +5973,8 @@ var init_rbac = __esm({
5886
5973
  });
5887
5974
 
5888
5975
  // src/server/entities/permissions.ts
5889
- import { text as text19, boolean as boolean5, index as index16 } from "drizzle-orm/pg-core";
5890
- import { id as id18, timestamps as timestamps16, enumText as enumText12, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5976
+ import { text as text20, boolean as boolean5, index as index17 } from "drizzle-orm/pg-core";
5977
+ import { id as id19, timestamps as timestamps17, enumText as enumText13, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5891
5978
  var permissions;
5892
5979
  var init_permissions = __esm({
5893
5980
  "src/server/entities/permissions.ts"() {
@@ -5898,7 +5985,7 @@ var init_permissions = __esm({
5898
5985
  "permissions",
5899
5986
  {
5900
5987
  // Primary key
5901
- id: id18(),
5988
+ id: id19(),
5902
5989
  // Permission identifier
5903
5990
  // Format: resource:action or namespace:resource:action
5904
5991
  // Examples:
@@ -5906,20 +5993,20 @@ var init_permissions = __esm({
5906
5993
  // - Namespaced: 'auth:user:delete', 'cms:post:publish'
5907
5994
  // Must be unique across all permissions
5908
5995
  // Used in: permission checks, role assignments, API guards
5909
- name: text19("name").notNull().unique(),
5996
+ name: text20("name").notNull().unique(),
5910
5997
  // Display name for UI
5911
5998
  // Human-readable name shown in admin panels
5912
5999
  // Example: "Delete Users", "Publish Posts"
5913
- displayName: text19("display_name").notNull(),
6000
+ displayName: text20("display_name").notNull(),
5914
6001
  // Permission description
5915
6002
  // Detailed explanation of what this permission allows
5916
6003
  // Example: "Allows deletion of user accounts from the system"
5917
- description: text19("description"),
6004
+ description: text20("description"),
5918
6005
  // Category for grouping
5919
6006
  // Used for: organizing permissions in UI, filtering
5920
6007
  // Built-in categories: auth, user, rbac, system
5921
6008
  // Custom categories: any app-specific category
5922
- category: enumText12("category", PERMISSION_CATEGORIES),
6009
+ category: enumText13("category", PERMISSION_CATEGORIES),
5923
6010
  // Built-in permission flag
5924
6011
  // true: Core package permissions (auth:*, user:*, rbac:*)
5925
6012
  // - Cannot be deleted or modified
@@ -5950,22 +6037,22 @@ var init_permissions = __esm({
5950
6037
  // - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
5951
6038
  // Example: { icon: 'trash', color: 'red', requiresMfa: true }
5952
6039
  metadata: typedJsonb3("metadata"),
5953
- ...timestamps16()
6040
+ ...timestamps17()
5954
6041
  },
5955
6042
  (table) => [
5956
- index16("permissions_name_idx").on(table.name),
5957
- index16("permissions_category_idx").on(table.category),
5958
- index16("permissions_is_system_idx").on(table.isSystem),
5959
- index16("permissions_is_active_idx").on(table.isActive),
5960
- index16("permissions_is_builtin_idx").on(table.isBuiltin)
6043
+ index17("permissions_name_idx").on(table.name),
6044
+ index17("permissions_category_idx").on(table.category),
6045
+ index17("permissions_is_system_idx").on(table.isSystem),
6046
+ index17("permissions_is_active_idx").on(table.isActive),
6047
+ index17("permissions_is_builtin_idx").on(table.isBuiltin)
5961
6048
  ]
5962
6049
  );
5963
6050
  }
5964
6051
  });
5965
6052
 
5966
6053
  // src/server/entities/role-permissions.ts
5967
- import { index as index17, unique } from "drizzle-orm/pg-core";
5968
- import { id as id19, timestamps as timestamps17, foreignKey as foreignKey13 } from "@spfn/core/db";
6054
+ import { index as index18, unique } from "drizzle-orm/pg-core";
6055
+ import { id as id20, timestamps as timestamps18, foreignKey as foreignKey14 } from "@spfn/core/db";
5969
6056
  var rolePermissions;
5970
6057
  var init_role_permissions = __esm({
5971
6058
  "src/server/entities/role-permissions.ts"() {
@@ -5977,25 +6064,25 @@ var init_role_permissions = __esm({
5977
6064
  "role_permissions",
5978
6065
  {
5979
6066
  // Primary key
5980
- id: id19(),
6067
+ id: id20(),
5981
6068
  // Role reference
5982
6069
  // Foreign key to roles table
5983
6070
  // Cascade delete: when role is deleted, all role-permission mappings are removed
5984
6071
  // Used for: defining which permissions each role has
5985
6072
  // Example: Admin role → [user:create, user:delete, user:update]
5986
- roleId: foreignKey13("role", () => roles.id, { onDelete: "cascade" }),
6073
+ roleId: foreignKey14("role", () => roles.id, { onDelete: "cascade" }),
5987
6074
  // Permission reference
5988
6075
  // Foreign key to permissions table
5989
6076
  // Cascade delete: when permission is deleted, all role-permission mappings are removed
5990
6077
  // Used for: granting permissions to roles
5991
6078
  // Example: user:delete permission → [Admin, Superadmin]
5992
- permissionId: foreignKey13("permission", () => permissions.id, { onDelete: "cascade" }),
5993
- ...timestamps17()
6079
+ permissionId: foreignKey14("permission", () => permissions.id, { onDelete: "cascade" }),
6080
+ ...timestamps18()
5994
6081
  },
5995
6082
  (table) => [
5996
6083
  // Indexes for query performance
5997
- index17("role_permissions_role_id_idx").on(table.roleId),
5998
- index17("role_permissions_permission_id_idx").on(table.permissionId),
6084
+ index18("role_permissions_role_id_idx").on(table.roleId),
6085
+ index18("role_permissions_permission_id_idx").on(table.permissionId),
5999
6086
  // Unique constraint: one role-permission pair only
6000
6087
  unique("role_permissions_unique").on(table.roleId, table.permissionId)
6001
6088
  ]
@@ -6004,8 +6091,8 @@ var init_role_permissions = __esm({
6004
6091
  });
6005
6092
 
6006
6093
  // src/server/entities/user-permissions.ts
6007
- import { boolean as boolean6, text as text20, index as index18, unique as unique2 } from "drizzle-orm/pg-core";
6008
- import { id as id20, timestamps as timestamps18, utcTimestamp as utcTimestamp17, foreignKey as foreignKey14 } from "@spfn/core/db";
6094
+ import { boolean as boolean6, text as text21, index as index19, unique as unique2 } from "drizzle-orm/pg-core";
6095
+ import { id as id21, timestamps as timestamps19, utcTimestamp as utcTimestamp18, foreignKey as foreignKey15 } from "@spfn/core/db";
6009
6096
  var userPermissions;
6010
6097
  var init_user_permissions = __esm({
6011
6098
  "src/server/entities/user-permissions.ts"() {
@@ -6017,15 +6104,15 @@ var init_user_permissions = __esm({
6017
6104
  "user_permissions",
6018
6105
  {
6019
6106
  // Primary key
6020
- id: id20(),
6107
+ id: id21(),
6021
6108
  // User reference
6022
6109
  // Foreign key to users table
6023
6110
  // Cascade delete: when user is deleted, all overrides are removed
6024
- userId: foreignKey14("user", () => users.id, { onDelete: "cascade" }),
6111
+ userId: foreignKey15("user", () => users.id, { onDelete: "cascade" }),
6025
6112
  // Permission reference
6026
6113
  // Foreign key to permissions table
6027
6114
  // Cascade delete: when permission is deleted, all overrides are removed
6028
- permissionId: foreignKey14("permission", () => permissions.id, { onDelete: "cascade" }),
6115
+ permissionId: foreignKey15("permission", () => permissions.id, { onDelete: "cascade" }),
6029
6116
  // Grant or revoke flag
6030
6117
  // true: GRANT this permission to the user (additive override)
6031
6118
  // - Grants permission even if role doesn't have it
@@ -6040,19 +6127,19 @@ var init_user_permissions = __esm({
6040
6127
  // Reason for grant/revocation
6041
6128
  // Used for: audit trail, compliance documentation
6042
6129
  // Example: "Temporary access for project X", "Security incident - restricted"
6043
- reason: text20("reason"),
6130
+ reason: text21("reason"),
6044
6131
  // Expiration timestamp (optional)
6045
6132
  // null: Permanent override (remains until manually removed)
6046
6133
  // timestamp: Permission expires at this time (auto-revoked by background job)
6047
6134
  // Use case: Time-limited elevated access, temporary restrictions
6048
- expiresAt: utcTimestamp17("expires_at"),
6049
- ...timestamps18()
6135
+ expiresAt: utcTimestamp18("expires_at"),
6136
+ ...timestamps19()
6050
6137
  },
6051
6138
  (table) => [
6052
6139
  // Indexes for query performance
6053
- index18("user_permissions_user_id_idx").on(table.userId),
6054
- index18("user_permissions_permission_id_idx").on(table.permissionId),
6055
- index18("user_permissions_expires_at_idx").on(table.expiresAt),
6140
+ index19("user_permissions_user_id_idx").on(table.userId),
6141
+ index19("user_permissions_permission_id_idx").on(table.permissionId),
6142
+ index19("user_permissions_expires_at_idx").on(table.expiresAt),
6056
6143
  // Unique constraint: one user-permission pair only
6057
6144
  unique2("user_permissions_unique").on(table.userId, table.permissionId)
6058
6145
  ]
@@ -6061,8 +6148,8 @@ var init_user_permissions = __esm({
6061
6148
  });
6062
6149
 
6063
6150
  // src/server/entities/oauth2-clients.ts
6064
- import { text as text21 } from "drizzle-orm/pg-core";
6065
- import { id as id21, timestamps as timestamps19, utcTimestamp as utcTimestamp18 } from "@spfn/core/db";
6151
+ import { text as text22 } from "drizzle-orm/pg-core";
6152
+ import { id as id22, timestamps as timestamps20, utcTimestamp as utcTimestamp19 } from "@spfn/core/db";
6066
6153
  var oauth2Clients;
6067
6154
  var init_oauth2_clients = __esm({
6068
6155
  "src/server/entities/oauth2-clients.ts"() {
@@ -6071,35 +6158,35 @@ var init_oauth2_clients = __esm({
6071
6158
  oauth2Clients = authSchema.table(
6072
6159
  "oauth2_clients",
6073
6160
  {
6074
- id: id21(),
6161
+ id: id22(),
6075
6162
  // The `client_id` the client sends on every later request. Random, opaque,
6076
6163
  // and the unique constraint doubles as the lookup index.
6077
- clientId: text21("client_id").notNull().unique(),
6164
+ clientId: text22("client_id").notNull().unique(),
6078
6165
  // Self-declared label, shown on the consent screen and nowhere else.
6079
6166
  // A client that lies about it gains one wrong line on that screen.
6080
- clientName: text21("client_name").notNull(),
6167
+ clientName: text22("client_name").notNull(),
6081
6168
  // Registered redirect URIs, stored exactly as the client wrote them.
6082
6169
  // The registration answer echoes them back verbatim, so normalising here
6083
6170
  // would answer a client with a URI it did not register. Matching
6084
6171
  // normalises both sides instead — see lib/oauth2/redirect-uri.ts.
6085
- redirectUris: text21("redirect_uris").array().notNull(),
6172
+ redirectUris: text22("redirect_uris").array().notNull(),
6086
6173
  // Client IP the registration arrived from, so the per-IP cap on clients
6087
6174
  // nobody has approved yet can be counted. Nullable: an IP is not always
6088
6175
  // knowable behind a proxy that forwards none, and a registration is not
6089
6176
  // worth refusing over that.
6090
- createdIp: text21("created_ip"),
6177
+ createdIp: text22("created_ip"),
6091
6178
  // Last token issuance or refresh under this client, updated
6092
6179
  // fire-and-forget. Operator-facing only; nothing is authorized by it.
6093
- lastUsedAt: utcTimestamp18("last_used_at"),
6094
- ...timestamps19()
6180
+ lastUsedAt: utcTimestamp19("last_used_at"),
6181
+ ...timestamps20()
6095
6182
  }
6096
6183
  );
6097
6184
  }
6098
6185
  });
6099
6186
 
6100
6187
  // src/server/entities/oauth2-grants.ts
6101
- import { uniqueIndex as uniqueIndex10, index as index19, text as text22 } from "drizzle-orm/pg-core";
6102
- import { id as id22, timestamps as timestamps20, utcTimestamp as utcTimestamp19, foreignKey as foreignKey15 } from "@spfn/core/db";
6188
+ import { uniqueIndex as uniqueIndex11, index as index20, text as text23 } from "drizzle-orm/pg-core";
6189
+ import { id as id23, timestamps as timestamps21, utcTimestamp as utcTimestamp20, foreignKey as foreignKey16 } from "@spfn/core/db";
6103
6190
  var oauth2Grants;
6104
6191
  var init_oauth2_grants = __esm({
6105
6192
  "src/server/entities/oauth2-grants.ts"() {
@@ -6110,39 +6197,39 @@ var init_oauth2_grants = __esm({
6110
6197
  oauth2Grants = authSchema.table(
6111
6198
  "oauth2_grants",
6112
6199
  {
6113
- id: id22(),
6200
+ id: id23(),
6114
6201
  // `oauth2_client_id` and not `client_id`: the opaque string a client
6115
6202
  // sends is called `client_id` everywhere in the protocol, and a bigint
6116
6203
  // foreign key under that name in this schema would read as that value.
6117
- client: foreignKey15("oauth2_client", () => oauth2Clients.id, { onDelete: "cascade" }),
6204
+ client: foreignKey16("oauth2_client", () => oauth2Clients.id, { onDelete: "cascade" }),
6118
6205
  // Whose consent this is. Read from the approving session at authorize
6119
6206
  // time, never from a request body — that is the whole authorization.
6120
- user: foreignKey15("user", () => users.id, { onDelete: "cascade" }),
6207
+ user: foreignKey16("user", () => users.id, { onDelete: "cascade" }),
6121
6208
  // The RFC 8707 target this consent is for, normalised (see lib/oauth2).
6122
6209
  // An access token is only good against the resource its grant names.
6123
- resource: text22("resource").notNull(),
6210
+ resource: text23("resource").notNull(),
6124
6211
  // Scope names the user approved. A refresh may ask for a subset of these
6125
6212
  // and never for more.
6126
- scopes: text22("scopes").array().notNull(),
6213
+ scopes: text23("scopes").array().notNull(),
6127
6214
  // null = live; a timestamp cuts off every code and token beneath it
6128
- revokedAt: utcTimestamp19("revoked_at"),
6129
- ...timestamps20()
6215
+ revokedAt: utcTimestamp20("revoked_at"),
6216
+ ...timestamps21()
6130
6217
  },
6131
6218
  (table) => [
6132
6219
  // One consent per (client, user, resource) — the re-consent path updates
6133
6220
  // this row rather than inserting beside it.
6134
- uniqueIndex10("oauth2_grant_client_user_resource_idx").on(table.client, table.user, table.resource),
6221
+ uniqueIndex11("oauth2_grant_client_user_resource_idx").on(table.client, table.user, table.resource),
6135
6222
  // The global-revocation path and the user's own grant list both address
6136
6223
  // rows by user alone.
6137
- index19("oauth2_grant_user_idx").on(table.user)
6224
+ index20("oauth2_grant_user_idx").on(table.user)
6138
6225
  ]
6139
6226
  );
6140
6227
  }
6141
6228
  });
6142
6229
 
6143
6230
  // src/server/entities/oauth2-authorization-codes.ts
6144
- import { text as text23 } from "drizzle-orm/pg-core";
6145
- import { id as id23, timestamps as timestamps21, utcTimestamp as utcTimestamp20, foreignKey as foreignKey16 } from "@spfn/core/db";
6231
+ import { text as text24 } from "drizzle-orm/pg-core";
6232
+ import { id as id24, timestamps as timestamps22, utcTimestamp as utcTimestamp21, foreignKey as foreignKey17 } from "@spfn/core/db";
6146
6233
  var oauth2AuthorizationCodes;
6147
6234
  var init_oauth2_authorization_codes = __esm({
6148
6235
  "src/server/entities/oauth2-authorization-codes.ts"() {
@@ -6152,33 +6239,33 @@ var init_oauth2_authorization_codes = __esm({
6152
6239
  oauth2AuthorizationCodes = authSchema.table(
6153
6240
  "oauth2_authorization_codes",
6154
6241
  {
6155
- id: id23(),
6242
+ id: id24(),
6156
6243
  // SHA-256 hex of the code. The code itself (32 random bytes, base64url)
6157
6244
  // is in the redirect and nowhere else.
6158
- codeHash: text23("code_hash").notNull().unique(),
6159
- grant: foreignKey16("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
6245
+ codeHash: text24("code_hash").notNull().unique(),
6246
+ grant: foreignKey17("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
6160
6247
  // The exact redirect_uri string the authorize request carried. The token
6161
6248
  // request must repeat it character for character (RFC 6749 §4.1.3).
6162
- redirectUri: text23("redirect_uri").notNull(),
6249
+ redirectUri: text24("redirect_uri").notNull(),
6163
6250
  // The PKCE S256 challenge. `code_verifier` at the token endpoint is
6164
6251
  // hashed and compared against this; `plain` is not accepted anywhere,
6165
6252
  // so no method column is needed.
6166
- codeChallenge: text23("code_challenge").notNull(),
6253
+ codeChallenge: text24("code_challenge").notNull(),
6167
6254
  // 60 seconds from issuance. Judged by the database in the statement that
6168
6255
  // spends the row, not only in a read before it.
6169
- expiresAt: utcTimestamp20("expires_at").notNull(),
6256
+ expiresAt: utcTimestamp21("expires_at").notNull(),
6170
6257
  // null = unspent. Non-null means spent, and a second presentation of the
6171
6258
  // same code revokes the grant.
6172
- usedAt: utcTimestamp20("used_at"),
6173
- ...timestamps21()
6259
+ usedAt: utcTimestamp21("used_at"),
6260
+ ...timestamps22()
6174
6261
  }
6175
6262
  );
6176
6263
  }
6177
6264
  });
6178
6265
 
6179
6266
  // src/server/entities/oauth2-tokens.ts
6180
- import { index as index20, text as text24 } from "drizzle-orm/pg-core";
6181
- import { id as id24, timestamps as timestamps22, enumText as enumText13, utcTimestamp as utcTimestamp21, foreignKey as foreignKey17 } from "@spfn/core/db";
6267
+ import { index as index21, text as text25 } from "drizzle-orm/pg-core";
6268
+ import { id as id25, timestamps as timestamps23, enumText as enumText14, utcTimestamp as utcTimestamp22, foreignKey as foreignKey18 } from "@spfn/core/db";
6182
6269
  var OAUTH2_TOKEN_KINDS, oauth2Tokens;
6183
6270
  var init_oauth2_tokens = __esm({
6184
6271
  "src/server/entities/oauth2-tokens.ts"() {
@@ -6189,30 +6276,30 @@ var init_oauth2_tokens = __esm({
6189
6276
  oauth2Tokens = authSchema.table(
6190
6277
  "oauth2_tokens",
6191
6278
  {
6192
- id: id24(),
6279
+ id: id25(),
6193
6280
  // SHA-256 hex of the token value. Lookup key; the unique constraint
6194
6281
  // doubles as the index.
6195
- tokenHash: text24("token_hash").notNull().unique(),
6196
- kind: enumText13("kind", OAUTH2_TOKEN_KINDS).notNull(),
6197
- grant: foreignKey17("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
6282
+ tokenHash: text25("token_hash").notNull().unique(),
6283
+ kind: enumText14("kind", OAUTH2_TOKEN_KINDS).notNull(),
6284
+ grant: foreignKey18("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
6198
6285
  // What this particular token carries, which may be narrower than its
6199
6286
  // grant's scopes: a refresh request is allowed to ask for a subset.
6200
6287
  // Never wider — that is `invalid_scope`.
6201
- scopes: text24("scopes").array().notNull(),
6202
- expiresAt: utcTimestamp21("expires_at").notNull(),
6288
+ scopes: text25("scopes").array().notNull(),
6289
+ expiresAt: utcTimestamp22("expires_at").notNull(),
6203
6290
  // null = live. Set by revoke, by a grant revocation, and by the two
6204
6291
  // replay detections (code reuse, refresh reuse).
6205
- revokedAt: utcTimestamp21("revoked_at"),
6292
+ revokedAt: utcTimestamp22("revoked_at"),
6206
6293
  // Refresh only: set when a rotation issued the successor. A row with
6207
6294
  // this set is spent, and presenting it revokes the grant.
6208
- replacedAt: utcTimestamp21("replaced_at"),
6295
+ replacedAt: utcTimestamp22("replaced_at"),
6209
6296
  // Last successful verification, updated fire-and-forget like ops tokens
6210
- lastUsedAt: utcTimestamp21("last_used_at"),
6211
- ...timestamps22()
6297
+ lastUsedAt: utcTimestamp22("last_used_at"),
6298
+ ...timestamps23()
6212
6299
  },
6213
6300
  (table) => [
6214
6301
  // Revocation addresses every token under one grant.
6215
- index20("oauth2_token_grant_idx").on(table.grant)
6302
+ index21("oauth2_token_grant_idx").on(table.grant)
6216
6303
  ]
6217
6304
  );
6218
6305
  }
@@ -6220,7 +6307,7 @@ var init_oauth2_tokens = __esm({
6220
6307
 
6221
6308
  // src/server/entities/auth-metadata.ts
6222
6309
  import { sql as sql3 } from "drizzle-orm";
6223
- import { text as text25, timestamp } from "drizzle-orm/pg-core";
6310
+ import { text as text26, timestamp } from "drizzle-orm/pg-core";
6224
6311
  var authMetadata;
6225
6312
  var init_auth_metadata = __esm({
6226
6313
  "src/server/entities/auth-metadata.ts"() {
@@ -6230,9 +6317,9 @@ var init_auth_metadata = __esm({
6230
6317
  "auth_metadata",
6231
6318
  {
6232
6319
  // Metadata key (primary key)
6233
- key: text25("key").primaryKey(),
6320
+ key: text26("key").primaryKey(),
6234
6321
  // Metadata value
6235
- value: text25("value").notNull(),
6322
+ value: text26("value").notNull(),
6236
6323
  // Last updated timestamp — stamped by the database on insert and on update
6237
6324
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
6238
6325
  }
@@ -6241,8 +6328,8 @@ var init_auth_metadata = __esm({
6241
6328
  });
6242
6329
 
6243
6330
  // src/server/entities/ops-tokens.ts
6244
- import { text as text26 } from "drizzle-orm/pg-core";
6245
- import { id as id25, timestamps as timestamps23, utcTimestamp as utcTimestamp22 } from "@spfn/core/db";
6331
+ import { text as text27 } from "drizzle-orm/pg-core";
6332
+ import { id as id26, timestamps as timestamps24, utcTimestamp as utcTimestamp23 } from "@spfn/core/db";
6246
6333
  var opsTokens;
6247
6334
  var init_ops_tokens = __esm({
6248
6335
  "src/server/entities/ops-tokens.ts"() {
@@ -6251,22 +6338,22 @@ var init_ops_tokens = __esm({
6251
6338
  opsTokens = authSchema.table(
6252
6339
  "ops_tokens",
6253
6340
  {
6254
- id: id25(),
6341
+ id: id26(),
6255
6342
  // Operator-facing label ("ci-deploy", "rayim-laptop")
6256
- name: text26("name").notNull(),
6343
+ name: text27("name").notNull(),
6257
6344
  // SHA-256 hex of the token secret. Lookup key — the secret never lands
6258
6345
  // here, and the unique constraint doubles as the lookup index.
6259
- tokenHash: text26("token_hash").notNull().unique(),
6346
+ tokenHash: text27("token_hash").notNull().unique(),
6260
6347
  // Granted scopes as permission strings ('waitlist:read', ...).
6261
6348
  // '*' grants every scope.
6262
- scopes: text26("scopes").array().notNull(),
6349
+ scopes: text27("scopes").array().notNull(),
6263
6350
  // null = the token does not expire
6264
- expiresAt: utcTimestamp22("expires_at"),
6351
+ expiresAt: utcTimestamp23("expires_at"),
6265
6352
  // null = active; a timestamp revokes the token permanently
6266
- revokedAt: utcTimestamp22("revoked_at"),
6353
+ revokedAt: utcTimestamp23("revoked_at"),
6267
6354
  // Last successful verification, updated fire-and-forget
6268
- lastUsedAt: utcTimestamp22("last_used_at"),
6269
- ...timestamps23()
6355
+ lastUsedAt: utcTimestamp23("last_used_at"),
6356
+ ...timestamps24()
6270
6357
  }
6271
6358
  );
6272
6359
  }
@@ -6292,6 +6379,7 @@ var init_entities = __esm({
6292
6379
  init_mfa_verifications();
6293
6380
  init_mfa_challenges();
6294
6381
  init_device_authorizations();
6382
+ init_device_links();
6295
6383
  init_user_invitations();
6296
6384
  init_account_deletion_requests();
6297
6385
  init_roles();
@@ -6328,16 +6416,16 @@ var init_users_repository = __esm({
6328
6416
  *
6329
6417
  * Write primary 사용
6330
6418
  */
6331
- async currentKeyEpoch(id26) {
6332
- const result = await this.db.select({ keyEpoch: users.keyEpoch }).from(users).where(eq(users.id, id26)).limit(1);
6419
+ async currentKeyEpoch(id27) {
6420
+ const result = await this.db.select({ keyEpoch: users.keyEpoch }).from(users).where(eq(users.id, id27)).limit(1);
6333
6421
  return result[0]?.keyEpoch ?? 0;
6334
6422
  }
6335
6423
  /**
6336
6424
  * ID로 사용자 조회
6337
6425
  * Read replica 사용
6338
6426
  */
6339
- async findById(id26) {
6340
- const result = await this.readDb.select().from(users).where(eq(users.id, id26)).limit(1);
6427
+ async findById(id27) {
6428
+ const result = await this.readDb.select().from(users).where(eq(users.id, id27)).limit(1);
6341
6429
  return result[0] ?? null;
6342
6430
  }
6343
6431
  /**
@@ -6347,8 +6435,8 @@ var init_users_repository = __esm({
6347
6435
  * 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
6348
6436
  * 계속 사용할 것.
6349
6437
  */
6350
- async findByIdOnPrimary(id26) {
6351
- const result = await this.db.select().from(users).where(eq(users.id, id26)).limit(1);
6438
+ async findByIdOnPrimary(id27) {
6439
+ const result = await this.db.select().from(users).where(eq(users.id, id27)).limit(1);
6352
6440
  return result[0] ?? null;
6353
6441
  }
6354
6442
  /**
@@ -6364,8 +6452,8 @@ var init_users_repository = __esm({
6364
6452
  * Only meaningful inside a transaction — the lock is released at commit.
6365
6453
  * Write primary.
6366
6454
  */
6367
- async lockById(id26) {
6368
- await this.db.select({ id: users.id }).from(users).where(eq(users.id, id26)).for("update");
6455
+ async lockById(id27) {
6456
+ await this.db.select({ id: users.id }).from(users).where(eq(users.id, id27)).for("update");
6369
6457
  }
6370
6458
  /**
6371
6459
  * 이메일로 사용자 조회
@@ -6435,13 +6523,13 @@ var init_users_repository = __esm({
6435
6523
  *
6436
6524
  * roleId가 null인 유저는 role: null 반환
6437
6525
  */
6438
- async findByIdWithRole(id26) {
6526
+ async findByIdWithRole(id27) {
6439
6527
  const result = await this.readDb.select({
6440
6528
  user: users,
6441
6529
  roleName: roles.name,
6442
6530
  roleDisplayName: roles.displayName,
6443
6531
  rolePriority: roles.priority
6444
- }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id26)).limit(1);
6532
+ }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id27)).limit(1);
6445
6533
  const row = result[0];
6446
6534
  if (!row) {
6447
6535
  return null;
@@ -6516,9 +6604,9 @@ var init_users_repository = __esm({
6516
6604
  * 사용자 정보 업데이트
6517
6605
  * Write primary 사용
6518
6606
  */
6519
- async updateById(id26, data) {
6607
+ async updateById(id27, data) {
6520
6608
  const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
6521
- const result = await this.db.update(users).set(patch).where(eq(users.id, id26)).returning();
6609
+ const result = await this.db.update(users).set(patch).where(eq(users.id, id27)).returning();
6522
6610
  return result[0] ?? null;
6523
6611
  }
6524
6612
  /**
@@ -6530,10 +6618,10 @@ var init_users_repository = __esm({
6530
6618
  * status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
6531
6619
  * Write primary 사용
6532
6620
  */
6533
- async reactivateFromPendingDeletion(id26) {
6621
+ async reactivateFromPendingDeletion(id27) {
6534
6622
  const result = await this.db.update(users).set({ status: "active" }).where(
6535
6623
  and(
6536
- eq(users.id, id26),
6624
+ eq(users.id, id27),
6537
6625
  eq(users.status, "pending_deletion")
6538
6626
  )
6539
6627
  ).returning();
@@ -6543,32 +6631,32 @@ var init_users_repository = __esm({
6543
6631
  * 비밀번호 업데이트
6544
6632
  * Write primary 사용
6545
6633
  */
6546
- async updatePassword(id26, passwordHash, clearPasswordChangeRequired = true) {
6634
+ async updatePassword(id27, passwordHash, clearPasswordChangeRequired = true) {
6547
6635
  const updateData = {
6548
6636
  passwordHash
6549
6637
  };
6550
6638
  if (clearPasswordChangeRequired) {
6551
6639
  updateData.passwordChangeRequired = false;
6552
6640
  }
6553
- const result = await this.db.update(users).set(updateData).where(eq(users.id, id26)).returning();
6641
+ const result = await this.db.update(users).set(updateData).where(eq(users.id, id27)).returning();
6554
6642
  return result[0] ?? null;
6555
6643
  }
6556
6644
  /**
6557
6645
  * 마지막 로그인 시간 업데이트
6558
6646
  * Write primary 사용
6559
6647
  */
6560
- async updateLastLogin(id26) {
6648
+ async updateLastLogin(id27) {
6561
6649
  const result = await this.db.update(users).set({
6562
6650
  lastLoginAt: /* @__PURE__ */ new Date()
6563
- }).where(eq(users.id, id26)).returning();
6651
+ }).where(eq(users.id, id27)).returning();
6564
6652
  return result[0] ?? null;
6565
6653
  }
6566
6654
  /**
6567
6655
  * 사용자 삭제
6568
6656
  * Write primary 사용
6569
6657
  */
6570
- async deleteById(id26) {
6571
- const result = await this.db.delete(users).where(eq(users.id, id26)).returning();
6658
+ async deleteById(id27) {
6659
+ const result = await this.db.delete(users).where(eq(users.id, id27)).returning();
6572
6660
  return result[0] ?? null;
6573
6661
  }
6574
6662
  /**
@@ -6756,10 +6844,10 @@ function parseDuration(duration) {
6756
6844
  throw new Error(`Unknown duration unit: ${unit}`);
6757
6845
  }
6758
6846
  }
6759
- function configureAuth(config4) {
6847
+ function configureAuth(config5) {
6760
6848
  globalConfig = {
6761
6849
  ...globalConfig,
6762
- ...config4
6850
+ ...config5
6763
6851
  };
6764
6852
  }
6765
6853
  function getAuthConfig() {
@@ -7170,6 +7258,18 @@ var init_keys_repository = __esm({
7170
7258
  async revokeAllActiveByUserIdExcept(userId, keepKeyId, reason) {
7171
7259
  return await this.revokeActive(userId, reason, keepKeyId);
7172
7260
  }
7261
+ /**
7262
+ * Lock the user's active key rows until the transaction ends, for a revocation
7263
+ * that has to touch other rows before it revokes the keys.
7264
+ *
7265
+ * Device-link statements lock the issuing key row before the link row. A
7266
+ * transaction that expires links and then revokes keys takes them the other
7267
+ * way round, and deadlocks against an issue, redeem, approve or consume in
7268
+ * flight on the same key. Calling this first restores the key-before-link order.
7269
+ */
7270
+ async lockActiveByUserId(userId) {
7271
+ await this.db.select({ id: userPublicKeys.id }).from(userPublicKeys).where(and2(eq2(userPublicKeys.userId, userId), eq2(userPublicKeys.isActive, true))).for("no key update");
7272
+ }
7173
7273
  /**
7174
7274
  * The one statement behind both global revocations.
7175
7275
  *
@@ -7366,7 +7466,7 @@ var init_keys_repository = __esm({
7366
7466
  * replica-lagged address compared in application code both misses real
7367
7467
  * switches and invents ones that did not happen.
7368
7468
  */
7369
- async updateLastUsedById(id26, identity, ip) {
7469
+ async updateLastUsedById(id27, identity, ip) {
7370
7470
  const now = /* @__PURE__ */ new Date();
7371
7471
  const nowParam = sql5`${now.toISOString()}::timestamptz`;
7372
7472
  const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
@@ -7407,7 +7507,7 @@ var init_keys_repository = __esm({
7407
7507
  clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
7408
7508
  } : {}
7409
7509
  }).where(and2(
7410
- eq2(userPublicKeys.id, id26),
7510
+ eq2(userPublicKeys.id, id27),
7411
7511
  or(lastUsedIsStale, identityChanged, sql5`(${ipChanged} AND ${notStampedThisWindow})`)
7412
7512
  ));
7413
7513
  }
@@ -7458,8 +7558,8 @@ var init_verification_codes_repository = __esm({
7458
7558
  * ID로 인증 코드 조회
7459
7559
  * Read replica 사용
7460
7560
  */
7461
- async findById(id26) {
7462
- const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id26)).limit(1);
7561
+ async findById(id27) {
7562
+ const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id27)).limit(1);
7463
7563
  return result[0] ?? null;
7464
7564
  }
7465
7565
  /**
@@ -7473,22 +7573,22 @@ var init_verification_codes_repository = __esm({
7473
7573
  * 인증 코드 사용 처리
7474
7574
  * Write primary 사용
7475
7575
  */
7476
- async markAsUsed(id26) {
7576
+ async markAsUsed(id27) {
7477
7577
  const result = await this.db.update(verificationCodes).set({
7478
7578
  usedAt: /* @__PURE__ */ new Date()
7479
- }).where(eq3(verificationCodes.id, id26)).returning();
7579
+ }).where(eq3(verificationCodes.id, id27)).returning();
7480
7580
  return result[0] ?? null;
7481
7581
  }
7482
7582
  /**
7483
7583
  * 시도 횟수 증가
7484
7584
  * Write primary 사용
7485
7585
  */
7486
- async incrementAttempts(id26) {
7487
- const code = await this.findById(id26);
7586
+ async incrementAttempts(id27) {
7587
+ const code = await this.findById(id27);
7488
7588
  if (!code) return null;
7489
7589
  const result = await this.db.update(verificationCodes).set({
7490
7590
  attempts: code.attempts + 1
7491
- }).where(eq3(verificationCodes.id, id26)).returning();
7591
+ }).where(eq3(verificationCodes.id, id27)).returning();
7492
7592
  return result[0] ?? null;
7493
7593
  }
7494
7594
  /**
@@ -7572,10 +7672,10 @@ var init_signup_link_tokens_repository = __esm({
7572
7672
  *
7573
7673
  * @returns the issued row, or null if the row is no longer deliverable
7574
7674
  */
7575
- async issue(id26, tokenHash) {
7675
+ async issue(id27, tokenHash) {
7576
7676
  const result = await this.db.update(signupLinkTokens).set({ tokenHash }).where(
7577
7677
  and4(
7578
- eq4(signupLinkTokens.id, id26),
7678
+ eq4(signupLinkTokens.id, id27),
7579
7679
  isNull3(signupLinkTokens.consumedAt),
7580
7680
  isNull3(signupLinkTokens.supersededAt),
7581
7681
  isNull3(signupLinkTokens.completedAt),
@@ -7611,14 +7711,14 @@ var init_signup_link_tokens_repository = __esm({
7611
7711
  *
7612
7712
  * @returns the updated row, or null if another request claimed it first
7613
7713
  */
7614
- async claimLink(id26, setupSecretHash, setupExpiresAt) {
7714
+ async claimLink(id27, setupSecretHash, setupExpiresAt) {
7615
7715
  const result = await this.db.update(signupLinkTokens).set({
7616
7716
  consumedAt: /* @__PURE__ */ new Date(),
7617
7717
  setupSecretHash,
7618
7718
  setupExpiresAt
7619
7719
  }).where(
7620
7720
  and4(
7621
- eq4(signupLinkTokens.id, id26),
7721
+ eq4(signupLinkTokens.id, id27),
7622
7722
  isNull3(signupLinkTokens.consumedAt),
7623
7723
  isNull3(signupLinkTokens.supersededAt)
7624
7724
  )
@@ -7631,10 +7731,10 @@ var init_signup_link_tokens_repository = __esm({
7631
7731
  *
7632
7732
  * @returns the updated row, or null if another request completed it first
7633
7733
  */
7634
- async claimSetupSession(id26) {
7734
+ async claimSetupSession(id27) {
7635
7735
  const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
7636
7736
  and4(
7637
- eq4(signupLinkTokens.id, id26),
7737
+ eq4(signupLinkTokens.id, id27),
7638
7738
  isNull3(signupLinkTokens.completedAt),
7639
7739
  isNull3(signupLinkTokens.supersededAt)
7640
7740
  )
@@ -7716,10 +7816,10 @@ var init_password_reset_tokens_repository = __esm({
7716
7816
  *
7717
7817
  * @returns the issued row, or null if the row is no longer deliverable
7718
7818
  */
7719
- async issue(id26, tokenHash) {
7819
+ async issue(id27, tokenHash) {
7720
7820
  const result = await this.db.update(passwordResetTokens).set({ tokenHash }).where(
7721
7821
  and5(
7722
- eq5(passwordResetTokens.id, id26),
7822
+ eq5(passwordResetTokens.id, id27),
7723
7823
  isNull4(passwordResetTokens.consumedAt),
7724
7824
  isNull4(passwordResetTokens.supersededAt),
7725
7825
  isNull4(passwordResetTokens.completedAt),
@@ -7767,14 +7867,14 @@ var init_password_reset_tokens_repository = __esm({
7767
7867
  *
7768
7868
  * @returns the updated row, or null if another request claimed it first
7769
7869
  */
7770
- async consume(id26, setupSecretHash, setupExpiresAt) {
7870
+ async consume(id27, setupSecretHash, setupExpiresAt) {
7771
7871
  const result = await this.db.update(passwordResetTokens).set({
7772
7872
  consumedAt: /* @__PURE__ */ new Date(),
7773
7873
  setupSecretHash,
7774
7874
  setupExpiresAt
7775
7875
  }).where(
7776
7876
  and5(
7777
- eq5(passwordResetTokens.id, id26),
7877
+ eq5(passwordResetTokens.id, id27),
7778
7878
  isNull4(passwordResetTokens.consumedAt),
7779
7879
  isNull4(passwordResetTokens.supersededAt)
7780
7880
  )
@@ -7787,10 +7887,10 @@ var init_password_reset_tokens_repository = __esm({
7787
7887
  *
7788
7888
  * @returns the updated row, or null if another request completed it first
7789
7889
  */
7790
- async complete(id26) {
7890
+ async complete(id27) {
7791
7891
  const result = await this.db.update(passwordResetTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
7792
7892
  and5(
7793
- eq5(passwordResetTokens.id, id26),
7893
+ eq5(passwordResetTokens.id, id27),
7794
7894
  isNull4(passwordResetTokens.completedAt),
7795
7895
  isNull4(passwordResetTokens.supersededAt)
7796
7896
  )
@@ -7988,9 +8088,9 @@ var init_passkeys_repository = __esm({
7988
8088
  * Owner-scoped, so an id belonging to someone else answers null rather than
7989
8089
  * a row — a management route can only ever say "not yours".
7990
8090
  */
7991
- async findLiveByIdAndUserId(id26, userId) {
8091
+ async findLiveByIdAndUserId(id27, userId) {
7992
8092
  const result = await this.db.select().from(passkeys).where(and7(
7993
- eq7(passkeys.id, id26),
8093
+ eq7(passkeys.id, id27),
7994
8094
  eq7(passkeys.userId, userId),
7995
8095
  isNull6(passkeys.revokedAt)
7996
8096
  )).limit(1);
@@ -8000,17 +8100,17 @@ var init_passkeys_repository = __esm({
8000
8100
  * Record a successful assertion.
8001
8101
  * Write primary.
8002
8102
  */
8003
- async recordUse(id26, counter) {
8004
- await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id, id26));
8103
+ async recordUse(id27, counter) {
8104
+ await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id, id27));
8005
8105
  }
8006
8106
  /**
8007
8107
  * Rename, but only a credential this user still owns and has not revoked.
8008
8108
  *
8009
8109
  * @returns the updated row, or null if it is not theirs or already revoked
8010
8110
  */
8011
- async renameByIdAndUserId(id26, userId, label) {
8111
+ async renameByIdAndUserId(id27, userId, label) {
8012
8112
  const result = await this.db.update(passkeys).set({ label }).where(and7(
8013
- eq7(passkeys.id, id26),
8113
+ eq7(passkeys.id, id27),
8014
8114
  eq7(passkeys.userId, userId),
8015
8115
  isNull6(passkeys.revokedAt)
8016
8116
  )).returning();
@@ -8025,9 +8125,9 @@ var init_passkeys_repository = __esm({
8025
8125
  *
8026
8126
  * @returns the updated row, or null if it is not theirs or already revoked
8027
8127
  */
8028
- async revokeByIdAndUserId(id26, userId, reason) {
8128
+ async revokeByIdAndUserId(id27, userId, reason) {
8029
8129
  const result = await this.db.update(passkeys).set({ revokedAt: /* @__PURE__ */ new Date(), revokedReason: reason }).where(and7(
8030
- eq7(passkeys.id, id26),
8130
+ eq7(passkeys.id, id27),
8031
8131
  eq7(passkeys.userId, userId),
8032
8132
  isNull6(passkeys.revokedAt)
8033
8133
  )).returning();
@@ -8042,9 +8142,9 @@ var init_passkeys_repository = __esm({
8042
8142
  *
8043
8143
  * @returns the updated row, or null if it is not theirs or already revoked
8044
8144
  */
8045
- async markSecondFactorByIdAndUserId(id26, userId, secondFactor) {
8145
+ async markSecondFactorByIdAndUserId(id27, userId, secondFactor) {
8046
8146
  const result = await this.db.update(passkeys).set({ secondFactor }).where(and7(
8047
- eq7(passkeys.id, id26),
8147
+ eq7(passkeys.id, id27),
8048
8148
  eq7(passkeys.userId, userId),
8049
8149
  isNull6(passkeys.revokedAt)
8050
8150
  )).returning();
@@ -8265,8 +8365,8 @@ var init_mfa_recovery_codes_repository = __esm({
8265
8365
  *
8266
8366
  * @returns true when this call spent the row
8267
8367
  */
8268
- async consume(id26) {
8269
- const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id, id26), isNull9(mfaRecoveryCodes.usedAt))).returning();
8368
+ async consume(id27) {
8369
+ const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id, id27), isNull9(mfaRecoveryCodes.usedAt))).returning();
8270
8370
  return result.length > 0;
8271
8371
  }
8272
8372
  /** Drop every generation the account has. Write primary. */
@@ -8368,9 +8468,9 @@ var init_mfa_challenges_repository = __esm({
8368
8468
  * Every condition the caller checked is restated here, so the row that is
8369
8469
  * marked is a row that was still spendable at the moment it was marked.
8370
8470
  */
8371
- async markVerified(id26, keyEpoch) {
8471
+ async markVerified(id27, keyEpoch) {
8372
8472
  const result = await this.db.update(mfaChallenges).set({ verifiedAt: /* @__PURE__ */ new Date() }).where(and12(
8373
- eq12(mfaChallenges.id, id26),
8473
+ eq12(mfaChallenges.id, id27),
8374
8474
  eq12(mfaChallenges.keyEpoch, keyEpoch),
8375
8475
  isNull10(mfaChallenges.verifiedAt),
8376
8476
  gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
@@ -8384,8 +8484,8 @@ var init_mfa_challenges_repository = __esm({
8384
8484
  * is the whole reason lookup is by hash: there is no id for a caller to name,
8385
8485
  * so no counter but their own can be moved.
8386
8486
  */
8387
- async countFailure(id26) {
8388
- const result = await this.db.update(mfaChallenges).set({ attempts: sql8`${mfaChallenges.attempts} + 1` }).where(eq12(mfaChallenges.id, id26)).returning({ attempts: mfaChallenges.attempts });
8487
+ async countFailure(id27) {
8488
+ const result = await this.db.update(mfaChallenges).set({ attempts: sql8`${mfaChallenges.attempts} + 1` }).where(eq12(mfaChallenges.id, id27)).returning({ attempts: mfaChallenges.attempts });
8389
8489
  return (result[0]?.attempts ?? 0) >= MFA_CHALLENGE_ATTEMPT_LIMIT;
8390
8490
  }
8391
8491
  /**
@@ -8407,9 +8507,9 @@ var init_mfa_challenges_repository = __esm({
8407
8507
  * another ten minutes; only the secret is new, because the old one was never
8408
8508
  * stored and cannot be handed out twice.
8409
8509
  */
8410
- async resecret(id26, challengeHash) {
8510
+ async resecret(id27, challengeHash) {
8411
8511
  const result = await this.db.update(mfaChallenges).set({ challengeHash }).where(and12(
8412
- eq12(mfaChallenges.id, id26),
8512
+ eq12(mfaChallenges.id, id27),
8413
8513
  isNull10(mfaChallenges.verifiedAt),
8414
8514
  gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
8415
8515
  )).returning({ id: mfaChallenges.id });
@@ -8505,6 +8605,107 @@ var init_mfa_enrolment_repository = __esm({
8505
8605
  }
8506
8606
  });
8507
8607
 
8608
+ // src/server/lib/answer-waiters.ts
8609
+ import { onAfterCommit } from "@spfn/core/db";
8610
+ function createAnswerWaiters() {
8611
+ const parkedById = /* @__PURE__ */ new Map();
8612
+ const waitingById = /* @__PURE__ */ new Map();
8613
+ function wake(id27) {
8614
+ const parked = parkedById.get(id27);
8615
+ if (!parked) {
8616
+ return;
8617
+ }
8618
+ parkedById.delete(id27);
8619
+ for (const resolve of parked) {
8620
+ resolve();
8621
+ }
8622
+ }
8623
+ function unpark(id27, parked, resolver) {
8624
+ parked.delete(resolver);
8625
+ if (parked.size === 0 && parkedById.get(id27) === parked) {
8626
+ parkedById.delete(id27);
8627
+ }
8628
+ }
8629
+ function announce(ids) {
8630
+ if (ids.length === 0) {
8631
+ return;
8632
+ }
8633
+ onAfterCommit(() => {
8634
+ for (const id27 of ids) {
8635
+ wake(id27);
8636
+ }
8637
+ });
8638
+ }
8639
+ function wait(id27, timeoutMs, signal) {
8640
+ if (signal?.aborted) {
8641
+ return Promise.resolve();
8642
+ }
8643
+ return new Promise((resolve) => {
8644
+ const parked = parkedById.get(id27) ?? /* @__PURE__ */ new Set();
8645
+ const done = () => {
8646
+ clearTimeout(timer);
8647
+ signal?.removeEventListener("abort", done);
8648
+ unpark(id27, parked, done);
8649
+ resolve();
8650
+ };
8651
+ const timer = setTimeout(done, timeoutMs);
8652
+ signal?.addEventListener("abort", done, { once: true });
8653
+ parked.add(done);
8654
+ parkedById.set(id27, parked);
8655
+ });
8656
+ }
8657
+ function waiting(id27) {
8658
+ return waitingById.get(id27) ?? 0;
8659
+ }
8660
+ async function hold(id27, run) {
8661
+ waitingById.set(id27, waiting(id27) + 1);
8662
+ try {
8663
+ return await run();
8664
+ } finally {
8665
+ const left = waiting(id27) - 1;
8666
+ if (left > 0) {
8667
+ waitingById.set(id27, left);
8668
+ } else {
8669
+ waitingById.delete(id27);
8670
+ }
8671
+ }
8672
+ }
8673
+ return {
8674
+ announce,
8675
+ wait,
8676
+ waiting,
8677
+ hold,
8678
+ parkedCount: () => parkedById.size + waitingById.size
8679
+ };
8680
+ }
8681
+ var init_answer_waiters = __esm({
8682
+ "src/server/lib/answer-waiters.ts"() {
8683
+ "use strict";
8684
+ }
8685
+ });
8686
+
8687
+ // src/server/lib/device-auth-waiters.ts
8688
+ function announceDeviceAuthAnswered(ids) {
8689
+ waiters.announce(ids);
8690
+ }
8691
+ function waitForDeviceAuthAnswer(id27, timeoutMs, signal) {
8692
+ return waiters.wait(id27, timeoutMs, signal);
8693
+ }
8694
+ function waitingOnDeviceAuth(id27) {
8695
+ return waiters.waiting(id27);
8696
+ }
8697
+ function holdDeviceAuthWait(id27, wait) {
8698
+ return waiters.hold(id27, wait);
8699
+ }
8700
+ var waiters;
8701
+ var init_device_auth_waiters = __esm({
8702
+ "src/server/lib/device-auth-waiters.ts"() {
8703
+ "use strict";
8704
+ init_answer_waiters();
8705
+ waiters = createAnswerWaiters();
8706
+ }
8707
+ });
8708
+
8508
8709
  // src/server/repositories/device-authorizations.repository.ts
8509
8710
  import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
8510
8711
  import { eq as eq14, and as and14, gt as gt7, inArray as inArray2, sql as sql9 } from "drizzle-orm";
@@ -8513,6 +8714,7 @@ var init_device_authorizations_repository = __esm({
8513
8714
  "src/server/repositories/device-authorizations.repository.ts"() {
8514
8715
  "use strict";
8515
8716
  init_device_authorizations();
8717
+ init_device_auth_waiters();
8516
8718
  notExpired = () => gt7(deviceAuthorizations.expiresAt, sql9`now()`);
8517
8719
  DeviceAuthorizationsRepository = class extends BaseRepository14 {
8518
8720
  /**
@@ -8555,6 +8757,19 @@ var init_device_authorizations_repository = __esm({
8555
8757
  const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
8556
8758
  return result[0] ?? null;
8557
8759
  }
8760
+ /**
8761
+ * `findByDeviceCodeHash`, read from the primary even outside a transaction.
8762
+ *
8763
+ * For the long poll's wait, which runs before any transaction opens: it is
8764
+ * woken right after an answer commits, and a replica that has not caught up
8765
+ * yet would show it the record still pending and send it back to sleep.
8766
+ *
8767
+ * Write primary.
8768
+ */
8769
+ async findByDeviceCodeHashOnPrimary(deviceCodeHash) {
8770
+ const result = await this.db.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
8771
+ return result[0] ?? null;
8772
+ }
8558
8773
  /**
8559
8774
  * Bind the approving user and move the record to `approved`, but only from
8560
8775
  * `pending`.
@@ -8564,15 +8779,15 @@ var init_device_authorizations_repository = __esm({
8564
8779
  *
8565
8780
  * @returns the updated row, or null if it was no longer pending, or expired
8566
8781
  */
8567
- async approve(id26, userId) {
8782
+ async approve(id27, userId) {
8568
8783
  const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
8569
8784
  and14(
8570
- eq14(deviceAuthorizations.id, id26),
8785
+ eq14(deviceAuthorizations.id, id27),
8571
8786
  eq14(deviceAuthorizations.status, "pending"),
8572
8787
  notExpired()
8573
8788
  )
8574
8789
  ).returning();
8575
- return result[0] ?? null;
8790
+ return this.answered(result[0] ?? null);
8576
8791
  }
8577
8792
  /**
8578
8793
  * Move the record to `denied`, but only from `pending`.
@@ -8581,15 +8796,15 @@ var init_device_authorizations_repository = __esm({
8581
8796
  *
8582
8797
  * @returns the updated row, or null if it was no longer pending, or expired
8583
8798
  */
8584
- async deny(id26) {
8799
+ async deny(id27) {
8585
8800
  const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8586
8801
  and14(
8587
- eq14(deviceAuthorizations.id, id26),
8802
+ eq14(deviceAuthorizations.id, id27),
8588
8803
  eq14(deviceAuthorizations.status, "pending"),
8589
8804
  notExpired()
8590
8805
  )
8591
8806
  ).returning();
8592
- return result[0] ?? null;
8807
+ return this.answered(result[0] ?? null);
8593
8808
  }
8594
8809
  /**
8595
8810
  * Refuse every authorization a user still has in flight.
@@ -8617,12 +8832,14 @@ var init_device_authorizations_repository = __esm({
8617
8832
  * @returns the rows this call refused
8618
8833
  */
8619
8834
  async denyAllActiveByUserId(userId) {
8620
- return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8835
+ const denied = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8621
8836
  and14(
8622
8837
  eq14(deviceAuthorizations.userId, userId),
8623
8838
  inArray2(deviceAuthorizations.status, ["pending", "approved"])
8624
8839
  )
8625
8840
  ).returning();
8841
+ announceDeviceAuthAnswered(denied.map((record) => record.id));
8842
+ return denied;
8626
8843
  }
8627
8844
  /**
8628
8845
  * Spend an approved record, but only from `approved`, and address it by the
@@ -8645,32 +8862,255 @@ var init_device_authorizations_repository = __esm({
8645
8862
  ).returning();
8646
8863
  return result[0] ?? null;
8647
8864
  }
8865
+ /** Wake the polls parked on a record this call moved, and hand the row back. */
8866
+ answered(record) {
8867
+ if (record) {
8868
+ announceDeviceAuthAnswered([record.id]);
8869
+ }
8870
+ return record;
8871
+ }
8648
8872
  };
8649
8873
  deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
8650
8874
  }
8651
8875
  });
8652
8876
 
8653
- // src/server/repositories/roles.repository.ts
8877
+ // src/server/lib/device-link-waiters.ts
8878
+ function announceDeviceLinkMoved(ids) {
8879
+ waiters2.announce(ids);
8880
+ }
8881
+ function waitForDeviceLinkMove(id27, timeoutMs, signal) {
8882
+ return waiters2.wait(id27, timeoutMs, signal);
8883
+ }
8884
+ function waitingOnDeviceLink(id27) {
8885
+ return waiters2.waiting(id27);
8886
+ }
8887
+ function holdDeviceLinkWait(id27, wait) {
8888
+ return waiters2.hold(id27, wait);
8889
+ }
8890
+ var waiters2;
8891
+ var init_device_link_waiters = __esm({
8892
+ "src/server/lib/device-link-waiters.ts"() {
8893
+ "use strict";
8894
+ init_answer_waiters();
8895
+ waiters2 = createAnswerWaiters();
8896
+ }
8897
+ });
8898
+
8899
+ // src/server/repositories/device-links.repository.ts
8654
8900
  import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
8655
- import { eq as eq15, asc } from "drizzle-orm";
8901
+ import { eq as eq15, and as and15, gt as gt8, or as or4, isNull as isNull12, inArray as inArray3, exists, getTableColumns, sql as sql10 } from "drizzle-orm";
8902
+ var LIVE_STATUSES, notExpired2, DeviceLinksRepository, deviceLinksRepository;
8903
+ var init_device_links_repository = __esm({
8904
+ "src/server/repositories/device-links.repository.ts"() {
8905
+ "use strict";
8906
+ init_entities();
8907
+ init_device_link_waiters();
8908
+ LIVE_STATUSES = ["issued", "redeemed", "approved"];
8909
+ notExpired2 = () => gt8(deviceLinks.expiresAt, sql10`now()`);
8910
+ DeviceLinksRepository = class extends BaseRepository15 {
8911
+ /**
8912
+ * Insert an issued link, unless its user code is already taken.
8913
+ *
8914
+ * `onConflictDoNothing` for the reason `DeviceAuthorizationsRepository.create`
8915
+ * gives: a raised unique violation would abort the issue route's transaction
8916
+ * and leave no way to retry with a fresh code.
8917
+ *
8918
+ * @returns the inserted row, or null if the code collided
8919
+ */
8920
+ async create(data) {
8921
+ const result = await this.db.insert(deviceLinks).values(data).onConflictDoNothing().returning();
8922
+ return result[0] ?? null;
8923
+ }
8924
+ /**
8925
+ * Find a link by the issuer's handle, in any state.
8926
+ *
8927
+ * Unfiltered, as `DeviceAuthorizationsRepository.findByUserCode` is: which
8928
+ * refusal a caller is owed is the service's decision.
8929
+ */
8930
+ async findByLinkId(linkId) {
8931
+ return this.findOne(eq15(deviceLinks.linkId, linkId));
8932
+ }
8933
+ /** Find a link by its normalized user code, in any state. */
8934
+ async findByUserCode(userCode) {
8935
+ return this.findOne(eq15(deviceLinks.userCode, userCode));
8936
+ }
8937
+ /** Find a link by the hash of the device code its redeemer holds, in any state. */
8938
+ async findByDeviceCodeHash(deviceCodeHash) {
8939
+ return this.findOne(eq15(deviceLinks.deviceCodeHash, deviceCodeHash));
8940
+ }
8941
+ /**
8942
+ * Park a device's key on an issued link and move it to `redeemed`.
8943
+ *
8944
+ * From `issued` only, so of two devices redeeming one code exactly one wins;
8945
+ * the other matches nothing and is answered as if the code were unknown.
8946
+ *
8947
+ * @returns the updated row, or null if it was no longer issued, expired, or its issuer signed out
8948
+ */
8949
+ async redeem(id27, redemption) {
8950
+ return this.transition(
8951
+ { ...redemption, status: "redeemed", redeemedAt: /* @__PURE__ */ new Date() },
8952
+ and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "issued"), notExpired2(), this.issuerKeyLive(true))
8953
+ );
8954
+ }
8955
+ /**
8956
+ * The issuer picked the right number: move the link to `approved`, from
8957
+ * `redeemed` only.
8958
+ *
8959
+ * The key is not registered here, for the reason device-code approval gives:
8960
+ * the redeeming device may never come back for it.
8961
+ *
8962
+ * @returns the updated row, or null if it was no longer redeemed, expired, or its issuer signed out
8963
+ */
8964
+ async approve(id27) {
8965
+ return this.transition(
8966
+ { status: "approved", approvedAt: /* @__PURE__ */ new Date() },
8967
+ and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "redeemed"), notExpired2(), this.issuerKeyLive(true))
8968
+ );
8969
+ }
8970
+ /**
8971
+ * Refuse a redeemed link — the issuer said no, or picked the wrong number.
8972
+ *
8973
+ * No issuing-key condition: refusing is always safe, and a signed-out issuer's
8974
+ * link is refused anyway.
8975
+ *
8976
+ * @returns the updated row, or null if it was no longer redeemed, or expired
8977
+ */
8978
+ async deny(id27) {
8979
+ return this.transition(
8980
+ { status: "denied" },
8981
+ and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "redeemed"), notExpired2())
8982
+ );
8983
+ }
8984
+ /**
8985
+ * The issuer closed the link before anyone was let in: `issued` or `redeemed`
8986
+ * to `expired`.
8987
+ *
8988
+ * @returns the updated row, or null if it had moved past redeemed, or expired
8989
+ */
8990
+ async cancel(id27) {
8991
+ return this.transition(
8992
+ { status: "expired" },
8993
+ and15(eq15(deviceLinks.id, id27), inArray3(deviceLinks.status, ["issued", "redeemed"]), notExpired2())
8994
+ );
8995
+ }
8996
+ /**
8997
+ * Spend an approved link, addressed by the device code hash the caller
8998
+ * presented — the one-shot `DeviceAuthorizationsRepository.consumeApproved`
8999
+ * is, with the issuing key judged in the same statement.
9000
+ *
9001
+ * @returns the spent row, or null if it was not approved (any more), expired, or its issuer signed out
9002
+ */
9003
+ async consumeApproved(deviceCodeHash) {
9004
+ return this.transition(
9005
+ { status: "consumed", consumedAt: /* @__PURE__ */ new Date() },
9006
+ and15(
9007
+ eq15(deviceLinks.deviceCodeHash, deviceCodeHash),
9008
+ eq15(deviceLinks.status, "approved"),
9009
+ notExpired2(),
9010
+ this.issuerKeyLive(true)
9011
+ )
9012
+ );
9013
+ }
9014
+ /**
9015
+ * Lock the issuing key's row until the transaction ends, so two issues from
9016
+ * one key run one after the other: each expires the other's link or waits for
9017
+ * it to be inserted, and never both read "nothing live" and insert.
9018
+ *
9019
+ * `no key update` is the lock a plain UPDATE of the row takes: it conflicts
9020
+ * with itself, with the share lock `issuerKeyLive(true)` takes and with a
9021
+ * revocation's UPDATE, and it leaves foreign-key checks on the row alone.
9022
+ * Taken first, before any link row, so every path that locks both — redeem,
9023
+ * approve, consume and the revocations — locks the key row before the link.
9024
+ */
9025
+ async lockIssuerKey(issuerKeyId) {
9026
+ await this.db.select({ one: sql10`1` }).from(userPublicKeys).where(eq15(userPublicKeys.keyId, issuerKeyId)).for("no key update");
9027
+ }
9028
+ /**
9029
+ * Expire the live link a key issued, so a fresh issue leaves one link per key.
9030
+ *
9031
+ * Race-free only after `lockIssuerKey` in the same transaction.
9032
+ *
9033
+ * @returns the rows this call expired
9034
+ */
9035
+ async expireLiveByIssuerKey(issuerKeyId) {
9036
+ return this.expireLive(eq15(deviceLinks.issuerKeyId, issuerKeyId));
9037
+ }
9038
+ /**
9039
+ * Expire every link an account still has in play — the device-link half of a
9040
+ * global revocation, beside `DeviceAuthorizationsRepository.denyAllActiveByUserId`.
9041
+ *
9042
+ * A revoke-all that spares the calling device spares its key too, so the
9043
+ * issuing-key condition alone would leave that device's link able to let a
9044
+ * new device in seconds after the owner signed everything else out.
9045
+ *
9046
+ * @returns the rows this call expired
9047
+ */
9048
+ async expireAllLiveByUserId(userId) {
9049
+ return this.expireLive(eq15(deviceLinks.issuerUserId, userId));
9050
+ }
9051
+ async expireLive(owner) {
9052
+ const expired = await this.db.update(deviceLinks).set({ status: "expired" }).where(and15(owner, inArray3(deviceLinks.status, LIVE_STATUSES))).returning();
9053
+ announceDeviceLinkMoved(expired.map((link) => link.id));
9054
+ return expired;
9055
+ }
9056
+ /** Apply one conditional UPDATE, wake the waiters of the row it moved, and hand the row back. */
9057
+ async transition(values, where) {
9058
+ const result = await this.db.update(deviceLinks).set(values).where(where).returning();
9059
+ const moved = result[0] ?? null;
9060
+ if (moved) {
9061
+ announceDeviceLinkMoved([moved.id]);
9062
+ }
9063
+ return moved;
9064
+ }
9065
+ /** One link with its issuing key's standing. */
9066
+ async findOne(where) {
9067
+ const result = await this.db.select({ ...getTableColumns(deviceLinks), issuerKeyLive: this.issuerKeyLive(false) }).from(deviceLinks).where(where).limit(1);
9068
+ return result[0] ?? null;
9069
+ }
9070
+ /**
9071
+ * Whether the link's issuing key is still registered to its issuer, active
9072
+ * and unexpired — the test `authenticate` applies to the same row.
9073
+ *
9074
+ * @param lock take a share lock on the key row, so a transition waits for a
9075
+ * revocation in flight instead of judging the version before it
9076
+ */
9077
+ issuerKeyLive(lock) {
9078
+ const key = this.db.select({ one: sql10`1` }).from(userPublicKeys).where(
9079
+ and15(
9080
+ eq15(userPublicKeys.keyId, deviceLinks.issuerKeyId),
9081
+ eq15(userPublicKeys.userId, deviceLinks.issuerUserId),
9082
+ eq15(userPublicKeys.isActive, true),
9083
+ or4(isNull12(userPublicKeys.expiresAt), gt8(userPublicKeys.expiresAt, sql10`now()`))
9084
+ )
9085
+ );
9086
+ return sql10`${exists(lock ? key.for("share") : key)}`;
9087
+ }
9088
+ };
9089
+ deviceLinksRepository = new DeviceLinksRepository();
9090
+ }
9091
+ });
9092
+
9093
+ // src/server/repositories/roles.repository.ts
9094
+ import { BaseRepository as BaseRepository16 } from "@spfn/core/db";
9095
+ import { eq as eq16, asc } from "drizzle-orm";
8656
9096
  var RolesRepository, rolesRepository;
8657
9097
  var init_roles_repository = __esm({
8658
9098
  "src/server/repositories/roles.repository.ts"() {
8659
9099
  "use strict";
8660
9100
  init_roles();
8661
- RolesRepository = class extends BaseRepository15 {
9101
+ RolesRepository = class extends BaseRepository16 {
8662
9102
  /**
8663
9103
  * ID로 역할 조회
8664
9104
  */
8665
- async findById(id26) {
8666
- const result = await this.readDb.select().from(roles).where(eq15(roles.id, id26)).limit(1);
9105
+ async findById(id27) {
9106
+ const result = await this.readDb.select().from(roles).where(eq16(roles.id, id27)).limit(1);
8667
9107
  return result[0] ?? null;
8668
9108
  }
8669
9109
  /**
8670
9110
  * Name으로 역할 조회
8671
9111
  */
8672
9112
  async findByName(name) {
8673
- const result = await this.readDb.select().from(roles).where(eq15(roles.name, name)).limit(1);
9113
+ const result = await this.readDb.select().from(roles).where(eq16(roles.name, name)).limit(1);
8674
9114
  return result[0] ?? null;
8675
9115
  }
8676
9116
  /**
@@ -8683,7 +9123,7 @@ var init_roles_repository = __esm({
8683
9123
  * 활성 역할만 조회
8684
9124
  */
8685
9125
  async findActive() {
8686
- return this.readDb.select().from(roles).where(eq15(roles.isActive, true)).orderBy(asc(roles.priority));
9126
+ return this.readDb.select().from(roles).where(eq16(roles.isActive, true)).orderBy(asc(roles.priority));
8687
9127
  }
8688
9128
  /**
8689
9129
  * 역할 생성
@@ -8694,15 +9134,15 @@ var init_roles_repository = __esm({
8694
9134
  /**
8695
9135
  * 역할 업데이트
8696
9136
  */
8697
- async updateById(id26, data) {
8698
- const result = await this.db.update(roles).set(data).where(eq15(roles.id, id26)).returning();
9137
+ async updateById(id27, data) {
9138
+ const result = await this.db.update(roles).set(data).where(eq16(roles.id, id27)).returning();
8699
9139
  return result[0] ?? null;
8700
9140
  }
8701
9141
  /**
8702
9142
  * 역할 삭제
8703
9143
  */
8704
- async deleteById(id26) {
8705
- const result = await this.db.delete(roles).where(eq15(roles.id, id26)).returning();
9144
+ async deleteById(id27) {
9145
+ const result = await this.db.delete(roles).where(eq16(roles.id, id27)).returning();
8706
9146
  return result[0] ?? null;
8707
9147
  }
8708
9148
  };
@@ -8711,26 +9151,26 @@ var init_roles_repository = __esm({
8711
9151
  });
8712
9152
 
8713
9153
  // src/server/repositories/permissions.repository.ts
8714
- import { BaseRepository as BaseRepository16 } from "@spfn/core/db";
8715
- import { asc as asc2, eq as eq16, inArray as inArray3 } from "drizzle-orm";
9154
+ import { BaseRepository as BaseRepository17 } from "@spfn/core/db";
9155
+ import { asc as asc2, eq as eq17, inArray as inArray4 } from "drizzle-orm";
8716
9156
  var PermissionsRepository, permissionsRepository;
8717
9157
  var init_permissions_repository = __esm({
8718
9158
  "src/server/repositories/permissions.repository.ts"() {
8719
9159
  "use strict";
8720
9160
  init_permissions();
8721
- PermissionsRepository = class extends BaseRepository16 {
9161
+ PermissionsRepository = class extends BaseRepository17 {
8722
9162
  /**
8723
9163
  * ID로 권한 조회
8724
9164
  */
8725
- async findById(id26) {
8726
- const result = await this.readDb.select().from(permissions).where(eq16(permissions.id, id26)).limit(1);
9165
+ async findById(id27) {
9166
+ const result = await this.readDb.select().from(permissions).where(eq17(permissions.id, id27)).limit(1);
8727
9167
  return result[0] ?? null;
8728
9168
  }
8729
9169
  /**
8730
9170
  * Name으로 권한 조회
8731
9171
  */
8732
9172
  async findByName(name) {
8733
- const result = await this.readDb.select().from(permissions).where(eq16(permissions.name, name)).limit(1);
9173
+ const result = await this.readDb.select().from(permissions).where(eq17(permissions.name, name)).limit(1);
8734
9174
  return result[0] ?? null;
8735
9175
  }
8736
9176
  /**
@@ -8738,7 +9178,7 @@ var init_permissions_repository = __esm({
8738
9178
  */
8739
9179
  async findByNames(names) {
8740
9180
  if (names.length === 0) return [];
8741
- return this.readDb.select().from(permissions).where(inArray3(permissions.name, names));
9181
+ return this.readDb.select().from(permissions).where(inArray4(permissions.name, names));
8742
9182
  }
8743
9183
  /**
8744
9184
  * 모든 권한 조회
@@ -8750,13 +9190,13 @@ var init_permissions_repository = __esm({
8750
9190
  * 활성 권한만 조회
8751
9191
  */
8752
9192
  async findActive() {
8753
- return this.readDb.select().from(permissions).where(eq16(permissions.isActive, true)).orderBy(asc2(permissions.name));
9193
+ return this.readDb.select().from(permissions).where(eq17(permissions.isActive, true)).orderBy(asc2(permissions.name));
8754
9194
  }
8755
9195
  /**
8756
9196
  * 카테고리별 권한 조회
8757
9197
  */
8758
9198
  async findByCategory(category) {
8759
- return this.readDb.select().from(permissions).where(eq16(permissions.category, category)).orderBy(asc2(permissions.name));
9199
+ return this.readDb.select().from(permissions).where(eq17(permissions.category, category)).orderBy(asc2(permissions.name));
8760
9200
  }
8761
9201
  /**
8762
9202
  * 권한 생성
@@ -8774,15 +9214,15 @@ var init_permissions_repository = __esm({
8774
9214
  /**
8775
9215
  * 권한 업데이트
8776
9216
  */
8777
- async updateById(id26, data) {
8778
- const result = await this.db.update(permissions).set(data).where(eq16(permissions.id, id26)).returning();
9217
+ async updateById(id27, data) {
9218
+ const result = await this.db.update(permissions).set(data).where(eq17(permissions.id, id27)).returning();
8779
9219
  return result[0] ?? null;
8780
9220
  }
8781
9221
  /**
8782
9222
  * 권한 삭제
8783
9223
  */
8784
- async deleteById(id26) {
8785
- const result = await this.db.delete(permissions).where(eq16(permissions.id, id26)).returning();
9224
+ async deleteById(id27) {
9225
+ const result = await this.db.delete(permissions).where(eq17(permissions.id, id27)).returning();
8786
9226
  return result[0] ?? null;
8787
9227
  }
8788
9228
  };
@@ -8791,25 +9231,25 @@ var init_permissions_repository = __esm({
8791
9231
  });
8792
9232
 
8793
9233
  // src/server/repositories/role-permissions.repository.ts
8794
- import { BaseRepository as BaseRepository17 } from "@spfn/core/db";
8795
- import { and as and15, eq as eq17 } from "drizzle-orm";
9234
+ import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
9235
+ import { and as and16, eq as eq18 } from "drizzle-orm";
8796
9236
  var RolePermissionsRepository, rolePermissionsRepository;
8797
9237
  var init_role_permissions_repository = __esm({
8798
9238
  "src/server/repositories/role-permissions.repository.ts"() {
8799
9239
  "use strict";
8800
9240
  init_role_permissions();
8801
- RolePermissionsRepository = class extends BaseRepository17 {
9241
+ RolePermissionsRepository = class extends BaseRepository18 {
8802
9242
  /**
8803
9243
  * 역할 ID로 모든 권한 조회
8804
9244
  */
8805
9245
  async findByRoleId(roleId) {
8806
- return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.roleId, roleId));
9246
+ return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.roleId, roleId));
8807
9247
  }
8808
9248
  /**
8809
9249
  * 권한 ID로 모든 역할 조회
8810
9250
  */
8811
9251
  async findByPermissionId(permissionId) {
8812
- return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.permissionId, permissionId));
9252
+ return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.permissionId, permissionId));
8813
9253
  }
8814
9254
  /**
8815
9255
  * 역할-권한 매핑 생성
@@ -8829,9 +9269,9 @@ var init_role_permissions_repository = __esm({
8829
9269
  */
8830
9270
  async deleteByRoleIdAndPermissionId(roleId, permissionId) {
8831
9271
  const result = await this.db.delete(rolePermissions).where(
8832
- and15(
8833
- eq17(rolePermissions.roleId, roleId),
8834
- eq17(rolePermissions.permissionId, permissionId)
9272
+ and16(
9273
+ eq18(rolePermissions.roleId, roleId),
9274
+ eq18(rolePermissions.permissionId, permissionId)
8835
9275
  )
8836
9276
  ).returning();
8837
9277
  return result[0] ?? null;
@@ -8840,7 +9280,7 @@ var init_role_permissions_repository = __esm({
8840
9280
  * 역할의 모든 권한 매핑 삭제
8841
9281
  */
8842
9282
  async deleteByRoleId(roleId) {
8843
- const result = await this.db.delete(rolePermissions).where(eq17(rolePermissions.roleId, roleId)).returning();
9283
+ const result = await this.db.delete(rolePermissions).where(eq18(rolePermissions.roleId, roleId)).returning();
8844
9284
  return result.length;
8845
9285
  }
8846
9286
  /**
@@ -8861,19 +9301,19 @@ var init_role_permissions_repository = __esm({
8861
9301
  });
8862
9302
 
8863
9303
  // src/server/repositories/user-permissions.repository.ts
8864
- import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
8865
- import { eq as eq18, and as and16, or as or4, isNull as isNull12, isNotNull as isNotNull3, lt as lt6, gt as gt8 } from "drizzle-orm";
9304
+ import { BaseRepository as BaseRepository19 } from "@spfn/core/db";
9305
+ import { eq as eq19, and as and17, or as or5, isNull as isNull13, isNotNull as isNotNull3, lt as lt6, gt as gt9 } from "drizzle-orm";
8866
9306
  var UserPermissionsRepository, userPermissionsRepository;
8867
9307
  var init_user_permissions_repository = __esm({
8868
9308
  "src/server/repositories/user-permissions.repository.ts"() {
8869
9309
  "use strict";
8870
9310
  init_user_permissions();
8871
- UserPermissionsRepository = class extends BaseRepository18 {
9311
+ UserPermissionsRepository = class extends BaseRepository19 {
8872
9312
  /**
8873
9313
  * 사용자 ID로 모든 권한 오버라이드 조회
8874
9314
  */
8875
9315
  async findByUserId(userId) {
8876
- return this.readDb.select().from(userPermissions).where(eq18(userPermissions.userId, userId));
9316
+ return this.readDb.select().from(userPermissions).where(eq19(userPermissions.userId, userId));
8877
9317
  }
8878
9318
  /**
8879
9319
  * 사용자 ID로 유효한 권한 오버라이드만 조회
@@ -8882,11 +9322,11 @@ var init_user_permissions_repository = __esm({
8882
9322
  async findValidByUserId(userId) {
8883
9323
  const now = /* @__PURE__ */ new Date();
8884
9324
  return this.readDb.select().from(userPermissions).where(
8885
- and16(
8886
- eq18(userPermissions.userId, userId),
8887
- or4(
8888
- isNull12(userPermissions.expiresAt),
8889
- gt8(userPermissions.expiresAt, now)
9325
+ and17(
9326
+ eq19(userPermissions.userId, userId),
9327
+ or5(
9328
+ isNull13(userPermissions.expiresAt),
9329
+ gt9(userPermissions.expiresAt, now)
8890
9330
  )
8891
9331
  )
8892
9332
  );
@@ -8896,9 +9336,9 @@ var init_user_permissions_repository = __esm({
8896
9336
  */
8897
9337
  async findByUserIdAndPermissionId(userId, permissionId) {
8898
9338
  const result = await this.readDb.select().from(userPermissions).where(
8899
- and16(
8900
- eq18(userPermissions.userId, userId),
8901
- eq18(userPermissions.permissionId, permissionId)
9339
+ and17(
9340
+ eq19(userPermissions.userId, userId),
9341
+ eq19(userPermissions.permissionId, permissionId)
8902
9342
  )
8903
9343
  ).limit(1);
8904
9344
  return result[0] ?? null;
@@ -8912,8 +9352,8 @@ var init_user_permissions_repository = __esm({
8912
9352
  /**
8913
9353
  * 사용자 권한 오버라이드 업데이트
8914
9354
  */
8915
- async updateById(id26, data) {
8916
- const result = await this.db.update(userPermissions).set(data).where(eq18(userPermissions.id, id26)).returning();
9355
+ async updateById(id27, data) {
9356
+ const result = await this.db.update(userPermissions).set(data).where(eq19(userPermissions.id, id27)).returning();
8917
9357
  return result[0] ?? null;
8918
9358
  }
8919
9359
  /**
@@ -8921,9 +9361,9 @@ var init_user_permissions_repository = __esm({
8921
9361
  */
8922
9362
  async deleteByUserIdAndPermissionId(userId, permissionId) {
8923
9363
  const result = await this.db.delete(userPermissions).where(
8924
- and16(
8925
- eq18(userPermissions.userId, userId),
8926
- eq18(userPermissions.permissionId, permissionId)
9364
+ and17(
9365
+ eq19(userPermissions.userId, userId),
9366
+ eq19(userPermissions.permissionId, permissionId)
8927
9367
  )
8928
9368
  ).returning();
8929
9369
  return result[0] ?? null;
@@ -8932,7 +9372,7 @@ var init_user_permissions_repository = __esm({
8932
9372
  * 사용자의 모든 권한 오버라이드 삭제
8933
9373
  */
8934
9374
  async deleteByUserId(userId) {
8935
- const result = await this.db.delete(userPermissions).where(eq18(userPermissions.userId, userId)).returning();
9375
+ const result = await this.db.delete(userPermissions).where(eq19(userPermissions.userId, userId)).returning();
8936
9376
  return result.length;
8937
9377
  }
8938
9378
  /**
@@ -8941,7 +9381,7 @@ var init_user_permissions_repository = __esm({
8941
9381
  async deleteExpired() {
8942
9382
  const now = /* @__PURE__ */ new Date();
8943
9383
  const result = await this.db.delete(userPermissions).where(
8944
- and16(
9384
+ and17(
8945
9385
  isNotNull3(userPermissions.expiresAt),
8946
9386
  lt6(userPermissions.expiresAt, now)
8947
9387
  )
@@ -8954,33 +9394,33 @@ var init_user_permissions_repository = __esm({
8954
9394
  });
8955
9395
 
8956
9396
  // src/server/repositories/user-profiles.repository.ts
8957
- import { BaseRepository as BaseRepository19 } from "@spfn/core/db";
8958
- import { eq as eq19 } from "drizzle-orm";
9397
+ import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
9398
+ import { eq as eq20 } from "drizzle-orm";
8959
9399
  var UserProfilesRepository, userProfilesRepository;
8960
9400
  var init_user_profiles_repository = __esm({
8961
9401
  "src/server/repositories/user-profiles.repository.ts"() {
8962
9402
  "use strict";
8963
9403
  init_user_profiles();
8964
- UserProfilesRepository = class extends BaseRepository19 {
9404
+ UserProfilesRepository = class extends BaseRepository20 {
8965
9405
  /**
8966
9406
  * ID로 프로필 조회
8967
9407
  */
8968
- async findById(id26) {
8969
- const result = await this.readDb.select().from(userProfiles).where(eq19(userProfiles.id, id26)).limit(1);
9408
+ async findById(id27) {
9409
+ const result = await this.readDb.select().from(userProfiles).where(eq20(userProfiles.id, id27)).limit(1);
8970
9410
  return result[0] ?? null;
8971
9411
  }
8972
9412
  /**
8973
9413
  * User ID로 locale만 조회 (경량)
8974
9414
  */
8975
9415
  async findLocaleByUserId(userId) {
8976
- const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1);
9416
+ const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1);
8977
9417
  return result[0]?.locale || "en";
8978
9418
  }
8979
9419
  /**
8980
9420
  * User ID로 프로필 조회
8981
9421
  */
8982
9422
  async findByUserId(userId) {
8983
- const result = await this.readDb.select().from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1);
9423
+ const result = await this.readDb.select().from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1);
8984
9424
  return result[0] ?? null;
8985
9425
  }
8986
9426
  /**
@@ -8992,29 +9432,29 @@ var init_user_profiles_repository = __esm({
8992
9432
  /**
8993
9433
  * 프로필 업데이트 (by ID)
8994
9434
  */
8995
- async updateById(id26, data) {
8996
- const result = await this.db.update(userProfiles).set(data).where(eq19(userProfiles.id, id26)).returning();
9435
+ async updateById(id27, data) {
9436
+ const result = await this.db.update(userProfiles).set(data).where(eq20(userProfiles.id, id27)).returning();
8997
9437
  return result[0] ?? null;
8998
9438
  }
8999
9439
  /**
9000
9440
  * 프로필 업데이트 (by User ID)
9001
9441
  */
9002
9442
  async updateByUserId(userId, data) {
9003
- const result = await this.db.update(userProfiles).set(data).where(eq19(userProfiles.userId, userId)).returning();
9443
+ const result = await this.db.update(userProfiles).set(data).where(eq20(userProfiles.userId, userId)).returning();
9004
9444
  return result[0] ?? null;
9005
9445
  }
9006
9446
  /**
9007
9447
  * 프로필 삭제 (by ID)
9008
9448
  */
9009
- async deleteById(id26) {
9010
- const result = await this.db.delete(userProfiles).where(eq19(userProfiles.id, id26)).returning();
9449
+ async deleteById(id27) {
9450
+ const result = await this.db.delete(userProfiles).where(eq20(userProfiles.id, id27)).returning();
9011
9451
  return result[0] ?? null;
9012
9452
  }
9013
9453
  /**
9014
9454
  * 프로필 삭제 (by User ID)
9015
9455
  */
9016
9456
  async deleteByUserId(userId) {
9017
- const result = await this.db.delete(userProfiles).where(eq19(userProfiles.userId, userId)).returning();
9457
+ const result = await this.db.delete(userProfiles).where(eq20(userProfiles.userId, userId)).returning();
9018
9458
  return result[0] ?? null;
9019
9459
  }
9020
9460
  /**
@@ -9056,7 +9496,7 @@ var init_user_profiles_repository = __esm({
9056
9496
  metadata: userProfiles.metadata,
9057
9497
  createdAt: userProfiles.createdAt,
9058
9498
  updatedAt: userProfiles.updatedAt
9059
- }).from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
9499
+ }).from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
9060
9500
  if (!profile) {
9061
9501
  return null;
9062
9502
  }
@@ -9084,8 +9524,8 @@ var init_user_profiles_repository = __esm({
9084
9524
  });
9085
9525
 
9086
9526
  // src/server/repositories/invitations.repository.ts
9087
- import { eq as eq20, and as and17, lt as lt7, desc as desc3, sql as sql10 } from "drizzle-orm";
9088
- import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
9527
+ import { eq as eq21, and as and18, lt as lt7, desc as desc3, sql as sql11 } from "drizzle-orm";
9528
+ import { BaseRepository as BaseRepository21 } from "@spfn/core/db";
9089
9529
  var InvitationsRepository, invitationsRepository;
9090
9530
  var init_invitations_repository = __esm({
9091
9531
  "src/server/repositories/invitations.repository.ts"() {
@@ -9094,19 +9534,19 @@ var init_invitations_repository = __esm({
9094
9534
  init_roles();
9095
9535
  init_user_invitations();
9096
9536
  init_email();
9097
- InvitationsRepository = class extends BaseRepository20 {
9537
+ InvitationsRepository = class extends BaseRepository21 {
9098
9538
  /**
9099
9539
  * ID로 초대 조회
9100
9540
  */
9101
- async findById(id26) {
9102
- const result = await this.readDb.select().from(userInvitations).where(eq20(userInvitations.id, id26)).limit(1);
9541
+ async findById(id27) {
9542
+ const result = await this.readDb.select().from(userInvitations).where(eq21(userInvitations.id, id27)).limit(1);
9103
9543
  return result[0] ?? null;
9104
9544
  }
9105
9545
  /**
9106
9546
  * Token으로 초대 조회
9107
9547
  */
9108
9548
  async findByToken(token) {
9109
- const result = await this.readDb.select().from(userInvitations).where(eq20(userInvitations.token, token)).limit(1);
9549
+ const result = await this.readDb.select().from(userInvitations).where(eq21(userInvitations.token, token)).limit(1);
9110
9550
  return result[0] ?? null;
9111
9551
  }
9112
9552
  /**
@@ -9114,9 +9554,9 @@ var init_invitations_repository = __esm({
9114
9554
  */
9115
9555
  async findPendingByEmail(email) {
9116
9556
  const result = await this.readDb.select().from(userInvitations).where(
9117
- and17(
9118
- eq20(userInvitations.email, normalizeEmail(email)),
9119
- eq20(userInvitations.status, "pending")
9557
+ and18(
9558
+ eq21(userInvitations.email, normalizeEmail(email)),
9559
+ eq21(userInvitations.status, "pending")
9120
9560
  )
9121
9561
  ).limit(1);
9122
9562
  return result[0] ?? null;
@@ -9125,13 +9565,13 @@ var init_invitations_repository = __esm({
9125
9565
  * 초대자 ID로 모든 초대 조회
9126
9566
  */
9127
9567
  async findByInvitedBy(invitedBy) {
9128
- return this.readDb.select().from(userInvitations).where(eq20(userInvitations.invitedBy, invitedBy));
9568
+ return this.readDb.select().from(userInvitations).where(eq21(userInvitations.invitedBy, invitedBy));
9129
9569
  }
9130
9570
  /**
9131
9571
  * 상태별 초대 조회
9132
9572
  */
9133
9573
  async findByStatus(status) {
9134
- return this.readDb.select().from(userInvitations).where(eq20(userInvitations.status, status));
9574
+ return this.readDb.select().from(userInvitations).where(eq21(userInvitations.status, status));
9135
9575
  }
9136
9576
  /**
9137
9577
  * 초대 생성
@@ -9142,7 +9582,7 @@ var init_invitations_repository = __esm({
9142
9582
  /**
9143
9583
  * 초대 상태 업데이트
9144
9584
  */
9145
- async updateStatus(id26, status, timestamp2) {
9585
+ async updateStatus(id27, status, timestamp2) {
9146
9586
  const updates = {
9147
9587
  status
9148
9588
  };
@@ -9153,14 +9593,14 @@ var init_invitations_repository = __esm({
9153
9593
  updates.cancelledAt = timestamp2;
9154
9594
  }
9155
9595
  }
9156
- const result = await this.db.update(userInvitations).set(updates).where(eq20(userInvitations.id, id26)).returning();
9596
+ const result = await this.db.update(userInvitations).set(updates).where(eq21(userInvitations.id, id27)).returning();
9157
9597
  return result[0] ?? null;
9158
9598
  }
9159
9599
  /**
9160
9600
  * 초대 삭제
9161
9601
  */
9162
- async deleteById(id26) {
9163
- const result = await this.db.delete(userInvitations).where(eq20(userInvitations.id, id26)).returning();
9602
+ async deleteById(id27) {
9603
+ const result = await this.db.delete(userInvitations).where(eq21(userInvitations.id, id27)).returning();
9164
9604
  return result[0] ?? null;
9165
9605
  }
9166
9606
  /**
@@ -9169,8 +9609,8 @@ var init_invitations_repository = __esm({
9169
9609
  async updateExpiredInvitations() {
9170
9610
  const now = /* @__PURE__ */ new Date();
9171
9611
  const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
9172
- and17(
9173
- eq20(userInvitations.status, "pending"),
9612
+ and18(
9613
+ eq21(userInvitations.status, "pending"),
9174
9614
  lt7(userInvitations.expiresAt, now)
9175
9615
  )
9176
9616
  ).returning();
@@ -9202,7 +9642,7 @@ var init_invitations_repository = __esm({
9202
9642
  id: users.id,
9203
9643
  email: users.email
9204
9644
  }
9205
- }).from(userInvitations).innerJoin(roles, eq20(userInvitations.roleId, roles.id)).innerJoin(users, eq20(userInvitations.invitedBy, users.id)).where(eq20(userInvitations.token, token)).limit(1);
9645
+ }).from(userInvitations).innerJoin(roles, eq21(userInvitations.roleId, roles.id)).innerJoin(users, eq21(userInvitations.invitedBy, users.id)).where(eq21(userInvitations.token, token)).limit(1);
9206
9646
  return result[0] ?? null;
9207
9647
  }
9208
9648
  /**
@@ -9213,13 +9653,13 @@ var init_invitations_repository = __esm({
9213
9653
  const offset = (page - 1) * limit;
9214
9654
  const conditions = [];
9215
9655
  if (status) {
9216
- conditions.push(eq20(userInvitations.status, status));
9656
+ conditions.push(eq21(userInvitations.status, status));
9217
9657
  }
9218
9658
  if (invitedBy) {
9219
- conditions.push(eq20(userInvitations.invitedBy, invitedBy));
9659
+ conditions.push(eq21(userInvitations.invitedBy, invitedBy));
9220
9660
  }
9221
- const whereClause = conditions.length > 0 ? and17(...conditions) : void 0;
9222
- const countResult = await this.readDb.select({ count: sql10`count(*)` }).from(userInvitations).where(whereClause);
9661
+ const whereClause = conditions.length > 0 ? and18(...conditions) : void 0;
9662
+ const countResult = await this.readDb.select({ count: sql11`count(*)` }).from(userInvitations).where(whereClause);
9223
9663
  const total = Number(countResult[0]?.count || 0);
9224
9664
  const results = await this.readDb.select({
9225
9665
  id: userInvitations.id,
@@ -9243,7 +9683,7 @@ var init_invitations_repository = __esm({
9243
9683
  id: users.id,
9244
9684
  email: users.email
9245
9685
  }
9246
- }).from(userInvitations).innerJoin(roles, eq20(userInvitations.roleId, roles.id)).innerJoin(users, eq20(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc3(userInvitations.createdAt)).limit(limit).offset(offset);
9686
+ }).from(userInvitations).innerJoin(roles, eq21(userInvitations.roleId, roles.id)).innerJoin(users, eq21(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc3(userInvitations.createdAt)).limit(limit).offset(offset);
9247
9687
  return {
9248
9688
  invitations: results,
9249
9689
  total,
@@ -9255,31 +9695,31 @@ var init_invitations_repository = __esm({
9255
9695
  /**
9256
9696
  * 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
9257
9697
  */
9258
- async updateById(id26, data) {
9698
+ async updateById(id27, data) {
9259
9699
  const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
9260
- const result = await this.db.update(userInvitations).set(patch).where(eq20(userInvitations.id, id26)).returning();
9700
+ const result = await this.db.update(userInvitations).set(patch).where(eq21(userInvitations.id, id27)).returning();
9261
9701
  return result[0] ?? null;
9262
9702
  }
9263
9703
  /**
9264
9704
  * 초대 재전송 (status와 expiresAt 동시 업데이트)
9265
9705
  */
9266
- async resend(id26, newExpiresAt) {
9706
+ async resend(id27, newExpiresAt) {
9267
9707
  const result = await this.db.update(userInvitations).set({
9268
9708
  status: "pending",
9269
9709
  expiresAt: newExpiresAt
9270
- }).where(eq20(userInvitations.id, id26)).returning();
9710
+ }).where(eq21(userInvitations.id, id27)).returning();
9271
9711
  return result[0] ?? null;
9272
9712
  }
9273
9713
  /**
9274
9714
  * 초대 취소 (status, metadata 동시 업데이트)
9275
9715
  */
9276
- async cancel(id26, cancelledBy, reason, currentMetadata) {
9716
+ async cancel(id27, cancelledBy, reason, currentMetadata) {
9277
9717
  const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
9278
9718
  const result = await this.db.update(userInvitations).set({
9279
9719
  status: "cancelled",
9280
9720
  cancelledAt: /* @__PURE__ */ new Date(),
9281
9721
  metadata: newMetadata
9282
- }).where(eq20(userInvitations.id, id26)).returning();
9722
+ }).where(eq21(userInvitations.id, id27)).returning();
9283
9723
  return result[0] ?? null;
9284
9724
  }
9285
9725
  };
@@ -10119,15 +10559,15 @@ var init_token_cipher = __esm({
10119
10559
  });
10120
10560
 
10121
10561
  // src/server/repositories/social-accounts.repository.ts
10122
- import { eq as eq21, and as and18 } from "drizzle-orm";
10123
- import { BaseRepository as BaseRepository21 } from "@spfn/core/db";
10562
+ import { eq as eq22, and as and19 } from "drizzle-orm";
10563
+ import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
10124
10564
  var SocialAccountsRepository, socialAccountsRepository;
10125
10565
  var init_social_accounts_repository = __esm({
10126
10566
  "src/server/repositories/social-accounts.repository.ts"() {
10127
10567
  "use strict";
10128
10568
  init_entities();
10129
10569
  init_token_cipher();
10130
- SocialAccountsRepository = class extends BaseRepository21 {
10570
+ SocialAccountsRepository = class extends BaseRepository22 {
10131
10571
  /**
10132
10572
  * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
10133
10573
  *
@@ -10155,10 +10595,10 @@ var init_social_accounts_repository = __esm({
10155
10595
  if (refresh?.needsRotation) {
10156
10596
  heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
10157
10597
  }
10158
- await this.db.update(userSocialAccounts).set(heal).where(and18(
10159
- eq21(userSocialAccounts.id, account.id),
10160
- access?.needsRotation && account.accessToken !== null ? eq21(userSocialAccounts.accessToken, account.accessToken) : void 0,
10161
- refresh?.needsRotation && account.refreshToken !== null ? eq21(userSocialAccounts.refreshToken, account.refreshToken) : void 0
10598
+ await this.db.update(userSocialAccounts).set(heal).where(and19(
10599
+ eq22(userSocialAccounts.id, account.id),
10600
+ access?.needsRotation && account.accessToken !== null ? eq22(userSocialAccounts.accessToken, account.accessToken) : void 0,
10601
+ refresh?.needsRotation && account.refreshToken !== null ? eq22(userSocialAccounts.refreshToken, account.refreshToken) : void 0
10162
10602
  ));
10163
10603
  } catch {
10164
10604
  }
@@ -10175,9 +10615,9 @@ var init_social_accounts_repository = __esm({
10175
10615
  */
10176
10616
  async findByProviderAndProviderId(provider, providerUserId) {
10177
10617
  const result = await this.readDb.select().from(userSocialAccounts).where(
10178
- and18(
10179
- eq21(userSocialAccounts.provider, provider),
10180
- eq21(userSocialAccounts.providerUserId, providerUserId)
10618
+ and19(
10619
+ eq22(userSocialAccounts.provider, provider),
10620
+ eq22(userSocialAccounts.providerUserId, providerUserId)
10181
10621
  )
10182
10622
  ).limit(1);
10183
10623
  return this.decryptAccount(result[0] ?? null);
@@ -10187,7 +10627,7 @@ var init_social_accounts_repository = __esm({
10187
10627
  * Read replica 사용
10188
10628
  */
10189
10629
  async findByUserId(userId) {
10190
- const result = await this.readDb.select().from(userSocialAccounts).where(eq21(userSocialAccounts.userId, userId));
10630
+ const result = await this.readDb.select().from(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId));
10191
10631
  return Promise.all(result.map((account) => this.decryptAccount(account)));
10192
10632
  }
10193
10633
  /**
@@ -10196,9 +10636,9 @@ var init_social_accounts_repository = __esm({
10196
10636
  */
10197
10637
  async findByUserIdAndProvider(userId, provider) {
10198
10638
  const result = await this.readDb.select().from(userSocialAccounts).where(
10199
- and18(
10200
- eq21(userSocialAccounts.userId, userId),
10201
- eq21(userSocialAccounts.provider, provider)
10639
+ and19(
10640
+ eq22(userSocialAccounts.userId, userId),
10641
+ eq22(userSocialAccounts.provider, provider)
10202
10642
  )
10203
10643
  ).limit(1);
10204
10644
  return this.decryptAccount(result[0] ?? null);
@@ -10224,11 +10664,11 @@ var init_social_accounts_repository = __esm({
10224
10664
  * 토큰 정보 업데이트
10225
10665
  * Write primary 사용
10226
10666
  */
10227
- async updateTokens(id26, data) {
10667
+ async updateTokens(id27, data) {
10228
10668
  const accounts = await this.db.select({
10229
10669
  provider: userSocialAccounts.provider,
10230
10670
  providerUserId: userSocialAccounts.providerUserId
10231
- }).from(userSocialAccounts).where(eq21(userSocialAccounts.id, id26)).limit(1);
10671
+ }).from(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).limit(1);
10232
10672
  const account = accounts[0];
10233
10673
  if (!account) {
10234
10674
  return null;
@@ -10242,15 +10682,15 @@ var init_social_accounts_repository = __esm({
10242
10682
  ...data,
10243
10683
  accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
10244
10684
  refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
10245
- }).where(eq21(userSocialAccounts.id, id26)).returning();
10685
+ }).where(eq22(userSocialAccounts.id, id27)).returning();
10246
10686
  return this.decryptAccount(result[0] ?? null);
10247
10687
  }
10248
10688
  /**
10249
10689
  * 소셜 계정 삭제
10250
10690
  * Write primary 사용
10251
10691
  */
10252
- async deleteById(id26) {
10253
- const result = await this.db.delete(userSocialAccounts).where(eq21(userSocialAccounts.id, id26)).returning();
10692
+ async deleteById(id27) {
10693
+ const result = await this.db.delete(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).returning();
10254
10694
  return result[0] ?? null;
10255
10695
  }
10256
10696
  /**
@@ -10259,9 +10699,9 @@ var init_social_accounts_repository = __esm({
10259
10699
  */
10260
10700
  async deleteByUserIdAndProvider(userId, provider) {
10261
10701
  const result = await this.db.delete(userSocialAccounts).where(
10262
- and18(
10263
- eq21(userSocialAccounts.userId, userId),
10264
- eq21(userSocialAccounts.provider, provider)
10702
+ and19(
10703
+ eq22(userSocialAccounts.userId, userId),
10704
+ eq22(userSocialAccounts.provider, provider)
10265
10705
  )
10266
10706
  ).returning();
10267
10707
  return result[0] ?? null;
@@ -10274,7 +10714,7 @@ var init_social_accounts_repository = __esm({
10274
10714
  * Write primary 사용
10275
10715
  */
10276
10716
  async deleteAllByUserId(userId) {
10277
- const result = await this.db.delete(userSocialAccounts).where(eq21(userSocialAccounts.userId, userId)).returning();
10717
+ const result = await this.db.delete(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId)).returning();
10278
10718
  return result.length;
10279
10719
  }
10280
10720
  };
@@ -10283,19 +10723,19 @@ var init_social_accounts_repository = __esm({
10283
10723
  });
10284
10724
 
10285
10725
  // src/server/repositories/auth-metadata.repository.ts
10286
- import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
10287
- import { eq as eq22 } from "drizzle-orm";
10726
+ import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
10727
+ import { eq as eq23 } from "drizzle-orm";
10288
10728
  var AuthMetadataRepository, authMetadataRepository;
10289
10729
  var init_auth_metadata_repository = __esm({
10290
10730
  "src/server/repositories/auth-metadata.repository.ts"() {
10291
10731
  "use strict";
10292
10732
  init_auth_metadata();
10293
- AuthMetadataRepository = class extends BaseRepository22 {
10733
+ AuthMetadataRepository = class extends BaseRepository23 {
10294
10734
  /**
10295
10735
  * 키로 값 조회
10296
10736
  */
10297
10737
  async get(key) {
10298
- const result = await this.readDb.select().from(authMetadata).where(eq22(authMetadata.key, key)).limit(1);
10738
+ const result = await this.readDb.select().from(authMetadata).where(eq23(authMetadata.key, key)).limit(1);
10299
10739
  return result[0]?.value ?? null;
10300
10740
  }
10301
10741
  /**
@@ -10318,20 +10758,20 @@ var init_auth_metadata_repository = __esm({
10318
10758
  });
10319
10759
 
10320
10760
  // src/server/repositories/account-deletion-requests.repository.ts
10321
- import { eq as eq23, and as and19, lte as lte2 } from "drizzle-orm";
10322
- import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
10761
+ import { eq as eq24, and as and20, lte as lte2 } from "drizzle-orm";
10762
+ import { BaseRepository as BaseRepository24 } from "@spfn/core/db";
10323
10763
  var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
10324
10764
  var init_account_deletion_requests_repository = __esm({
10325
10765
  "src/server/repositories/account-deletion-requests.repository.ts"() {
10326
10766
  "use strict";
10327
10767
  init_account_deletion_requests();
10328
- AccountDeletionRequestsRepository = class extends BaseRepository23 {
10768
+ AccountDeletionRequestsRepository = class extends BaseRepository24 {
10329
10769
  /**
10330
10770
  * ID로 요청 조회
10331
10771
  * Read replica 사용
10332
10772
  */
10333
- async findById(id26) {
10334
- const result = await this.readDb.select().from(accountDeletionRequests).where(eq23(accountDeletionRequests.id, id26)).limit(1);
10773
+ async findById(id27) {
10774
+ const result = await this.readDb.select().from(accountDeletionRequests).where(eq24(accountDeletionRequests.id, id27)).limit(1);
10335
10775
  return result[0] ?? null;
10336
10776
  }
10337
10777
  /**
@@ -10340,9 +10780,9 @@ var init_account_deletion_requests_repository = __esm({
10340
10780
  */
10341
10781
  async findPendingByUserId(userId) {
10342
10782
  const result = await this.readDb.select().from(accountDeletionRequests).where(
10343
- and19(
10344
- eq23(accountDeletionRequests.userId, userId),
10345
- eq23(accountDeletionRequests.status, "pending")
10783
+ and20(
10784
+ eq24(accountDeletionRequests.userId, userId),
10785
+ eq24(accountDeletionRequests.status, "pending")
10346
10786
  )
10347
10787
  ).limit(1);
10348
10788
  return result[0] ?? null;
@@ -10356,9 +10796,9 @@ var init_account_deletion_requests_repository = __esm({
10356
10796
  */
10357
10797
  async findPendingByUserIdOnPrimary(userId) {
10358
10798
  const result = await this.db.select().from(accountDeletionRequests).where(
10359
- and19(
10360
- eq23(accountDeletionRequests.userId, userId),
10361
- eq23(accountDeletionRequests.status, "pending")
10799
+ and20(
10800
+ eq24(accountDeletionRequests.userId, userId),
10801
+ eq24(accountDeletionRequests.status, "pending")
10362
10802
  )
10363
10803
  ).limit(1);
10364
10804
  return result[0] ?? null;
@@ -10369,8 +10809,8 @@ var init_account_deletion_requests_repository = __esm({
10369
10809
  */
10370
10810
  async findDueForPurge(now) {
10371
10811
  return this.readDb.select().from(accountDeletionRequests).where(
10372
- and19(
10373
- eq23(accountDeletionRequests.status, "pending"),
10812
+ and20(
10813
+ eq24(accountDeletionRequests.status, "pending"),
10374
10814
  lte2(accountDeletionRequests.purgeScheduledAt, now)
10375
10815
  )
10376
10816
  );
@@ -10390,14 +10830,14 @@ var init_account_deletion_requests_repository = __esm({
10390
10830
  * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
10391
10831
  * Write primary 사용
10392
10832
  */
10393
- async markCancelled(id26) {
10833
+ async markCancelled(id27) {
10394
10834
  const result = await this.db.update(accountDeletionRequests).set({
10395
10835
  status: "cancelled",
10396
10836
  cancelledAt: /* @__PURE__ */ new Date()
10397
10837
  }).where(
10398
- and19(
10399
- eq23(accountDeletionRequests.id, id26),
10400
- eq23(accountDeletionRequests.status, "pending")
10838
+ and20(
10839
+ eq24(accountDeletionRequests.id, id27),
10840
+ eq24(accountDeletionRequests.status, "pending")
10401
10841
  )
10402
10842
  ).returning();
10403
10843
  return result[0] ?? null;
@@ -10412,15 +10852,15 @@ var init_account_deletion_requests_repository = __esm({
10412
10852
  * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
10413
10853
  * Write primary 사용
10414
10854
  */
10415
- async markCompleted(id26, purgeStrategy) {
10855
+ async markCompleted(id27, purgeStrategy) {
10416
10856
  const result = await this.db.update(accountDeletionRequests).set({
10417
10857
  status: "completed",
10418
10858
  completedAt: /* @__PURE__ */ new Date(),
10419
10859
  purgeStrategy
10420
10860
  }).where(
10421
- and19(
10422
- eq23(accountDeletionRequests.id, id26),
10423
- eq23(accountDeletionRequests.status, "pending")
10861
+ and20(
10862
+ eq24(accountDeletionRequests.id, id27),
10863
+ eq24(accountDeletionRequests.status, "pending")
10424
10864
  )
10425
10865
  ).returning();
10426
10866
  return result[0] ?? null;
@@ -10431,14 +10871,14 @@ var init_account_deletion_requests_repository = __esm({
10431
10871
  });
10432
10872
 
10433
10873
  // src/server/repositories/ops-tokens.repository.ts
10434
- import { and as and20, desc as desc4, eq as eq24, isNull as isNull13 } from "drizzle-orm";
10435
- import { BaseRepository as BaseRepository24 } from "@spfn/core/db";
10874
+ import { and as and21, desc as desc4, eq as eq25, isNull as isNull14 } from "drizzle-orm";
10875
+ import { BaseRepository as BaseRepository25 } from "@spfn/core/db";
10436
10876
  var OpsTokensRepository, opsTokensRepository;
10437
10877
  var init_ops_tokens_repository = __esm({
10438
10878
  "src/server/repositories/ops-tokens.repository.ts"() {
10439
10879
  "use strict";
10440
10880
  init_ops_tokens();
10441
- OpsTokensRepository = class extends BaseRepository24 {
10881
+ OpsTokensRepository = class extends BaseRepository25 {
10442
10882
  /**
10443
10883
  * Lookup by the secret's hash — the verification path.
10444
10884
  *
@@ -10448,7 +10888,7 @@ var init_ops_tokens_repository = __esm({
10448
10888
  * and revocation is documented as taking effect immediately.
10449
10889
  */
10450
10890
  async findByTokenHash(tokenHash) {
10451
- const result = await this.db.select().from(opsTokens).where(eq24(opsTokens.tokenHash, tokenHash)).limit(1);
10891
+ const result = await this.db.select().from(opsTokens).where(eq25(opsTokens.tokenHash, tokenHash)).limit(1);
10452
10892
  return result[0] ?? null;
10453
10893
  }
10454
10894
  async create(data) {
@@ -10463,13 +10903,13 @@ var init_ops_tokens_repository = __esm({
10463
10903
  * token is already revoked — the first revocation's timestamp is never
10464
10904
  * overwritten.
10465
10905
  */
10466
- async revokeById(id26) {
10467
- const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and20(eq24(opsTokens.id, id26), isNull13(opsTokens.revokedAt))).returning();
10906
+ async revokeById(id27) {
10907
+ const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and21(eq25(opsTokens.id, id27), isNull14(opsTokens.revokedAt))).returning();
10468
10908
  return result[0] ?? null;
10469
10909
  }
10470
10910
  /** Fire-and-forget from the verification path. */
10471
- async updateLastUsedById(id26) {
10472
- await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq24(opsTokens.id, id26));
10911
+ async updateLastUsedById(id27) {
10912
+ await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq25(opsTokens.id, id27));
10473
10913
  }
10474
10914
  };
10475
10915
  opsTokensRepository = new OpsTokensRepository();
@@ -10477,15 +10917,15 @@ var init_ops_tokens_repository = __esm({
10477
10917
  });
10478
10918
 
10479
10919
  // src/server/repositories/oauth2-clients.repository.ts
10480
- import { and as and21, eq as eq25, gt as gt9, lt as lt8, sql as sql11 } from "drizzle-orm";
10481
- import { BaseRepository as BaseRepository25, runInTransaction } from "@spfn/core/db";
10920
+ import { and as and22, eq as eq26, gt as gt10, lt as lt8, sql as sql12 } from "drizzle-orm";
10921
+ import { BaseRepository as BaseRepository26, runInTransaction } from "@spfn/core/db";
10482
10922
  var OAuth2ClientsRepository, oauth2ClientsRepository;
10483
10923
  var init_oauth2_clients_repository = __esm({
10484
10924
  "src/server/repositories/oauth2-clients.repository.ts"() {
10485
10925
  "use strict";
10486
10926
  init_oauth2_clients();
10487
10927
  init_oauth2_grants();
10488
- OAuth2ClientsRepository = class extends BaseRepository25 {
10928
+ OAuth2ClientsRepository = class extends BaseRepository26 {
10489
10929
  async create(data) {
10490
10930
  const result = await this.db.insert(oauth2Clients).values(data).returning();
10491
10931
  return result[0];
@@ -10499,12 +10939,12 @@ var init_oauth2_clients_repository = __esm({
10499
10939
  * at the one moment a CLI is being connected.
10500
10940
  */
10501
10941
  async findByClientId(clientId) {
10502
- const result = await this.db.select().from(oauth2Clients).where(eq25(oauth2Clients.clientId, clientId)).limit(1);
10942
+ const result = await this.db.select().from(oauth2Clients).where(eq26(oauth2Clients.clientId, clientId)).limit(1);
10503
10943
  return result[0] ?? null;
10504
10944
  }
10505
10945
  /** Fire-and-forget from the token-issuing path. */
10506
- async updateLastUsedById(id26) {
10507
- await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq25(oauth2Clients.id, id26));
10946
+ async updateLastUsedById(id27) {
10947
+ await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq26(oauth2Clients.id, id27));
10508
10948
  }
10509
10949
  /**
10510
10950
  * Register a client unless this address is already at its standing cap.
@@ -10526,7 +10966,7 @@ var init_oauth2_clients_repository = __esm({
10526
10966
  */
10527
10967
  async createWithinStandingCap(data, limit) {
10528
10968
  return await runInTransaction(async () => {
10529
- await this.db.execute(sql11`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
10969
+ await this.db.execute(sql12`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
10530
10970
  const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
10531
10971
  return standing < limit.max ? await this.create(data) : null;
10532
10972
  });
@@ -10548,11 +10988,11 @@ var init_oauth2_clients_repository = __esm({
10548
10988
  * count exists to see.
10549
10989
  */
10550
10990
  async countRecentUngrantedByIp(ip, windowMs) {
10551
- const result = await this.db.select({ count: sql11`count(*)::int` }).from(oauth2Clients).where(
10552
- and21(
10553
- eq25(oauth2Clients.createdIp, ip),
10554
- gt9(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
10555
- sql11`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
10991
+ const result = await this.db.select({ count: sql12`count(*)::int` }).from(oauth2Clients).where(
10992
+ and22(
10993
+ eq26(oauth2Clients.createdIp, ip),
10994
+ gt10(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
10995
+ sql12`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
10556
10996
  )
10557
10997
  );
10558
10998
  return result[0]?.count ?? 0;
@@ -10569,9 +11009,9 @@ var init_oauth2_clients_repository = __esm({
10569
11009
  */
10570
11010
  async deleteStaleUngranted(before) {
10571
11011
  const deleted = await this.db.delete(oauth2Clients).where(
10572
- and21(
11012
+ and22(
10573
11013
  lt8(oauth2Clients.createdAt, before),
10574
- sql11`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
11014
+ sql12`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
10575
11015
  )
10576
11016
  ).returning({ id: oauth2Clients.id });
10577
11017
  return deleted.length;
@@ -10582,8 +11022,8 @@ var init_oauth2_clients_repository = __esm({
10582
11022
  });
10583
11023
 
10584
11024
  // src/server/repositories/oauth2-grants.repository.ts
10585
- import { and as and22, desc as desc5, eq as eq26, inArray as inArray4, isNull as isNull14, sql as sql12 } from "drizzle-orm";
10586
- import { BaseRepository as BaseRepository26 } from "@spfn/core/db";
11025
+ import { and as and23, desc as desc5, eq as eq27, inArray as inArray5, isNull as isNull15, sql as sql13 } from "drizzle-orm";
11026
+ import { BaseRepository as BaseRepository27 } from "@spfn/core/db";
10587
11027
  var OAuth2GrantsRepository, oauth2GrantsRepository;
10588
11028
  var init_oauth2_grants_repository = __esm({
10589
11029
  "src/server/repositories/oauth2-grants.repository.ts"() {
@@ -10591,7 +11031,7 @@ var init_oauth2_grants_repository = __esm({
10591
11031
  init_oauth2_grants();
10592
11032
  init_oauth2_clients();
10593
11033
  init_oauth2_tokens();
10594
- OAuth2GrantsRepository = class extends BaseRepository26 {
11034
+ OAuth2GrantsRepository = class extends BaseRepository27 {
10595
11035
  /**
10596
11036
  * Record a consent, or refresh the scopes of the one already there.
10597
11037
  *
@@ -10618,13 +11058,13 @@ var init_oauth2_grants_repository = __esm({
10618
11058
  *
10619
11059
  * Read primary: revocation is documented as taking effect immediately.
10620
11060
  */
10621
- async findWithClientById(id26) {
10622
- const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq26(oauth2Grants.client, oauth2Clients.id)).where(eq26(oauth2Grants.id, id26)).limit(1);
11061
+ async findWithClientById(id27) {
11062
+ const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq27(oauth2Grants.client, oauth2Clients.id)).where(eq27(oauth2Grants.id, id27)).limit(1);
10623
11063
  return result[0] ?? null;
10624
11064
  }
10625
11065
  /** What the user's "connected apps" screen lists. Read replica. */
10626
11066
  async listActiveByUserId(userId) {
10627
- return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq26(oauth2Grants.client, oauth2Clients.id)).where(and22(eq26(oauth2Grants.user, userId), isNull14(oauth2Grants.revokedAt))).orderBy(desc5(oauth2Grants.createdAt));
11067
+ return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq27(oauth2Grants.client, oauth2Clients.id)).where(and23(eq27(oauth2Grants.user, userId), isNull15(oauth2Grants.revokedAt))).orderBy(desc5(oauth2Grants.createdAt));
10628
11068
  }
10629
11069
  /**
10630
11070
  * Revoke one live grant belonging to one user.
@@ -10637,19 +11077,19 @@ var init_oauth2_grants_repository = __esm({
10637
11077
  * @returns the revoked row, or null if there was no live grant of that id
10638
11078
  * for that user
10639
11079
  */
10640
- async revokeByIdForUser(id26, userId) {
11080
+ async revokeByIdForUser(id27, userId) {
10641
11081
  const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
10642
- and22(
10643
- eq26(oauth2Grants.id, id26),
10644
- eq26(oauth2Grants.user, userId),
10645
- isNull14(oauth2Grants.revokedAt)
11082
+ and23(
11083
+ eq27(oauth2Grants.id, id27),
11084
+ eq27(oauth2Grants.user, userId),
11085
+ isNull15(oauth2Grants.revokedAt)
10646
11086
  )
10647
11087
  ).returning();
10648
11088
  return result[0] ?? null;
10649
11089
  }
10650
11090
  /** Revoke one grant by id, whoever it belongs to — the replay detections. */
10651
- async revokeById(id26) {
10652
- const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and22(eq26(oauth2Grants.id, id26), isNull14(oauth2Grants.revokedAt))).returning();
11091
+ async revokeById(id27) {
11092
+ const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and23(eq27(oauth2Grants.id, id27), isNull15(oauth2Grants.revokedAt))).returning();
10653
11093
  return result[0] ?? null;
10654
11094
  }
10655
11095
  /**
@@ -10659,7 +11099,7 @@ var init_oauth2_grants_repository = __esm({
10659
11099
  * @returns the grant ids this call revoked
10660
11100
  */
10661
11101
  async revokeAllActiveByUserId(userId) {
10662
- const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and22(eq26(oauth2Grants.user, userId), isNull14(oauth2Grants.revokedAt))).returning({ id: oauth2Grants.id });
11102
+ const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and23(eq27(oauth2Grants.user, userId), isNull15(oauth2Grants.revokedAt))).returning({ id: oauth2Grants.id });
10663
11103
  return revoked.map((row) => row.id);
10664
11104
  }
10665
11105
  /**
@@ -10675,7 +11115,7 @@ var init_oauth2_grants_repository = __esm({
10675
11115
  if (grantIds.length === 0) {
10676
11116
  return 0;
10677
11117
  }
10678
- const revoked = await this.db.update(oauth2Tokens).set({ revokedAt: sql12`now()` }).where(and22(inArray4(oauth2Tokens.grant, grantIds), isNull14(oauth2Tokens.revokedAt))).returning({ id: oauth2Tokens.id });
11118
+ const revoked = await this.db.update(oauth2Tokens).set({ revokedAt: sql13`now()` }).where(and23(inArray5(oauth2Tokens.grant, grantIds), isNull15(oauth2Tokens.revokedAt))).returning({ id: oauth2Tokens.id });
10679
11119
  return revoked.length;
10680
11120
  }
10681
11121
  };
@@ -10684,14 +11124,14 @@ var init_oauth2_grants_repository = __esm({
10684
11124
  });
10685
11125
 
10686
11126
  // src/server/repositories/oauth2-authorization-codes.repository.ts
10687
- import { and as and23, eq as eq27, gt as gt10, isNull as isNull15, sql as sql13 } from "drizzle-orm";
10688
- import { BaseRepository as BaseRepository27 } from "@spfn/core/db";
11127
+ import { and as and24, eq as eq28, gt as gt11, isNull as isNull16, sql as sql14 } from "drizzle-orm";
11128
+ import { BaseRepository as BaseRepository28 } from "@spfn/core/db";
10689
11129
  var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
10690
11130
  var init_oauth2_authorization_codes_repository = __esm({
10691
11131
  "src/server/repositories/oauth2-authorization-codes.repository.ts"() {
10692
11132
  "use strict";
10693
11133
  init_oauth2_authorization_codes();
10694
- OAuth2AuthorizationCodesRepository = class extends BaseRepository27 {
11134
+ OAuth2AuthorizationCodesRepository = class extends BaseRepository28 {
10695
11135
  async create(data) {
10696
11136
  const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
10697
11137
  return result[0];
@@ -10703,11 +11143,11 @@ var init_oauth2_authorization_codes_repository = __esm({
10703
11143
  * spent, or past its 60 seconds
10704
11144
  */
10705
11145
  async consume(codeHash) {
10706
- const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql13`now()` }).where(
10707
- and23(
10708
- eq27(oauth2AuthorizationCodes.codeHash, codeHash),
10709
- isNull15(oauth2AuthorizationCodes.usedAt),
10710
- gt10(oauth2AuthorizationCodes.expiresAt, sql13`now()`)
11146
+ const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql14`now()` }).where(
11147
+ and24(
11148
+ eq28(oauth2AuthorizationCodes.codeHash, codeHash),
11149
+ isNull16(oauth2AuthorizationCodes.usedAt),
11150
+ gt11(oauth2AuthorizationCodes.expiresAt, sql14`now()`)
10711
11151
  )
10712
11152
  ).returning();
10713
11153
  return result[0] ?? null;
@@ -10718,7 +11158,7 @@ var init_oauth2_authorization_codes_repository = __esm({
10718
11158
  * would be indistinguishable from a code that never existed.
10719
11159
  */
10720
11160
  async findByCodeHash(codeHash) {
10721
- const result = await this.db.select().from(oauth2AuthorizationCodes).where(eq27(oauth2AuthorizationCodes.codeHash, codeHash)).limit(1);
11161
+ const result = await this.db.select().from(oauth2AuthorizationCodes).where(eq28(oauth2AuthorizationCodes.codeHash, codeHash)).limit(1);
10722
11162
  return result[0] ?? null;
10723
11163
  }
10724
11164
  };
@@ -10727,14 +11167,14 @@ var init_oauth2_authorization_codes_repository = __esm({
10727
11167
  });
10728
11168
 
10729
11169
  // src/server/repositories/oauth2-tokens.repository.ts
10730
- import { and as and24, eq as eq28, gt as gt11, isNull as isNull16, sql as sql14 } from "drizzle-orm";
10731
- import { BaseRepository as BaseRepository28 } from "@spfn/core/db";
11170
+ import { and as and25, eq as eq29, gt as gt12, isNull as isNull17, sql as sql15 } from "drizzle-orm";
11171
+ import { BaseRepository as BaseRepository29 } from "@spfn/core/db";
10732
11172
  var OAuth2TokensRepository, oauth2TokensRepository;
10733
11173
  var init_oauth2_tokens_repository = __esm({
10734
11174
  "src/server/repositories/oauth2-tokens.repository.ts"() {
10735
11175
  "use strict";
10736
11176
  init_oauth2_tokens();
10737
- OAuth2TokensRepository = class extends BaseRepository28 {
11177
+ OAuth2TokensRepository = class extends BaseRepository29 {
10738
11178
  async create(data) {
10739
11179
  const result = await this.db.insert(oauth2Tokens).values(data).returning();
10740
11180
  return result[0];
@@ -10748,7 +11188,7 @@ var init_oauth2_tokens_repository = __esm({
10748
11188
  * Unfiltered, so the service can tell revoked from expired from unknown.
10749
11189
  */
10750
11190
  async findByTokenHash(tokenHash) {
10751
- const result = await this.db.select().from(oauth2Tokens).where(eq28(oauth2Tokens.tokenHash, tokenHash)).limit(1);
11191
+ const result = await this.db.select().from(oauth2Tokens).where(eq29(oauth2Tokens.tokenHash, tokenHash)).limit(1);
10752
11192
  return result[0] ?? null;
10753
11193
  }
10754
11194
  /**
@@ -10758,13 +11198,13 @@ var init_oauth2_tokens_repository = __esm({
10758
11198
  * rotated, revoked, or expired
10759
11199
  */
10760
11200
  async rotate(tokenHash) {
10761
- const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql14`now()` }).where(
10762
- and24(
10763
- eq28(oauth2Tokens.tokenHash, tokenHash),
10764
- eq28(oauth2Tokens.kind, "refresh"),
10765
- isNull16(oauth2Tokens.replacedAt),
10766
- isNull16(oauth2Tokens.revokedAt),
10767
- gt11(oauth2Tokens.expiresAt, sql14`now()`)
11201
+ const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql15`now()` }).where(
11202
+ and25(
11203
+ eq29(oauth2Tokens.tokenHash, tokenHash),
11204
+ eq29(oauth2Tokens.kind, "refresh"),
11205
+ isNull17(oauth2Tokens.replacedAt),
11206
+ isNull17(oauth2Tokens.revokedAt),
11207
+ gt12(oauth2Tokens.expiresAt, sql15`now()`)
10768
11208
  )
10769
11209
  ).returning();
10770
11210
  return result[0] ?? null;
@@ -10777,12 +11217,12 @@ var init_oauth2_tokens_repository = __esm({
10777
11217
  * about whether the value it presented ever existed.
10778
11218
  */
10779
11219
  async revokeByTokenHash(tokenHash) {
10780
- const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and24(eq28(oauth2Tokens.tokenHash, tokenHash), isNull16(oauth2Tokens.revokedAt))).returning();
11220
+ const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and25(eq29(oauth2Tokens.tokenHash, tokenHash), isNull17(oauth2Tokens.revokedAt))).returning();
10781
11221
  return result[0] ?? null;
10782
11222
  }
10783
11223
  /** Fire-and-forget from the verification path, as ops tokens do. */
10784
- async updateLastUsedById(id26) {
10785
- await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq28(oauth2Tokens.id, id26));
11224
+ async updateLastUsedById(id27) {
11225
+ await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq29(oauth2Tokens.id, id27));
10786
11226
  }
10787
11227
  };
10788
11228
  oauth2TokensRepository = new OAuth2TokensRepository();
@@ -10807,6 +11247,7 @@ var init_repositories = __esm({
10807
11247
  init_mfa_challenges_repository();
10808
11248
  init_mfa_enrolment_repository();
10809
11249
  init_device_authorizations_repository();
11250
+ init_device_links_repository();
10810
11251
  init_roles_repository();
10811
11252
  init_permissions_repository();
10812
11253
  init_role_permissions_repository();
@@ -10912,7 +11353,7 @@ async function removePermissionFromRole(roleId, permissionId) {
10912
11353
  }
10913
11354
  async function setRolePermissions(roleId, permissionIds) {
10914
11355
  const roleIdNum = Number(roleId);
10915
- const permissionIdNums = permissionIds.map((id26) => Number(id26));
11356
+ const permissionIdNums = permissionIds.map((id27) => Number(id27));
10916
11357
  await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
10917
11358
  }
10918
11359
  async function getAllRoles(includeInactive = false) {
@@ -10932,7 +11373,7 @@ async function getRolePermissions(roleId) {
10932
11373
  }
10933
11374
  const permissionIds = mappings.map((m) => m.permissionId);
10934
11375
  const perms = await Promise.all(
10935
- permissionIds.map((id26) => permissionsRepository.findById(id26))
11376
+ permissionIds.map((id27) => permissionsRepository.findById(id27))
10936
11377
  );
10937
11378
  return perms.filter((p) => p !== null).map((p) => p.name);
10938
11379
  }
@@ -11196,8 +11637,8 @@ async function listOAuth2GrantsService(userId) {
11196
11637
  lastUsedAtMillis: client.lastUsedAt?.getTime()
11197
11638
  }));
11198
11639
  }
11199
- async function revokeOAuth2GrantService(id26, userId) {
11200
- const revoked = await oauth2GrantsRepository.revokeByIdForUser(id26, userId);
11640
+ async function revokeOAuth2GrantService(id27, userId) {
11641
+ const revoked = await oauth2GrantsRepository.revokeByIdForUser(id27, userId);
11201
11642
  if (!revoked) {
11202
11643
  throw new OAuth2GrantNotFoundError();
11203
11644
  }
@@ -11587,7 +12028,7 @@ import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/a
11587
12028
  import { ValidationError as ValidationError3 } from "@spfn/core/errors";
11588
12029
 
11589
12030
  // src/server/services/device-registration.service.ts
11590
- import { onAfterCommit } from "@spfn/core/db";
12031
+ import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
11591
12032
 
11592
12033
  // src/server/events/index.ts
11593
12034
  init_esm();
@@ -11623,6 +12064,7 @@ var DeviceRegistrationChannelSchema = Type.Union([
11623
12064
  Type.Literal("oauth"),
11624
12065
  Type.Literal("oauth-native"),
11625
12066
  Type.Literal("device-code"),
12067
+ Type.Literal("device-link"),
11626
12068
  Type.Literal("password-reset"),
11627
12069
  Type.Literal("passkey"),
11628
12070
  Type.Literal("renewal")
@@ -11739,7 +12181,7 @@ init_repositories();
11739
12181
  var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
11740
12182
  async function emitDeviceRegistered(row, channel) {
11741
12183
  const mfaEnrolled = await mfaEnrolmentRepository.isEnrolled(row.userId);
11742
- onAfterCommit(() => authDeviceRegisteredEvent.emit({
12184
+ onAfterCommit2(() => authDeviceRegisteredEvent.emit({
11743
12185
  userId: String(row.userId),
11744
12186
  keyId: row.keyId,
11745
12187
  algorithm: row.algorithm,
@@ -11757,7 +12199,7 @@ async function emitDeviceRegistered(row, channel) {
11757
12199
  // src/server/services/mfa.service.ts
11758
12200
  init_logger();
11759
12201
  init_config();
11760
- import { onAfterCommit as onAfterCommit2, runInTransaction as runInTransaction2 } from "@spfn/core/db";
12202
+ import { onAfterCommit as onAfterCommit3, runInTransaction as runInTransaction2 } from "@spfn/core/db";
11761
12203
  import { ValidationError as ValidationError2 } from "@spfn/core/errors";
11762
12204
  import {
11763
12205
  MfaAlreadyEnrolledError,
@@ -11855,16 +12297,16 @@ function encodeBase32(bytes) {
11855
12297
  }
11856
12298
  return bits > 0 ? output + BASE32_ALPHABET[value << 5 - bits & 31] : output;
11857
12299
  }
11858
- function decodeBase32(text27) {
12300
+ function decodeBase32(text28) {
11859
12301
  let bits = 0;
11860
12302
  let value = 0;
11861
12303
  const bytes = [];
11862
- for (const character of text27.replace(/=+$/, "").toUpperCase()) {
11863
- const index21 = BASE32_ALPHABET.indexOf(character);
11864
- if (index21 < 0) {
12304
+ for (const character of text28.replace(/=+$/, "").toUpperCase()) {
12305
+ const index22 = BASE32_ALPHABET.indexOf(character);
12306
+ if (index22 < 0) {
11865
12307
  throw new Error("Value is not RFC 4648 base32");
11866
12308
  }
11867
- value = value << 5 | index21;
12309
+ value = value << 5 | index22;
11868
12310
  bits += 5;
11869
12311
  if (bits >= 8) {
11870
12312
  bytes.push(value >>> bits - 8 & 255);
@@ -12154,9 +12596,9 @@ function presentedChallenge(clientDataJSON) {
12154
12596
  const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
12155
12597
  return typeof decoded?.challenge === "string" ? decoded.challenge : "";
12156
12598
  }
12157
- function parseJson(text27) {
12599
+ function parseJson(text28) {
12158
12600
  try {
12159
- return JSON.parse(text27);
12601
+ return JSON.parse(text28);
12160
12602
  } catch {
12161
12603
  return null;
12162
12604
  }
@@ -12230,7 +12672,7 @@ async function releaseDeferredAnnouncements(row, key) {
12230
12672
  return;
12231
12673
  }
12232
12674
  const mfaEnrolled = await mfaEnrolledForUser(row.userId);
12233
- onAfterCommit2(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
12675
+ onAfterCommit3(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
12234
12676
  }
12235
12677
  async function verifyMfaChallengeService(params) {
12236
12678
  const challengeHash = hashCredential(params.challenge);
@@ -12324,7 +12766,7 @@ async function countConfirmFailure(userId) {
12324
12766
  function readSecret(secretEnc, userId) {
12325
12767
  const { value, needsRotation } = decryptMfaSecret(secretEnc, userId);
12326
12768
  if (needsRotation) {
12327
- onAfterCommit2(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
12769
+ onAfterCommit3(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
12328
12770
  }
12329
12771
  return value;
12330
12772
  }
@@ -12623,6 +13065,7 @@ async function revokeAllKeysService(params) {
12623
13065
  }
12624
13066
  const revoked = !includeCurrent && currentKeyId ? await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason) : await keysRepository.revokeAllActiveByUserId(userId, reason);
12625
13067
  await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
13068
+ await deviceLinksRepository.expireAllLiveByUserId(userId);
12626
13069
  await revokeAllOAuth2GrantsForUser(userId);
12627
13070
  return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
12628
13071
  }
@@ -12633,7 +13076,7 @@ init_key_policy();
12633
13076
  // src/server/services/account-deletion.service.ts
12634
13077
  init_repositories();
12635
13078
  import { ValidationError as ValidationError4, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
12636
- import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit3 } from "@spfn/core/db";
13079
+ import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
12637
13080
  import { sendEmail as sendEmail3 } from "@spfn/notification/server";
12638
13081
  import {
12639
13082
  InvalidCredentialsError,
@@ -12708,8 +13151,8 @@ async function verifyReauthCredential(user, params) {
12708
13151
  throw new VerificationTokenTargetMismatchError();
12709
13152
  }
12710
13153
  }
12711
- async function sendDeletionEmail(to, subject, text27) {
12712
- const result = await sendEmail3({ to, subject, text: text27 });
13154
+ async function sendDeletionEmail(to, subject, text28) {
13155
+ const result = await sendEmail3({ to, subject, text: text28 });
12713
13156
  if (!result.success) {
12714
13157
  authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
12715
13158
  }
@@ -12761,12 +13204,12 @@ async function requestAccountDeletionService(userId, params) {
12761
13204
  if (requestedBy === "self") {
12762
13205
  await verifyReauthCredential(user, { password, verificationToken });
12763
13206
  }
12764
- const config4 = getDeletionConfig();
13207
+ const config5 = getDeletionConfig();
12765
13208
  const wantsImmediate = immediate === true;
12766
- if (wantsImmediate && requestedBy === "self" && !config4.allowSelfImmediate) {
13209
+ if (wantsImmediate && requestedBy === "self" && !config5.allowSelfImmediate) {
12767
13210
  throw new ImmediateDeletionNotAllowedError();
12768
13211
  }
12769
- const gracePeriodDays = wantsImmediate ? 0 : config4.gracePeriodDays;
13212
+ const gracePeriodDays = wantsImmediate ? 0 : config5.gracePeriodDays;
12770
13213
  const requestedAt = /* @__PURE__ */ new Date();
12771
13214
  const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
12772
13215
  await usersRepository.updateById(user.id, { status: "pending_deletion" });
@@ -12789,14 +13232,15 @@ async function requestAccountDeletionService(userId, params) {
12789
13232
  }
12790
13233
  await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
12791
13234
  await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
13235
+ await deviceLinksRepository.expireAllLiveByUserId(user.id);
12792
13236
  await revokeAllOAuth2GrantsForUser(user.id);
12793
- onAfterCommit3(() => authDeletionRequestedEvent.emit({
13237
+ onAfterCommit4(() => authDeletionRequestedEvent.emit({
12794
13238
  userId: String(user.id),
12795
13239
  userPublicId: user.publicId,
12796
13240
  purgeScheduledAt: purgeScheduledAt.toISOString(),
12797
13241
  requestedBy
12798
13242
  }));
12799
- onAfterCommit3(() => notifyDeletionRequested(user, purgeScheduledAt));
13243
+ onAfterCommit4(() => notifyDeletionRequested(user, purgeScheduledAt));
12800
13244
  if (gracePeriodDays === 0) {
12801
13245
  await purgePendingRequest(request);
12802
13246
  }
@@ -12828,11 +13272,11 @@ async function cancelAccountDeletionService(params) {
12828
13272
  throw new DeletionNotRequestedError();
12829
13273
  }
12830
13274
  await usersRepository.reactivateFromPendingDeletion(user.id);
12831
- onAfterCommit3(() => authDeletionCancelledEvent.emit({
13275
+ onAfterCommit4(() => authDeletionCancelledEvent.emit({
12832
13276
  userId: String(user.id),
12833
13277
  userPublicId: user.publicId
12834
13278
  }));
12835
- onAfterCommit3(() => notifyDeletionCancelled(user));
13279
+ onAfterCommit4(() => notifyDeletionCancelled(user));
12836
13280
  return { userId: String(user.id) };
12837
13281
  }
12838
13282
  async function anonymizeUser(user) {
@@ -12873,10 +13317,10 @@ async function purgePendingRequest(request) {
12873
13317
  if (!precheckUser || precheckUser.status !== "pending_deletion") {
12874
13318
  return { outcome: "skipped" };
12875
13319
  }
12876
- const config4 = getDeletionConfig();
12877
- if (config4.onBeforePurge) {
13320
+ const config5 = getDeletionConfig();
13321
+ if (config5.onBeforePurge) {
12878
13322
  try {
12879
- await config4.onBeforePurge({
13323
+ await config5.onBeforePurge({
12880
13324
  id: precheckUser.id,
12881
13325
  publicId: precheckUser.publicId,
12882
13326
  email: precheckUser.email,
@@ -12890,7 +13334,7 @@ async function purgePendingRequest(request) {
12890
13334
  return { outcome: "skipped" };
12891
13335
  }
12892
13336
  }
12893
- const purgeStrategy = config4.purgeStrategy;
13337
+ const purgeStrategy = config5.purgeStrategy;
12894
13338
  let purgedUser = null;
12895
13339
  await runInTransaction3(async () => {
12896
13340
  const user = await usersRepository.findById(userId);
@@ -12919,9 +13363,9 @@ async function purgePendingRequest(request) {
12919
13363
  }
12920
13364
  const { email, publicId } = purgedUser;
12921
13365
  if (email) {
12922
- onAfterCommit3(() => notifyPurgeFinal(email));
13366
+ onAfterCommit4(() => notifyPurgeFinal(email));
12923
13367
  }
12924
- onAfterCommit3(() => authDeletionCompletedEvent.emit({
13368
+ onAfterCommit4(() => authDeletionCompletedEvent.emit({
12925
13369
  userPublicId: publicId,
12926
13370
  purgeStrategy
12927
13371
  }));
@@ -13135,6 +13579,7 @@ async function changePasswordService(params) {
13135
13579
  const newPasswordHash = await hashPassword(newPassword);
13136
13580
  await usersRepository.updatePassword(userId, newPasswordHash, true);
13137
13581
  await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
13582
+ await deviceLinksRepository.expireAllLiveByUserId(userId);
13138
13583
  await revokeAllOAuth2GrantsForUser(userId);
13139
13584
  await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
13140
13585
  }
@@ -13275,7 +13720,7 @@ init_repositories();
13275
13720
  import { env as env11 } from "@spfn/auth/config";
13276
13721
  import { PasswordResetLinkError, PasswordResetSessionError } from "@spfn/auth/errors";
13277
13722
  import { ValidationError as ValidationError6 } from "@spfn/core/errors";
13278
- import { onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
13723
+ import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
13279
13724
  init_key_policy();
13280
13725
  async function activeUserOf(userId) {
13281
13726
  const user = await usersRepository.findByIdOnPrimary(userId);
@@ -13336,7 +13781,9 @@ async function replaceCredentials(row, user, params) {
13336
13781
  // for accounts created before the register flows stamped it.
13337
13782
  ...user.emailVerifiedAt ? {} : { emailVerifiedAt: /* @__PURE__ */ new Date() }
13338
13783
  });
13784
+ await keysRepository.lockActiveByUserId(user.id);
13339
13785
  await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
13786
+ await deviceLinksRepository.expireAllLiveByUserId(user.id);
13340
13787
  await revokeAllOAuth2GrantsForUser(user.id);
13341
13788
  await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
13342
13789
  const registered = await registerPublicKeyService({
@@ -13376,7 +13823,7 @@ async function completePasswordResetService(params) {
13376
13823
  throw new PasswordResetSessionError();
13377
13824
  }
13378
13825
  const registered = await replaceCredentials(row, user, params);
13379
- onAfterCommit4(() => authPasswordResetEvent.emit({
13826
+ onAfterCommit5(() => authPasswordResetEvent.emit({
13380
13827
  userId: String(user.id),
13381
13828
  email: row.email
13382
13829
  }));
@@ -13468,21 +13915,26 @@ import {
13468
13915
  DeviceAuthDeniedError,
13469
13916
  InvalidKeyFingerprintError as InvalidKeyFingerprintError2
13470
13917
  } from "@spfn/auth/errors";
13471
- import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
13918
+ import { onAfterCommit as onAfterCommit6 } from "@spfn/core/db";
13919
+ import { getShutdownManager } from "@spfn/core/server";
13472
13920
 
13473
13921
  // src/server/lib/device-auth-config.ts
13474
13922
  var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
13475
13923
  var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
13924
+ var DEFAULT_DEVICE_AUTH_MAX_WAIT_MS = 20 * 1e3;
13476
13925
  var config2 = {
13477
13926
  ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
13478
- intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
13927
+ intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS,
13928
+ maxWaitMs: DEFAULT_DEVICE_AUTH_MAX_WAIT_MS
13479
13929
  };
13480
13930
  function configureDeviceAuth(options) {
13481
13931
  const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
13482
13932
  const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
13933
+ const maxWaitMs = options?.maxWaitMs ?? DEFAULT_DEVICE_AUTH_MAX_WAIT_MS;
13483
13934
  assertWholeMillis("ttlMs", ttlMs);
13484
13935
  assertWholeMillis("intervalMs", intervalMs);
13485
- config2 = { ttlMs, intervalMs };
13936
+ assertWholeMillis("maxWaitMs", maxWaitMs);
13937
+ config2 = { ttlMs, intervalMs, maxWaitMs };
13486
13938
  }
13487
13939
  function assertWholeMillis(name, value) {
13488
13940
  if (!Number.isInteger(value) || value <= 0) {
@@ -13495,6 +13947,9 @@ function getDeviceAuthConfig() {
13495
13947
  return config2;
13496
13948
  }
13497
13949
 
13950
+ // src/server/services/device-auth.service.ts
13951
+ init_device_auth_waiters();
13952
+
13498
13953
  // src/server/lib/device-code.ts
13499
13954
  import { createHash, randomBytes, randomInt } from "crypto";
13500
13955
  var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
@@ -13524,6 +13979,8 @@ function hashDeviceCode(deviceCode) {
13524
13979
  // src/server/services/device-auth.service.ts
13525
13980
  init_key_policy();
13526
13981
  var USER_CODE_ATTEMPTS = 3;
13982
+ var WAIT_RECHECK_MS = 1e3;
13983
+ var MAX_WAITERS_PER_RECORD = 3;
13527
13984
  function assertActionable(record) {
13528
13985
  if (!record || record.status === "consumed") {
13529
13986
  throw new DeviceAuthNotFoundError();
@@ -13623,6 +14080,37 @@ async function denyDeviceAuthService(params) {
13623
14080
  );
13624
14081
  }
13625
14082
  }
14083
+ async function waitForDeviceAuthAnswerService(params) {
14084
+ const requested = Math.min(params.waitMillis, getDeviceAuthConfig().maxWaitMs);
14085
+ const deviceCodeHash = hashDeviceCode(params.deviceCode);
14086
+ const record = requested > 0 ? await readWaitable(deviceCodeHash) : null;
14087
+ if (!record || waitingOnDeviceAuth(record.id) >= MAX_WAITERS_PER_RECORD) {
14088
+ return 0;
14089
+ }
14090
+ const startedAt = Date.now();
14091
+ const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
14092
+ await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
14093
+ return Date.now() - startedAt;
14094
+ }
14095
+ async function waitUntil(id27, deviceCodeHash, deadline, signal) {
14096
+ while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
14097
+ const remaining = deadline - Date.now();
14098
+ if (remaining <= 0) {
14099
+ return;
14100
+ }
14101
+ await waitForDeviceAuthAnswer(id27, Math.min(remaining, WAIT_RECHECK_MS), signal);
14102
+ if (!await readWaitable(deviceCodeHash)) {
14103
+ return;
14104
+ }
14105
+ }
14106
+ }
14107
+ async function readWaitable(deviceCodeHash) {
14108
+ const record = await deviceAuthorizationsRepository.findByDeviceCodeHashOnPrimary(deviceCodeHash).catch(() => null);
14109
+ return isWaitable(record) ? record : null;
14110
+ }
14111
+ function isWaitable(record) {
14112
+ return record?.status === "pending" && record.expiresAt.getTime() > Date.now();
14113
+ }
13626
14114
  async function pollDeviceAuthService(params) {
13627
14115
  const deviceCodeHash = hashDeviceCode(params.deviceCode);
13628
14116
  const record = assertActionable(
@@ -13632,7 +14120,10 @@ async function pollDeviceAuthService(params) {
13632
14120
  throw new DeviceAuthDeniedError();
13633
14121
  }
13634
14122
  if (record.status === "pending") {
13635
- return { status: "pending", intervalMillis: getDeviceAuthConfig().intervalMs };
14123
+ return {
14124
+ status: "pending",
14125
+ intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
14126
+ };
13636
14127
  }
13637
14128
  const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
13638
14129
  if (!consumed) {
@@ -13642,15 +14133,25 @@ async function pollDeviceAuthService(params) {
13642
14133
  () => new DeviceAuthNotFoundError()
13643
14134
  );
13644
14135
  }
13645
- return { status: "approved", ...await completeDeviceLogin(consumed, params) };
13646
- }
13647
- async function completeDeviceLogin(record, provenance) {
13648
- if (record.userId === null) {
14136
+ if (consumed.userId === null) {
13649
14137
  throw new DeviceAuthNotFoundError();
13650
14138
  }
13651
- const user = await usersRepository.findById(record.userId);
14139
+ return {
14140
+ status: "approved",
14141
+ ...await completeDeviceLogin({
14142
+ userId: consumed.userId,
14143
+ key: consumed,
14144
+ channel: "device-code",
14145
+ provenance: params,
14146
+ missingAccount: () => new DeviceAuthNotFoundError()
14147
+ })
14148
+ };
14149
+ }
14150
+ async function completeDeviceLogin(params) {
14151
+ const { key, provenance } = params;
14152
+ const user = await usersRepository.findById(params.userId);
13652
14153
  if (!user) {
13653
- throw new DeviceAuthNotFoundError();
14154
+ throw params.missingAccount();
13654
14155
  }
13655
14156
  if (user.status !== "active") {
13656
14157
  if (user.status === "pending_deletion") {
@@ -13663,22 +14164,22 @@ async function completeDeviceLogin(record, provenance) {
13663
14164
  }
13664
14165
  const registered = await registerPublicKeyService({
13665
14166
  userId: user.id,
13666
- keyId: record.keyId,
13667
- publicKey: record.publicKey,
13668
- fingerprint: record.fingerprint,
13669
- algorithm: record.algorithm,
13670
- deviceName: record.deviceName ?? void 0,
13671
- platform: record.platform ?? void 0,
13672
- channel: "device-code",
14167
+ keyId: key.keyId,
14168
+ publicKey: key.publicKey,
14169
+ fingerprint: key.fingerprint,
14170
+ algorithm: key.algorithm,
14171
+ deviceName: key.deviceName ?? void 0,
14172
+ platform: key.platform ?? void 0,
14173
+ channel: params.channel,
13673
14174
  ip: provenance.ip,
13674
14175
  userAgent: provenance.userAgent,
13675
14176
  binding: decideKeyBinding(user.sessionBinding, provenance.webProxy)
13676
14177
  });
13677
14178
  await updateLastLoginService(user.id);
13678
14179
  const result = {
13679
- // A device-code approval is itself a second factor: the owner read the
13680
- // code on a device that is already signed in and said yes, so this
13681
- // channel never steps up (#95).
14180
+ // An approval from a device that is already signed in is itself a
14181
+ // second factor — the owner read the code there and said yes — so
14182
+ // neither channel ever steps up (#95).
13682
14183
  mfaRequired: false,
13683
14184
  userId: String(user.id),
13684
14185
  publicId: user.publicId,
@@ -13688,7 +14189,7 @@ async function completeDeviceLogin(record, provenance) {
13688
14189
  ...loginBindingFields(registeredBinding(registered))
13689
14190
  };
13690
14191
  const mfaEnrolled = await mfaEnrolledForUser(user.id);
13691
- onAfterCommit5(() => authLoginEvent.emit({
14192
+ onAfterCommit6(() => authLoginEvent.emit({
13692
14193
  userId: String(user.id),
13693
14194
  provider: "device",
13694
14195
  email: result.email,
@@ -13698,10 +14199,324 @@ async function completeDeviceLogin(record, provenance) {
13698
14199
  return result;
13699
14200
  }
13700
14201
 
14202
+ // src/server/services/device-link.service.ts
14203
+ init_repositories();
14204
+ import { randomUUID } from "crypto";
14205
+ import {
14206
+ DeviceLinkAlreadyHandledError,
14207
+ DeviceLinkDeniedError,
14208
+ DeviceLinkExpiredError,
14209
+ DeviceLinkNotFoundError,
14210
+ DeviceLinkNotRedeemedError,
14211
+ DeviceLinkWrongMatchError,
14212
+ InvalidKeyFingerprintError as InvalidKeyFingerprintError3
14213
+ } from "@spfn/auth/errors";
14214
+ import { getShutdownManager as getShutdownManager2 } from "@spfn/core/server";
14215
+
14216
+ // src/server/lib/device-link-config.ts
14217
+ var DEFAULT_DEVICE_LINK_TTL_MS = 5 * 60 * 1e3;
14218
+ var config3 = {
14219
+ ttlMs: DEFAULT_DEVICE_LINK_TTL_MS
14220
+ };
14221
+ function configureDeviceLink(options) {
14222
+ const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_LINK_TTL_MS;
14223
+ if (!Number.isInteger(ttlMs) || ttlMs <= 0) {
14224
+ throw new Error(`deviceLink.ttlMs must be a positive whole number of milliseconds, received ${ttlMs}.`);
14225
+ }
14226
+ config3 = { ttlMs };
14227
+ }
14228
+ function getDeviceLinkConfig() {
14229
+ return config3;
14230
+ }
14231
+
14232
+ // src/server/lib/device-link-match.ts
14233
+ import { randomInt as randomInt2 } from "crypto";
14234
+ var MATCH_MIN = 10;
14235
+ var MATCH_MAX_EXCLUSIVE = 100;
14236
+ var DEVICE_LINK_CHOICE_COUNT = 3;
14237
+ function generateMatchNumber() {
14238
+ return randomInt2(MATCH_MIN, MATCH_MAX_EXCLUSIVE);
14239
+ }
14240
+ function generateChoices(matchNumber) {
14241
+ const choices = /* @__PURE__ */ new Set([matchNumber]);
14242
+ while (choices.size < DEVICE_LINK_CHOICE_COUNT) {
14243
+ choices.add(generateMatchNumber());
14244
+ }
14245
+ return shuffle([...choices]);
14246
+ }
14247
+ function shuffle(values) {
14248
+ for (let index22 = values.length - 1; index22 > 0; index22--) {
14249
+ const other = randomInt2(index22 + 1);
14250
+ [values[index22], values[other]] = [values[other], values[index22]];
14251
+ }
14252
+ return values;
14253
+ }
14254
+
14255
+ // src/server/services/device-link.service.ts
14256
+ init_device_link_waiters();
14257
+ var USER_CODE_ATTEMPTS2 = 3;
14258
+ var WAIT_RECHECK_MS2 = 1e3;
14259
+ var MAX_WAITERS_PER_RECORD2 = 3;
14260
+ function isDead(record) {
14261
+ return record.status === "expired" || record.expiresAt.getTime() <= Date.now() || !record.issuerKeyLive;
14262
+ }
14263
+ function assertReachable(record) {
14264
+ if (!record || record.status === "consumed") {
14265
+ throw new DeviceLinkNotFoundError();
14266
+ }
14267
+ if (isDead(record)) {
14268
+ throw new DeviceLinkExpiredError();
14269
+ }
14270
+ return record;
14271
+ }
14272
+ function assertIssuedBy(record, issuer) {
14273
+ if (!record || record.issuerUserId !== issuer.userId || record.issuerKeyId !== issuer.keyId) {
14274
+ throw new DeviceLinkNotFoundError();
14275
+ }
14276
+ if (isDead(record)) {
14277
+ throw new DeviceLinkExpiredError();
14278
+ }
14279
+ return record;
14280
+ }
14281
+ function refuseMissedTransition2(record, from, moved) {
14282
+ if (!record || record.status === "consumed") {
14283
+ throw new DeviceLinkNotFoundError();
14284
+ }
14285
+ if (record.status === from || isDead(record)) {
14286
+ throw new DeviceLinkExpiredError();
14287
+ }
14288
+ throw moved();
14289
+ }
14290
+ function assertAwaitingDecision(record) {
14291
+ if (record.status === "issued") {
14292
+ throw new DeviceLinkNotRedeemedError();
14293
+ }
14294
+ if (record.status !== "redeemed") {
14295
+ throw new DeviceLinkAlreadyHandledError();
14296
+ }
14297
+ }
14298
+ function describeLink(record) {
14299
+ return {
14300
+ status: record.status,
14301
+ expiresAtMillis: record.expiresAt.getTime(),
14302
+ ...describeDevice2(record),
14303
+ ...record.status === "redeemed" && record.choices ? { choices: record.choices } : {}
14304
+ };
14305
+ }
14306
+ function describeDevice2(record) {
14307
+ if (!record.redeemedAt || !record.fingerprint) {
14308
+ return {};
14309
+ }
14310
+ return {
14311
+ deviceName: record.deviceName ?? void 0,
14312
+ platform: record.platform ?? void 0,
14313
+ fingerprintPrefix: record.fingerprint.slice(0, KEY_FINGERPRINT_PREFIX_LENGTH),
14314
+ redeemedAtMillis: record.redeemedAt.getTime()
14315
+ };
14316
+ }
14317
+ async function issueDeviceLinkService(issuer) {
14318
+ await deviceLinksRepository.lockIssuerKey(issuer.keyId);
14319
+ await deviceLinksRepository.expireLiveByIssuerKey(issuer.keyId);
14320
+ const expiresAt = new Date(Date.now() + getDeviceLinkConfig().ttlMs);
14321
+ for (let attempt = 0; attempt < USER_CODE_ATTEMPTS2; attempt++) {
14322
+ const record = await deviceLinksRepository.create({
14323
+ linkId: randomUUID(),
14324
+ userCode: generateUserCode(),
14325
+ issuerUserId: issuer.userId,
14326
+ issuerKeyId: issuer.keyId,
14327
+ expiresAt
14328
+ });
14329
+ if (record) {
14330
+ return {
14331
+ linkId: record.linkId,
14332
+ userCode: formatUserCode(record.userCode),
14333
+ expiresAtMillis: expiresAt.getTime()
14334
+ };
14335
+ }
14336
+ }
14337
+ throw new Error(
14338
+ `Could not allocate a unique device link code in ${USER_CODE_ATTEMPTS2} attempts. Check the code generator and the user_code unique index.`
14339
+ );
14340
+ }
14341
+ async function redeemDeviceLinkService(params) {
14342
+ if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
14343
+ throw new InvalidKeyFingerprintError3();
14344
+ }
14345
+ const algorithm = params.algorithm ?? DEFAULT_KEY_ALGORITHM;
14346
+ assertKeyMatchesAlgorithm(params.publicKey, algorithm);
14347
+ const userCode = normalizeUserCode(params.userCode);
14348
+ const record = assertReachable(await deviceLinksRepository.findByUserCode(userCode));
14349
+ if (record.status !== "issued") {
14350
+ throw new DeviceLinkNotFoundError();
14351
+ }
14352
+ const deviceCode = generateDeviceCode();
14353
+ const matchNumber = generateMatchNumber();
14354
+ const redeemed = await deviceLinksRepository.redeem(record.id, {
14355
+ deviceCodeHash: hashDeviceCode(deviceCode),
14356
+ publicKey: params.publicKey,
14357
+ keyId: params.keyId,
14358
+ fingerprint: params.fingerprint,
14359
+ algorithm,
14360
+ deviceName: params.deviceName,
14361
+ platform: params.platform,
14362
+ matchNumber,
14363
+ choices: generateChoices(matchNumber)
14364
+ });
14365
+ if (!redeemed) {
14366
+ refuseMissedTransition2(
14367
+ await deviceLinksRepository.findByUserCode(userCode),
14368
+ "issued",
14369
+ () => new DeviceLinkNotFoundError()
14370
+ );
14371
+ }
14372
+ return {
14373
+ deviceCode,
14374
+ matchNumber,
14375
+ expiresAtMillis: redeemed.expiresAt.getTime(),
14376
+ intervalMillis: getDeviceAuthConfig().intervalMs
14377
+ };
14378
+ }
14379
+ async function getDeviceLinkStatusService(params) {
14380
+ return describeLink(assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer));
14381
+ }
14382
+ async function confirmDeviceLinkService(params) {
14383
+ const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
14384
+ assertAwaitingDecision(record);
14385
+ if (params.choice !== record.matchNumber) {
14386
+ await refuse(record);
14387
+ throw new DeviceLinkWrongMatchError();
14388
+ }
14389
+ const approved = await deviceLinksRepository.approve(record.id);
14390
+ if (!approved) {
14391
+ refuseMissedTransition2(
14392
+ await deviceLinksRepository.findByLinkId(params.linkId),
14393
+ "redeemed",
14394
+ () => new DeviceLinkAlreadyHandledError()
14395
+ );
14396
+ }
14397
+ return describeLink(approved);
14398
+ }
14399
+ async function denyDeviceLinkService(params) {
14400
+ const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
14401
+ assertAwaitingDecision(record);
14402
+ return describeLink(await refuse(record));
14403
+ }
14404
+ async function refuse(record) {
14405
+ const denied = await deviceLinksRepository.deny(record.id);
14406
+ if (!denied) {
14407
+ refuseMissedTransition2(
14408
+ await deviceLinksRepository.findByLinkId(record.linkId),
14409
+ "redeemed",
14410
+ () => new DeviceLinkAlreadyHandledError()
14411
+ );
14412
+ }
14413
+ return denied;
14414
+ }
14415
+ async function cancelDeviceLinkService(params) {
14416
+ const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
14417
+ if (record.status !== "issued" && record.status !== "redeemed") {
14418
+ throw new DeviceLinkAlreadyHandledError();
14419
+ }
14420
+ const cancelled = await deviceLinksRepository.cancel(record.id);
14421
+ if (!cancelled) {
14422
+ refuseMissedTransition2(
14423
+ await deviceLinksRepository.findByLinkId(params.linkId),
14424
+ record.status,
14425
+ () => new DeviceLinkAlreadyHandledError()
14426
+ );
14427
+ }
14428
+ return describeLink(cancelled);
14429
+ }
14430
+ async function pollDeviceLinkService(params) {
14431
+ const deviceCodeHash = hashDeviceCode(params.deviceCode);
14432
+ const record = assertReachable(await deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash));
14433
+ if (record.status === "denied") {
14434
+ throw new DeviceLinkDeniedError();
14435
+ }
14436
+ if (record.status === "redeemed") {
14437
+ return {
14438
+ status: "pending",
14439
+ intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
14440
+ };
14441
+ }
14442
+ const consumed = await deviceLinksRepository.consumeApproved(deviceCodeHash);
14443
+ if (!consumed) {
14444
+ refuseMissedTransition2(
14445
+ await deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash),
14446
+ "approved",
14447
+ () => new DeviceLinkNotFoundError()
14448
+ );
14449
+ }
14450
+ return {
14451
+ status: "approved",
14452
+ ...await completeDeviceLogin({
14453
+ userId: consumed.issuerUserId,
14454
+ key: parkedKey(consumed),
14455
+ channel: "device-link",
14456
+ provenance: params,
14457
+ missingAccount: () => new DeviceLinkNotFoundError()
14458
+ })
14459
+ };
14460
+ }
14461
+ function parkedKey(link) {
14462
+ const { keyId, publicKey, fingerprint, algorithm } = link;
14463
+ if (!keyId || !publicKey || !fingerprint || !algorithm) {
14464
+ throw new DeviceLinkNotFoundError();
14465
+ }
14466
+ return { keyId, publicKey, fingerprint, algorithm, deviceName: link.deviceName, platform: link.platform };
14467
+ }
14468
+ async function waitForDeviceLinkStatusService(params) {
14469
+ return holdWhileUnmoved(
14470
+ () => deviceLinksRepository.findByLinkId(params.linkId),
14471
+ (record) => isIssuedBy(record, params.issuer) && (record.status === "issued" || record.status === "approved"),
14472
+ params.waitMillis,
14473
+ params.signal
14474
+ );
14475
+ }
14476
+ async function waitForDeviceLinkAnswerService(params) {
14477
+ const deviceCodeHash = hashDeviceCode(params.deviceCode);
14478
+ return holdWhileUnmoved(
14479
+ () => deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash),
14480
+ (record) => record.status === "redeemed",
14481
+ params.waitMillis,
14482
+ params.signal
14483
+ );
14484
+ }
14485
+ function isIssuedBy(record, issuer) {
14486
+ return record.issuerUserId === issuer.userId && record.issuerKeyId === issuer.keyId;
14487
+ }
14488
+ async function holdWhileUnmoved(read, waitable, waitMillis, signal) {
14489
+ const requested = Math.min(waitMillis, getDeviceAuthConfig().maxWaitMs);
14490
+ const record = requested > 0 ? await read().catch(() => null) : null;
14491
+ if (!record || isDead(record) || !waitable(record) || waitingOnDeviceLink(record.id) >= MAX_WAITERS_PER_RECORD2) {
14492
+ return 0;
14493
+ }
14494
+ const startedAt = Date.now();
14495
+ const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
14496
+ const unmoved = async () => {
14497
+ const current = await read().catch(() => null);
14498
+ return current !== null && !isDead(current) && current.status === record.status;
14499
+ };
14500
+ await holdDeviceLinkWait(record.id, () => waitUntil2(record.id, deadline, unmoved, signal));
14501
+ return Date.now() - startedAt;
14502
+ }
14503
+ async function waitUntil2(id27, deadline, unmoved, signal) {
14504
+ while (!signal?.aborted && !getShutdownManager2().isShuttingDown()) {
14505
+ const remaining = deadline - Date.now();
14506
+ if (remaining <= 0) {
14507
+ return;
14508
+ }
14509
+ await waitForDeviceLinkMove(id27, Math.min(remaining, WAIT_RECHECK_MS2), signal);
14510
+ if (!await unmoved()) {
14511
+ return;
14512
+ }
14513
+ }
14514
+ }
14515
+
13701
14516
  // src/server/services/passkey.service.ts
13702
14517
  init_logger();
13703
14518
  init_config();
13704
- import { onAfterCommit as onAfterCommit6, runInTransaction as runInTransaction4 } from "@spfn/core/db";
14519
+ import { onAfterCommit as onAfterCommit7, runInTransaction as runInTransaction4 } from "@spfn/core/db";
13705
14520
  import { ValidationError as ValidationError8 } from "@spfn/core/errors";
13706
14521
  import {
13707
14522
  AccountDisabledError as AccountDisabledError3,
@@ -13819,7 +14634,7 @@ async function finishPasskeyEnrollmentService(params) {
13819
14634
  aaguid: verified.aaguid,
13820
14635
  label: params.label ?? null
13821
14636
  });
13822
- onAfterCommit6(() => passkeyEnrolledEvent.emit({
14637
+ onAfterCommit7(() => passkeyEnrolledEvent.emit({
13823
14638
  userId: String(params.userId),
13824
14639
  passkeyId: String(row.id),
13825
14640
  label: row.label ?? void 0
@@ -13919,7 +14734,7 @@ async function startSession(user, params) {
13919
14734
  ...loginBindingFields(registeredBinding(registered))
13920
14735
  };
13921
14736
  const mfaEnrolled = await mfaEnrolledForUser(user.id);
13922
- onAfterCommit6(() => authLoginEvent.emit({
14737
+ onAfterCommit7(() => authLoginEvent.emit({
13923
14738
  userId: String(user.id),
13924
14739
  provider: "passkey",
13925
14740
  email: result.email,
@@ -14002,7 +14817,7 @@ async function revokePasskeyService(params) {
14002
14817
  if (!revoked) {
14003
14818
  throw new PasskeyNotFoundError2();
14004
14819
  }
14005
- onAfterCommit6(() => passkeyRevokedEvent.emit({
14820
+ onAfterCommit7(() => passkeyRevokedEvent.emit({
14006
14821
  userId: String(params.userId),
14007
14822
  passkeyId: String(revoked.id),
14008
14823
  reason: "user"
@@ -14085,51 +14900,51 @@ async function initializeAuth(options = {}) {
14085
14900
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
14086
14901
  }
14087
14902
  async function syncRoles(configs, existingByName) {
14088
- for (const config4 of configs) {
14089
- const existing = existingByName.get(config4.name);
14903
+ for (const config5 of configs) {
14904
+ const existing = existingByName.get(config5.name);
14090
14905
  if (!existing) {
14091
14906
  await rolesRepository.create({
14092
- name: config4.name,
14093
- displayName: config4.displayName,
14094
- description: config4.description || null,
14095
- priority: config4.priority ?? 10,
14096
- isSystem: config4.isSystem ?? false,
14097
- isBuiltin: config4.isBuiltin ?? false,
14907
+ name: config5.name,
14908
+ displayName: config5.displayName,
14909
+ description: config5.description || null,
14910
+ priority: config5.priority ?? 10,
14911
+ isSystem: config5.isSystem ?? false,
14912
+ isBuiltin: config5.isBuiltin ?? false,
14098
14913
  isActive: true
14099
14914
  });
14100
- authLogger.service.info(` \u2705 Created role: ${config4.name}`);
14915
+ authLogger.service.info(` \u2705 Created role: ${config5.name}`);
14101
14916
  } else {
14102
14917
  const updateData = {
14103
- displayName: config4.displayName,
14104
- description: config4.description || null
14918
+ displayName: config5.displayName,
14919
+ description: config5.description || null
14105
14920
  };
14106
14921
  if (!existing.isBuiltin) {
14107
- updateData.priority = config4.priority ?? existing.priority;
14922
+ updateData.priority = config5.priority ?? existing.priority;
14108
14923
  }
14109
14924
  await rolesRepository.updateById(existing.id, updateData);
14110
14925
  }
14111
14926
  }
14112
14927
  }
14113
14928
  async function syncPermissions(configs, existingByName) {
14114
- for (const config4 of configs) {
14115
- const existing = existingByName.get(config4.name);
14929
+ for (const config5 of configs) {
14930
+ const existing = existingByName.get(config5.name);
14116
14931
  if (!existing) {
14117
14932
  await permissionsRepository.create({
14118
- name: config4.name,
14119
- displayName: config4.displayName,
14120
- description: config4.description || null,
14121
- category: config4.category || null,
14122
- isSystem: config4.isSystem ?? false,
14123
- isBuiltin: config4.isBuiltin ?? false,
14933
+ name: config5.name,
14934
+ displayName: config5.displayName,
14935
+ description: config5.description || null,
14936
+ category: config5.category || null,
14937
+ isSystem: config5.isSystem ?? false,
14938
+ isBuiltin: config5.isBuiltin ?? false,
14124
14939
  isActive: true,
14125
14940
  metadata: null
14126
14941
  });
14127
- authLogger.service.info(` \u2705 Created permission: ${config4.name}`);
14942
+ authLogger.service.info(` \u2705 Created permission: ${config5.name}`);
14128
14943
  } else {
14129
14944
  await permissionsRepository.updateById(existing.id, {
14130
- displayName: config4.displayName,
14131
- description: config4.description || null,
14132
- category: config4.category || null
14945
+ displayName: config5.displayName,
14946
+ description: config5.description || null,
14947
+ category: config5.category || null
14133
14948
  });
14134
14949
  }
14135
14950
  }
@@ -14191,7 +15006,7 @@ async function getUserPermissions(userId) {
14191
15006
  const permIds = rolePermMappings.map((rp) => rp.permissionId);
14192
15007
  if (permIds.length > 0) {
14193
15008
  const rolePerms = await Promise.all(
14194
- permIds.map((id26) => permissionsRepository.findById(id26))
15009
+ permIds.map((id27) => permissionsRepository.findById(id27))
14195
15010
  );
14196
15011
  for (const perm of rolePerms) {
14197
15012
  if (perm && perm.isActive) {
@@ -14409,20 +15224,20 @@ async function acceptInvitation(params) {
14409
15224
  async function listInvitations(params) {
14410
15225
  return await invitationsRepository.list(params);
14411
15226
  }
14412
- async function cancelInvitation(id26, cancelledBy, reason) {
14413
- const invitation = await invitationsRepository.findById(id26);
15227
+ async function cancelInvitation(id27, cancelledBy, reason) {
15228
+ const invitation = await invitationsRepository.findById(id27);
14414
15229
  if (!invitation) {
14415
15230
  throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
14416
15231
  }
14417
15232
  if (invitation.status !== "pending") {
14418
15233
  throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
14419
15234
  }
14420
- await invitationsRepository.cancel(id26, cancelledBy, reason, invitation.metadata);
15235
+ await invitationsRepository.cancel(id27, cancelledBy, reason, invitation.metadata);
14421
15236
  console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
14422
15237
  }
14423
- async function deleteInvitation(id26) {
14424
- await invitationsRepository.deleteById(id26);
14425
- console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id26}`);
15238
+ async function deleteInvitation(id27) {
15239
+ await invitationsRepository.deleteById(id27);
15240
+ console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id27}`);
14426
15241
  }
14427
15242
  async function expireOldInvitations() {
14428
15243
  const count2 = await invitationsRepository.updateExpiredInvitations();
@@ -14431,8 +15246,8 @@ async function expireOldInvitations() {
14431
15246
  }
14432
15247
  return count2;
14433
15248
  }
14434
- async function resendInvitation(id26, expiresInDays = 7) {
14435
- const invitation = await invitationsRepository.findById(id26);
15249
+ async function resendInvitation(id27, expiresInDays = 7) {
15250
+ const invitation = await invitationsRepository.findById(id27);
14436
15251
  if (!invitation) {
14437
15252
  throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
14438
15253
  }
@@ -14440,7 +15255,7 @@ async function resendInvitation(id26, expiresInDays = 7) {
14440
15255
  throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
14441
15256
  }
14442
15257
  const newExpiresAt = calculateExpiresAt(expiresInDays);
14443
- const updated = await invitationsRepository.resend(id26, newExpiresAt);
15258
+ const updated = await invitationsRepository.resend(id27, newExpiresAt);
14444
15259
  if (!updated) {
14445
15260
  throw new Error("Failed to update invitation");
14446
15261
  }
@@ -14479,13 +15294,13 @@ async function getAuthSessionService(userId) {
14479
15294
  // src/server/lib/one-time-token.ts
14480
15295
  import { SSETokenManager } from "@spfn/core/event/sse";
14481
15296
  var manager = null;
14482
- function initOneTimeTokenManager(config4) {
15297
+ function initOneTimeTokenManager(config5) {
14483
15298
  if (manager) {
14484
15299
  manager.destroy();
14485
15300
  }
14486
15301
  manager = new SSETokenManager({
14487
- ttl: config4?.ttl,
14488
- store: config4?.store
15302
+ ttl: config5?.ttl,
15303
+ store: config5?.store
14489
15304
  });
14490
15305
  }
14491
15306
  function getOneTimeTokenManager() {
@@ -14635,10 +15450,10 @@ function getDefaultScopes() {
14635
15450
  }
14636
15451
  function getGoogleAuthUrl(state, scopes) {
14637
15452
  const resolvedScopes = scopes ?? getDefaultScopes();
14638
- const config4 = getGoogleOAuthConfig();
15453
+ const config5 = getGoogleOAuthConfig();
14639
15454
  const params = new URLSearchParams({
14640
- client_id: config4.clientId,
14641
- redirect_uri: config4.redirectUri,
15455
+ client_id: config5.clientId,
15456
+ redirect_uri: config5.redirectUri,
14642
15457
  response_type: "code",
14643
15458
  scope: resolvedScopes.join(" "),
14644
15459
  state,
@@ -14650,16 +15465,16 @@ function getGoogleAuthUrl(state, scopes) {
14650
15465
  return `${GOOGLE_AUTH_URL}?${params.toString()}`;
14651
15466
  }
14652
15467
  async function exchangeCodeForTokens(code) {
14653
- const config4 = getGoogleOAuthConfig();
15468
+ const config5 = getGoogleOAuthConfig();
14654
15469
  const response = await fetch(GOOGLE_TOKEN_URL, {
14655
15470
  method: "POST",
14656
15471
  headers: {
14657
15472
  "Content-Type": "application/x-www-form-urlencoded"
14658
15473
  },
14659
15474
  body: new URLSearchParams({
14660
- client_id: config4.clientId,
14661
- client_secret: config4.clientSecret,
14662
- redirect_uri: config4.redirectUri,
15475
+ client_id: config5.clientId,
15476
+ client_secret: config5.clientSecret,
15477
+ redirect_uri: config5.redirectUri,
14663
15478
  grant_type: "authorization_code",
14664
15479
  code
14665
15480
  })
@@ -14683,15 +15498,15 @@ async function getGoogleUserInfo(accessToken) {
14683
15498
  return response.json();
14684
15499
  }
14685
15500
  async function refreshAccessToken(refreshToken) {
14686
- const config4 = getGoogleOAuthConfig();
15501
+ const config5 = getGoogleOAuthConfig();
14687
15502
  const response = await fetch(GOOGLE_TOKEN_URL, {
14688
15503
  method: "POST",
14689
15504
  headers: {
14690
15505
  "Content-Type": "application/x-www-form-urlencoded"
14691
15506
  },
14692
15507
  body: new URLSearchParams({
14693
- client_id: config4.clientId,
14694
- client_secret: config4.clientSecret,
15508
+ client_id: config5.clientId,
15509
+ client_secret: config5.clientSecret,
14695
15510
  refresh_token: refreshToken,
14696
15511
  grant_type: "refresh_token"
14697
15512
  })
@@ -14756,8 +15571,8 @@ var registry2 = /* @__PURE__ */ new Map();
14756
15571
  function registerOAuthProvider(provider) {
14757
15572
  registry2.set(provider.id, provider);
14758
15573
  }
14759
- function getOAuthProvider(id26) {
14760
- return registry2.get(id26);
15574
+ function getOAuthProvider(id27) {
15575
+ return registry2.get(id27);
14761
15576
  }
14762
15577
  function getRegisteredProviders() {
14763
15578
  return [...registry2.values()];
@@ -15012,21 +15827,21 @@ var githubProvider = {
15012
15827
  return !!(env4.SPFN_AUTH_GITHUB_CLIENT_ID && env4.SPFN_AUTH_GITHUB_CLIENT_SECRET);
15013
15828
  },
15014
15829
  getAuthUrl(state, scopes) {
15015
- const config4 = getGithubConfig();
15830
+ const config5 = getGithubConfig();
15016
15831
  const params = new URLSearchParams({
15017
- client_id: config4.clientId,
15018
- redirect_uri: config4.redirectUri,
15832
+ client_id: config5.clientId,
15833
+ redirect_uri: config5.redirectUri,
15019
15834
  state,
15020
15835
  scope: (scopes ?? getGithubScopes()).join(" ")
15021
15836
  });
15022
15837
  return `${GITHUB_AUTH_URL}?${params.toString()}`;
15023
15838
  },
15024
15839
  async exchangeCodeForTokens(code) {
15025
- const config4 = getGithubConfig();
15840
+ const config5 = getGithubConfig();
15026
15841
  return requestGithubTokens(new URLSearchParams({
15027
- client_id: config4.clientId,
15028
- client_secret: config4.clientSecret,
15029
- redirect_uri: config4.redirectUri,
15842
+ client_id: config5.clientId,
15843
+ client_secret: config5.clientSecret,
15844
+ redirect_uri: config5.redirectUri,
15030
15845
  code
15031
15846
  }));
15032
15847
  },
@@ -15056,11 +15871,11 @@ var githubProvider = {
15056
15871
  };
15057
15872
  },
15058
15873
  async refreshTokens(refreshToken) {
15059
- const config4 = getGithubConfig();
15874
+ const config5 = getGithubConfig();
15060
15875
  return requestGithubTokens(new URLSearchParams({
15061
15876
  grant_type: "refresh_token",
15062
- client_id: config4.clientId,
15063
- client_secret: config4.clientSecret,
15877
+ client_id: config5.clientId,
15878
+ client_secret: config5.clientSecret,
15064
15879
  refresh_token: refreshToken
15065
15880
  }));
15066
15881
  }
@@ -15184,26 +15999,26 @@ var kakaoProvider = {
15184
15999
  return !!env4.SPFN_AUTH_KAKAO_CLIENT_ID;
15185
16000
  },
15186
16001
  getAuthUrl(state, scopes) {
15187
- const config4 = getKakaoConfig();
16002
+ const config5 = getKakaoConfig();
15188
16003
  const params = new URLSearchParams({
15189
16004
  response_type: "code",
15190
- client_id: config4.clientId,
15191
- redirect_uri: config4.redirectUri,
16005
+ client_id: config5.clientId,
16006
+ redirect_uri: config5.redirectUri,
15192
16007
  state,
15193
16008
  scope: (scopes ?? getKakaoScopes()).join(",")
15194
16009
  });
15195
16010
  return `${KAKAO_AUTH_URL}?${params.toString()}`;
15196
16011
  },
15197
16012
  async exchangeCodeForTokens(code) {
15198
- const config4 = getKakaoConfig();
16013
+ const config5 = getKakaoConfig();
15199
16014
  const params = new URLSearchParams({
15200
16015
  grant_type: "authorization_code",
15201
- client_id: config4.clientId,
15202
- redirect_uri: config4.redirectUri,
16016
+ client_id: config5.clientId,
16017
+ redirect_uri: config5.redirectUri,
15203
16018
  code
15204
16019
  });
15205
- if (config4.clientSecret) {
15206
- params.set("client_secret", config4.clientSecret);
16020
+ if (config5.clientSecret) {
16021
+ params.set("client_secret", config5.clientSecret);
15207
16022
  }
15208
16023
  return requestKakaoTokens(params);
15209
16024
  },
@@ -15245,14 +16060,14 @@ var kakaoProvider = {
15245
16060
  return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
15246
16061
  },
15247
16062
  async refreshTokens(refreshToken) {
15248
- const config4 = getKakaoConfig();
16063
+ const config5 = getKakaoConfig();
15249
16064
  const params = new URLSearchParams({
15250
16065
  grant_type: "refresh_token",
15251
- client_id: config4.clientId,
16066
+ client_id: config5.clientId,
15252
16067
  refresh_token: refreshToken
15253
16068
  });
15254
- if (config4.clientSecret) {
15255
- params.set("client_secret", config4.clientSecret);
16069
+ if (config5.clientSecret) {
16070
+ params.set("client_secret", config5.clientSecret);
15256
16071
  }
15257
16072
  return requestKakaoTokens(params);
15258
16073
  },
@@ -15411,22 +16226,22 @@ var naverProvider = {
15411
16226
  return !!(env4.SPFN_AUTH_NAVER_CLIENT_ID && env4.SPFN_AUTH_NAVER_CLIENT_SECRET);
15412
16227
  },
15413
16228
  getAuthUrl(state) {
15414
- const config4 = getNaverConfig();
16229
+ const config5 = getNaverConfig();
15415
16230
  const params = new URLSearchParams({
15416
16231
  response_type: "code",
15417
- client_id: config4.clientId,
15418
- redirect_uri: config4.redirectUri,
16232
+ client_id: config5.clientId,
16233
+ redirect_uri: config5.redirectUri,
15419
16234
  state
15420
16235
  });
15421
16236
  return `${NAVER_AUTH_URL}?${params.toString()}`;
15422
16237
  },
15423
16238
  async exchangeCodeForTokens(code, options) {
15424
- const config4 = getNaverConfig();
16239
+ const config5 = getNaverConfig();
15425
16240
  return requestNaverTokens(new URLSearchParams({
15426
16241
  grant_type: "authorization_code",
15427
- client_id: config4.clientId,
15428
- client_secret: config4.clientSecret,
15429
- redirect_uri: config4.redirectUri,
16242
+ client_id: config5.clientId,
16243
+ client_secret: config5.clientSecret,
16244
+ redirect_uri: config5.redirectUri,
15430
16245
  code,
15431
16246
  state: options.state
15432
16247
  }));
@@ -15467,11 +16282,11 @@ var naverProvider = {
15467
16282
  return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
15468
16283
  },
15469
16284
  async refreshTokens(refreshToken) {
15470
- const config4 = getNaverConfig();
16285
+ const config5 = getNaverConfig();
15471
16286
  return requestNaverTokens(new URLSearchParams({
15472
16287
  grant_type: "refresh_token",
15473
- client_id: config4.clientId,
15474
- client_secret: config4.clientSecret,
16288
+ client_id: config5.clientId,
16289
+ client_secret: config5.clientSecret,
15475
16290
  refresh_token: refreshToken
15476
16291
  }));
15477
16292
  },
@@ -15830,9 +16645,9 @@ async function oauthUnlinkNotifyService(provider, notification) {
15830
16645
  }
15831
16646
 
15832
16647
  // src/server/services/oauth-native.service.ts
15833
- import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit7 } from "@spfn/core/db";
16648
+ import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
15834
16649
  import {
15835
- InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
16650
+ InvalidKeyFingerprintError as InvalidKeyFingerprintError4,
15836
16651
  NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
15837
16652
  NonceKeyBindingError
15838
16653
  } from "@spfn/auth/errors";
@@ -15859,7 +16674,7 @@ function assertNonceBindsPublicKey(params) {
15859
16674
  throw new NonceKeyBindingError();
15860
16675
  }
15861
16676
  if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
15862
- throw new InvalidKeyFingerprintError3();
16677
+ throw new InvalidKeyFingerprintError4();
15863
16678
  }
15864
16679
  }
15865
16680
  async function persistNativeLogin(identity, params) {
@@ -15903,7 +16718,7 @@ async function persistNativeLogin(identity, params) {
15903
16718
  }
15904
16719
  await updateLastLoginService(userId);
15905
16720
  const mfaEnrolled = isNewUser ? false : await mfaEnrolledForUser(userId);
15906
- onAfterCommit7(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
16721
+ onAfterCommit8(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
15907
16722
  return { mfaRequired: false, userId: String(userId), keyId: params.keyId, isNewUser };
15908
16723
  }, { context: "auth:oauth-native" });
15909
16724
  }
@@ -15953,8 +16768,8 @@ async function verifyOpsTokenService(token) {
15953
16768
  scopes: record.scopes
15954
16769
  };
15955
16770
  }
15956
- async function revokeOpsTokenService(id26) {
15957
- return await opsTokensRepository.revokeById(id26);
16771
+ async function revokeOpsTokenService(id27) {
16772
+ return await opsTokensRepository.revokeById(id27);
15958
16773
  }
15959
16774
  async function listOpsTokensService() {
15960
16775
  return await opsTokensRepository.list();
@@ -15970,7 +16785,7 @@ var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
15970
16785
  var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
15971
16786
  var DEFAULT_CODE_TTL_MS = 60 * 1e3;
15972
16787
  var AUTHORIZE_PATH = "/oauth/authorize";
15973
- var config3 = null;
16788
+ var config4 = null;
15974
16789
  function resolveIssuerSource(env21 = process.env) {
15975
16790
  return { value: env21.SPFN_API_URL, variable: "SPFN_API_URL" };
15976
16791
  }
@@ -15980,7 +16795,7 @@ function resolveAuthorizeUrl(env21) {
15980
16795
  }
15981
16796
  function configureAuthorizationServer(options, env21 = process.env) {
15982
16797
  if (!options) {
15983
- config3 = null;
16798
+ config4 = null;
15984
16799
  return;
15985
16800
  }
15986
16801
  const scopeNames = Object.keys(options.scopes ?? {});
@@ -15989,7 +16804,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
15989
16804
  "authorizationServer.scopes must name at least one scope. The names are published in /.well-known/oauth-authorization-server and shown on the consent screen, so there is nothing to derive them from."
15990
16805
  );
15991
16806
  }
15992
- config3 = {
16807
+ config4 = {
15993
16808
  issuer: canonicalIssuer(options.issuer ?? resolveIssuerSource(env21).value ?? ""),
15994
16809
  issuerSource: options.issuer ? "authorizationServer.issuer" : resolveIssuerSource(env21).variable,
15995
16810
  authorizeUrl: options.authorizeUrl ?? resolveAuthorizeUrl(env21),
@@ -16000,7 +16815,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
16000
16815
  refreshTokenTtlMs: options.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS,
16001
16816
  codeTtlMs: options.codeTtlMs ?? DEFAULT_CODE_TTL_MS
16002
16817
  };
16003
- assertKnownDefaultScopes(config3);
16818
+ assertKnownDefaultScopes(config4);
16004
16819
  }
16005
16820
  function assertKnownDefaultScopes(resolved) {
16006
16821
  const unknown = resolved.defaultScopes.filter((scope) => !(scope in resolved.scopes));
@@ -16011,7 +16826,7 @@ function assertKnownDefaultScopes(resolved) {
16011
16826
  }
16012
16827
  }
16013
16828
  function getAuthorizationServerConfig() {
16014
- return config3;
16829
+ return config4;
16015
16830
  }
16016
16831
  function isLoopbackHostname(hostname) {
16017
16832
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
@@ -16234,15 +17049,15 @@ var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
16234
17049
  var SUPPORTED_AUTH_METHOD = "none";
16235
17050
  var SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
16236
17051
  var SUPPORTED_RESPONSE_TYPES = ["code"];
16237
- function refuse(status, error, description) {
17052
+ function refuse2(status, error, description) {
16238
17053
  return { ok: false, status, error, description };
16239
17054
  }
16240
17055
  function requireConfig() {
16241
- const config4 = getAuthorizationServerConfig();
16242
- if (!config4) {
17056
+ const config5 = getAuthorizationServerConfig();
17057
+ if (!config5) {
16243
17058
  throw new Error("OAuth2 client service called with no authorization server configured.");
16244
17059
  }
16245
- return config4;
17060
+ return config5;
16246
17061
  }
16247
17062
  function asStringArray(value) {
16248
17063
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
@@ -16253,7 +17068,7 @@ function asStringArray(value) {
16253
17068
  function refuseDeclaredMetadata(request) {
16254
17069
  const { token_endpoint_auth_method: authMethod } = request;
16255
17070
  if (authMethod !== void 0 && authMethod !== SUPPORTED_AUTH_METHOD) {
16256
- return refuse(
17071
+ return refuse2(
16257
17072
  400,
16258
17073
  "invalid_client_metadata",
16259
17074
  `token_endpoint_auth_method must be "${SUPPORTED_AUTH_METHOD}". This authorization server registers public clients only \u2014 a client on a user's own machine cannot keep a secret.`
@@ -16267,7 +17082,7 @@ function refuseListedMetadata(field, value, supported) {
16267
17082
  }
16268
17083
  const declared = asStringArray(value);
16269
17084
  if (!declared || declared.length === 0 || declared.some((entry) => !supported.includes(entry))) {
16270
- return refuse(
17085
+ return refuse2(
16271
17086
  400,
16272
17087
  "invalid_client_metadata",
16273
17088
  `${field} must be a non-empty subset of ${supported.join(", ")}.`
@@ -16278,7 +17093,7 @@ function refuseListedMetadata(field, value, supported) {
16278
17093
  function refuseRedirectUris(uris, allowedRedirectOrigins) {
16279
17094
  const declared = asStringArray(uris);
16280
17095
  if (!declared || declared.length === 0) {
16281
- return refuse(
17096
+ return refuse2(
16282
17097
  400,
16283
17098
  "invalid_client_metadata",
16284
17099
  "redirect_uris must list at least one absolute URI. There is nowhere to send an authorization code without one."
@@ -16287,20 +17102,20 @@ function refuseRedirectUris(uris, allowedRedirectOrigins) {
16287
17102
  for (const uri of declared) {
16288
17103
  const detail = refuseRedirectUriRegistration(uri, allowedRedirectOrigins);
16289
17104
  if (detail) {
16290
- return refuse(400, "invalid_redirect_uri", detail);
17105
+ return refuse2(400, "invalid_redirect_uri", detail);
16291
17106
  }
16292
17107
  }
16293
17108
  return null;
16294
17109
  }
16295
17110
  async function registerOAuth2ClientService(request, clientIp) {
16296
- const config4 = requireConfig();
16297
- const refusal = refuseRedirectUris(request.redirect_uris, config4.allowedRedirectOrigins) ?? refuseDeclaredMetadata(request);
17111
+ const config5 = requireConfig();
17112
+ const refusal = refuseRedirectUris(request.redirect_uris, config5.allowedRedirectOrigins) ?? refuseDeclaredMetadata(request);
16298
17113
  if (refusal) {
16299
17114
  return refusal;
16300
17115
  }
16301
17116
  const record = await createUnderStandingCap(newClientRow(request, clientIp), clientIp);
16302
17117
  if (!record) {
16303
- return refuse(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
17118
+ return refuse2(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
16304
17119
  }
16305
17120
  authLogger.service.info("OAuth2 client registered", { clientName: record.clientName });
16306
17121
  return { ok: true, client: describeClient(record) };
@@ -16379,11 +17194,11 @@ function sameResource(presented, granted) {
16379
17194
 
16380
17195
  // src/server/services/oauth2-authorize.service.ts
16381
17196
  function requireConfig2() {
16382
- const config4 = getAuthorizationServerConfig();
16383
- if (!config4) {
17197
+ const config5 = getAuthorizationServerConfig();
17198
+ if (!config5) {
16384
17199
  throw new Error("OAuth2 authorize service called with no authorization server configured.");
16385
17200
  }
16386
- return config4;
17201
+ return config5;
16387
17202
  }
16388
17203
  async function resolveRedirectTarget(params) {
16389
17204
  const client = await oauth2ClientsRepository.findByClientId(params.clientId);
@@ -16395,10 +17210,10 @@ async function resolveRedirectTarget(params) {
16395
17210
  }
16396
17211
  return { client, redirectUri: params.redirectUri };
16397
17212
  }
16398
- function resolveScopes(params, config4) {
17213
+ function resolveScopes(params, config5) {
16399
17214
  const requested = params.scope?.trim();
16400
17215
  if (!requested) {
16401
- return config4.defaultScopes;
17216
+ return config5.defaultScopes;
16402
17217
  }
16403
17218
  return requested.split(/\s+/);
16404
17219
  }
@@ -16410,7 +17225,7 @@ function refuseRedirectable(params, redirectUri, error, message) {
16410
17225
  function hasUsableS256Challenge(params) {
16411
17226
  return params.codeChallengeMethod === "S256" && !!params.codeChallenge && isPkceS256ChallengeShaped(params.codeChallenge);
16412
17227
  }
16413
- function assertRedirectableRules(params, redirectUri, config4) {
17228
+ function assertRedirectableRules(params, redirectUri, config5) {
16414
17229
  if (!hasUsableS256Challenge(params)) {
16415
17230
  refuseRedirectable(params, redirectUri, "invalid_request", PKCE_REQUIRED_MESSAGE);
16416
17231
  }
@@ -16418,31 +17233,31 @@ function assertRedirectableRules(params, redirectUri, config4) {
16418
17233
  if (!resource) {
16419
17234
  refuseRedirectable(params, redirectUri, "invalid_target", RESOURCE_REQUIRED_MESSAGE);
16420
17235
  }
16421
- const scopes = resolveScopes(params, config4);
16422
- const unknown = scopes.filter((scope) => !(scope in config4.scopes));
17236
+ const scopes = resolveScopes(params, config5);
17237
+ const unknown = scopes.filter((scope) => !(scope in config5.scopes));
16423
17238
  if (unknown.length > 0) {
16424
17239
  refuseRedirectable(params, redirectUri, "invalid_scope", `Unknown scope: ${unknown.join(", ")}.`);
16425
17240
  }
16426
17241
  return { resource, scopes, codeChallenge: params.codeChallenge };
16427
17242
  }
16428
17243
  async function validate(params) {
16429
- const config4 = requireConfig2();
17244
+ const config5 = requireConfig2();
16430
17245
  const { client, redirectUri } = await resolveRedirectTarget(params);
16431
- const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config4);
17246
+ const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config5);
16432
17247
  return { client, redirectUri, resource, scopes, codeChallenge, state: params.state };
16433
17248
  }
16434
17249
  async function describeOAuth2AuthorizeRequestService(params) {
16435
- const config4 = requireConfig2();
17250
+ const config5 = requireConfig2();
16436
17251
  const validated = await validate(params);
16437
17252
  return {
16438
17253
  clientName: validated.client.clientName,
16439
17254
  redirectHost: redirectHostOf(validated.redirectUri),
16440
- scopes: validated.scopes.map((name) => ({ name, description: config4.scopes[name] })),
17255
+ scopes: validated.scopes.map((name) => ({ name, description: config5.scopes[name] })),
16441
17256
  resource: validated.resource
16442
17257
  };
16443
17258
  }
16444
17259
  async function approveOAuth2AuthorizeService(params, userId) {
16445
- const config4 = requireConfig2();
17260
+ const config5 = requireConfig2();
16446
17261
  const validated = await validate(params);
16447
17262
  const grant = await oauth2GrantsRepository.upsert({
16448
17263
  client: validated.client.id,
@@ -16456,7 +17271,7 @@ async function approveOAuth2AuthorizeService(params, userId) {
16456
17271
  grant: grant.id,
16457
17272
  redirectUri: validated.redirectUri,
16458
17273
  codeChallenge: validated.codeChallenge,
16459
- expiresAt: new Date(Date.now() + config4.codeTtlMs)
17274
+ expiresAt: new Date(Date.now() + config5.codeTtlMs)
16460
17275
  });
16461
17276
  return { code, redirectUri: validated.redirectUri, state: validated.state };
16462
17277
  }
@@ -16484,15 +17299,15 @@ function invalidGrant() {
16484
17299
  description: "The authorization code or refresh token is invalid, expired, already used, or was issued to another client."
16485
17300
  };
16486
17301
  }
16487
- function refuse2(error, description) {
17302
+ function refuse3(error, description) {
16488
17303
  return { ok: false, error, description };
16489
17304
  }
16490
17305
  function requireConfig3() {
16491
- const config4 = getAuthorizationServerConfig();
16492
- if (!config4) {
17306
+ const config5 = getAuthorizationServerConfig();
17307
+ if (!config5) {
16493
17308
  throw new Error("OAuth2 token service called with no authorization server configured.");
16494
17309
  }
16495
- return config4;
17310
+ return config5;
16496
17311
  }
16497
17312
  async function oauth2TokenService(request) {
16498
17313
  if (request.grant_type === "authorization_code") {
@@ -16501,14 +17316,14 @@ async function oauth2TokenService(request) {
16501
17316
  if (request.grant_type === "refresh_token") {
16502
17317
  return await refreshTokens(request);
16503
17318
  }
16504
- return refuse2(
17319
+ return refuse3(
16505
17320
  "unsupported_grant_type",
16506
17321
  "grant_type must be authorization_code or refresh_token."
16507
17322
  );
16508
17323
  }
16509
17324
  async function exchangeAuthorizationCode(request) {
16510
17325
  if (!request.code || !request.code_verifier || !request.client_id || !request.redirect_uri) {
16511
- return refuse2(
17326
+ return refuse3(
16512
17327
  "invalid_request",
16513
17328
  "authorization_code requires code, code_verifier, client_id and redirect_uri."
16514
17329
  );
@@ -16542,13 +17357,13 @@ async function spendBoundCode(request, record, pair) {
16542
17357
  return invalidGrant();
16543
17358
  }
16544
17359
  if (resolveResource(request.resource, pair.grant) === null) {
16545
- return refuse2("invalid_target", "resource does not match the resource this grant was issued for.");
17360
+ return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
16546
17361
  }
16547
17362
  return { ok: true, tokens: await issueTokenPair(pair.grant, pair.grant.scopes) };
16548
17363
  }
16549
17364
  async function refreshTokens(request) {
16550
17365
  if (!request.refresh_token || !request.client_id) {
16551
- return refuse2("invalid_request", "refresh_token requires refresh_token and client_id.");
17366
+ return refuse3("invalid_request", "refresh_token requires refresh_token and client_id.");
16552
17367
  }
16553
17368
  const tokenHash = hashOAuth2Secret(request.refresh_token);
16554
17369
  const presented = await oauth2TokensRepository.findByTokenHash(tokenHash);
@@ -16570,11 +17385,11 @@ async function rotateRefresh(request, tokenHash, grantId, presentedScopes) {
16570
17385
  return invalidGrant();
16571
17386
  }
16572
17387
  if (resolveResource(request.resource, pair.grant) === null) {
16573
- return refuse2("invalid_target", "resource does not match the resource this grant was issued for.");
17388
+ return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
16574
17389
  }
16575
17390
  const scopes = resolveRefreshScopes(request.scope, pair.grant, presentedScopes);
16576
17391
  if (!scopes) {
16577
- return refuse2("invalid_scope", "A refresh may ask for a subset of the granted scopes, never more.");
17392
+ return refuse3("invalid_scope", "A refresh may ask for a subset of the granted scopes, never more.");
16578
17393
  }
16579
17394
  if (!await oauth2TokensRepository.rotate(tokenHash)) {
16580
17395
  return await refuseLostRotation(tokenHash, grantId);
@@ -16603,10 +17418,10 @@ function resolveResource(requested, grant) {
16603
17418
  return sameResource(requested, grant.resource) ? grant.resource : null;
16604
17419
  }
16605
17420
  async function issueTokenPair(grant, scopes) {
16606
- const config4 = requireConfig3();
17421
+ const config5 = requireConfig3();
16607
17422
  const accessToken = generateAccessToken();
16608
17423
  const refreshToken = generateRefreshToken();
16609
- const expiresAt = new Date(Date.now() + config4.accessTokenTtlMs);
17424
+ const expiresAt = new Date(Date.now() + config5.accessTokenTtlMs);
16610
17425
  await runInTransaction6(async () => {
16611
17426
  await storeToken(accessToken, "access", grant.id, scopes, expiresAt);
16612
17427
  await storeToken(
@@ -16614,7 +17429,7 @@ async function issueTokenPair(grant, scopes) {
16614
17429
  "refresh",
16615
17430
  grant.id,
16616
17431
  scopes,
16617
- new Date(Date.now() + config4.refreshTokenTtlMs)
17432
+ new Date(Date.now() + config5.refreshTokenTtlMs)
16618
17433
  );
16619
17434
  });
16620
17435
  oauth2ClientsRepository.updateLastUsedById(grant.client).catch((err) => authLogger.service.error("Failed to update OAuth2 client lastUsedAt", err));
@@ -16937,6 +17752,72 @@ function attestedClientIp(c) {
16937
17752
  return provenance.webProxy ? provenance.ip ?? null : null;
16938
17753
  }
16939
17754
 
17755
+ // src/server/middleware/device-auth-long-poll.ts
17756
+ var DEVICE_AUTH_WAITED_MILLIS = "deviceAuthWaitedMillis";
17757
+ function deviceAuthLongPoll() {
17758
+ return async (c, next) => {
17759
+ const target = waitTarget(await c.req.json().catch(() => null));
17760
+ if (!target) {
17761
+ return next();
17762
+ }
17763
+ const signal = c.req.raw.signal;
17764
+ const waitedMillis = await waitForDeviceAuthAnswerService({ ...target, signal });
17765
+ if (signal.aborted) {
17766
+ return c.body(null, 204);
17767
+ }
17768
+ c.set(DEVICE_AUTH_WAITED_MILLIS, waitedMillis);
17769
+ return next();
17770
+ };
17771
+ }
17772
+ function waitTarget(body) {
17773
+ const { deviceCode, waitMillis } = body ?? {};
17774
+ if (typeof deviceCode !== "string" || !Number.isInteger(waitMillis) || waitMillis <= 0) {
17775
+ return null;
17776
+ }
17777
+ return { deviceCode, waitMillis };
17778
+ }
17779
+
17780
+ // src/server/middleware/device-link-long-poll.ts
17781
+ var DEVICE_LINK_WAITED_MILLIS = "deviceLinkWaitedMillis";
17782
+ function deviceLinkStatusLongPoll() {
17783
+ return longPoll(async (c, body, waitMillis, signal) => {
17784
+ const auth = getOptionalAuth(c);
17785
+ if (typeof body.linkId !== "string" || !auth) {
17786
+ return 0;
17787
+ }
17788
+ return waitForDeviceLinkStatusService({
17789
+ linkId: body.linkId,
17790
+ issuer: { userId: Number(auth.userId), keyId: auth.keyId },
17791
+ waitMillis,
17792
+ signal
17793
+ });
17794
+ });
17795
+ }
17796
+ function deviceLinkPollLongPoll() {
17797
+ return longPoll(async (_c, body, waitMillis, signal) => {
17798
+ if (typeof body.deviceCode !== "string") {
17799
+ return 0;
17800
+ }
17801
+ return waitForDeviceLinkAnswerService({ deviceCode: body.deviceCode, waitMillis, signal });
17802
+ });
17803
+ }
17804
+ function longPoll(wait) {
17805
+ return async (c, next) => {
17806
+ const body = await c.req.json().catch(() => null) ?? {};
17807
+ const { waitMillis } = body;
17808
+ if (!Number.isInteger(waitMillis) || waitMillis <= 0) {
17809
+ return next();
17810
+ }
17811
+ const signal = c.req.raw.signal;
17812
+ const waitedMillis = await wait(c, body, waitMillis, signal);
17813
+ if (signal.aborted) {
17814
+ return c.body(null, 204);
17815
+ }
17816
+ c.set(DEVICE_LINK_WAITED_MILLIS, waitedMillis);
17817
+ return next();
17818
+ };
17819
+ }
17820
+
16940
17821
  // src/server/routes/auth/index.ts
16941
17822
  var sendVerificationCode = route.post("/_auth/codes").input({
16942
17823
  body: Type.Object({
@@ -17095,11 +17976,23 @@ var startDeviceAuth = route.post("/_auth/device/start").input({
17095
17976
  });
17096
17977
  var pollDeviceAuth = route.post("/_auth/device/poll").input({
17097
17978
  body: Type.Object({
17098
- deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
17979
+ deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" }),
17980
+ waitMillis: Type.Optional(Type.Integer({
17981
+ minimum: 0,
17982
+ description: "Longest to hold the request while nobody has answered; capped by the server"
17983
+ }))
17099
17984
  })
17100
- }).use([rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
17985
+ }).use([
17986
+ rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }),
17987
+ deviceAuthLongPoll(),
17988
+ Transactional()
17989
+ ]).skip(["auth"]).handler(async (c) => {
17101
17990
  const { body } = await c.data();
17102
- return await pollDeviceAuthService({ ...body, ...deviceProvenance(c.raw) });
17991
+ return await pollDeviceAuthService({
17992
+ deviceCode: body.deviceCode,
17993
+ ...deviceProvenance(c.raw),
17994
+ waitedMillis: Number(c.raw.get(DEVICE_AUTH_WAITED_MILLIS) ?? 0)
17995
+ });
17103
17996
  });
17104
17997
  var getDeviceAuthInfo = route.post("/_auth/device/info").input({
17105
17998
  body: Type.Object({
@@ -17136,6 +18029,101 @@ var denyDeviceAuth = route.post("/_auth/device/deny").input({
17136
18029
  await denyDeviceAuthService(body);
17137
18030
  return c.noContent();
17138
18031
  });
18032
+ function issuerOf(c) {
18033
+ const { userId, keyId } = getAuth(c);
18034
+ return { userId: Number(userId), keyId };
18035
+ }
18036
+ var issueDeviceLink = route.post("/_auth/device/link/issue").use([
18037
+ rateLimitPolicy("auth-device-link-issue", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
18038
+ Transactional()
18039
+ ]).handler(async (c) => {
18040
+ return await issueDeviceLinkService(issuerOf(c));
18041
+ });
18042
+ var redeemDeviceLink = route.post("/_auth/device/link/redeem").input({
18043
+ body: Type.Object({
18044
+ userCode: UserCodeSchema,
18045
+ publicKey: PublicKeySchema,
18046
+ keyId: KeyIdSchema,
18047
+ fingerprint: FingerprintSchema,
18048
+ algorithm: Type.Optional(Type.Union(
18049
+ KEY_ALGORITHM.map((algo) => Type.Literal(algo)),
18050
+ { description: "Signature algorithm" }
18051
+ )),
18052
+ deviceName: Type.Optional(DeviceNameSchema),
18053
+ platform: Type.Optional(PlatformSchema)
18054
+ })
18055
+ }).use([rateLimitPolicy("auth-device-link-redeem", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
18056
+ const { body } = await c.data();
18057
+ return await redeemDeviceLinkService(body);
18058
+ });
18059
+ var getDeviceLinkStatus = route.post("/_auth/device/link/status").input({
18060
+ body: Type.Object({
18061
+ linkId: LinkIdSchema,
18062
+ waitMillis: Type.Optional(Type.Integer({
18063
+ minimum: 0,
18064
+ description: "Longest to hold the request while the link waits on the other device; capped by the server"
18065
+ }))
18066
+ })
18067
+ }).use([
18068
+ rateLimitPolicy("auth-device-link-status", { limit: 30, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 150 }) }),
18069
+ deviceLinkStatusLongPoll()
18070
+ ]).handler(async (c) => {
18071
+ const { body } = await c.data();
18072
+ return await getDeviceLinkStatusService({ linkId: body.linkId, issuer: issuerOf(c) });
18073
+ });
18074
+ var confirmDeviceLink = route.post("/_auth/device/link/confirm").input({
18075
+ body: Type.Object({
18076
+ linkId: LinkIdSchema,
18077
+ choice: MatchChoiceSchema
18078
+ })
18079
+ }).use([
18080
+ rateLimitPolicy("auth-device-link-confirm", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) })
18081
+ ]).handler(async (c) => {
18082
+ const { body } = await c.data();
18083
+ return await confirmDeviceLinkService({ linkId: body.linkId, choice: body.choice, issuer: issuerOf(c) });
18084
+ });
18085
+ var denyDeviceLink = route.post("/_auth/device/link/deny").input({
18086
+ body: Type.Object({
18087
+ linkId: LinkIdSchema
18088
+ })
18089
+ }).use([
18090
+ rateLimitPolicy("auth-device-link-deny", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
18091
+ Transactional()
18092
+ ]).handler(async (c) => {
18093
+ const { body } = await c.data();
18094
+ return await denyDeviceLinkService({ linkId: body.linkId, issuer: issuerOf(c) });
18095
+ });
18096
+ var cancelDeviceLink = route.post("/_auth/device/link/cancel").input({
18097
+ body: Type.Object({
18098
+ linkId: LinkIdSchema
18099
+ })
18100
+ }).use([
18101
+ rateLimitPolicy("auth-device-link-cancel", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
18102
+ Transactional()
18103
+ ]).handler(async (c) => {
18104
+ const { body } = await c.data();
18105
+ return await cancelDeviceLinkService({ linkId: body.linkId, issuer: issuerOf(c) });
18106
+ });
18107
+ var pollDeviceLink = route.post("/_auth/device/link/poll").input({
18108
+ body: Type.Object({
18109
+ deviceCode: Type.String({ description: "Device code returned by /_auth/device/link/redeem" }),
18110
+ waitMillis: Type.Optional(Type.Integer({
18111
+ minimum: 0,
18112
+ description: "Longest to hold the request while the issuer has not picked; capped by the server"
18113
+ }))
18114
+ })
18115
+ }).use([
18116
+ rateLimitPolicy("auth-device-link-poll", { limit: 30, windowMs: 6e4 }),
18117
+ deviceLinkPollLongPoll(),
18118
+ Transactional()
18119
+ ]).skip(["auth"]).handler(async (c) => {
18120
+ const { body } = await c.data();
18121
+ return await pollDeviceLinkService({
18122
+ deviceCode: body.deviceCode,
18123
+ ...deviceProvenance(c.raw),
18124
+ waitedMillis: Number(c.raw.get(DEVICE_LINK_WAITED_MILLIS) ?? 0)
18125
+ });
18126
+ });
17139
18127
  var logout = route.post("/_auth/logout").handler(async (c) => {
17140
18128
  const auth = getAuth(c);
17141
18129
  if (!auth) {
@@ -17251,6 +18239,13 @@ var authRouter = defineRouter({
17251
18239
  getDeviceAuthInfo,
17252
18240
  approveDeviceAuth,
17253
18241
  denyDeviceAuth,
18242
+ issueDeviceLink,
18243
+ redeemDeviceLink,
18244
+ getDeviceLinkStatus,
18245
+ confirmDeviceLink,
18246
+ denyDeviceLink,
18247
+ cancelDeviceLink,
18248
+ pollDeviceLink,
17254
18249
  logout,
17255
18250
  rotateKey,
17256
18251
  listKeys,
@@ -17558,13 +18553,13 @@ var CanonicalJsonError = class extends Error {
17558
18553
  var INT64_MIN = -(2n ** 63n);
17559
18554
  var INT64_MAX = 2n ** 63n - 1n;
17560
18555
  function parseCanonicalJson(bytes) {
17561
- let text27;
18556
+ let text28;
17562
18557
  try {
17563
- text27 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
18558
+ text28 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
17564
18559
  } catch {
17565
18560
  throw new CanonicalJsonError("INVALID_UTF8");
17566
18561
  }
17567
- const parser = new Parser(text27);
18562
+ const parser = new Parser(text28);
17568
18563
  const value = parser.parseValue();
17569
18564
  parser.skipWhitespace();
17570
18565
  if (!parser.atEnd()) {
@@ -17585,8 +18580,8 @@ function isCanonicalBytes(bytes, value) {
17585
18580
  return true;
17586
18581
  }
17587
18582
  var Parser = class {
17588
- constructor(text27) {
17589
- this.text = text27;
18583
+ constructor(text28) {
18584
+ this.text = text28;
17590
18585
  }
17591
18586
  pos = 0;
17592
18587
  atEnd() {
@@ -18176,7 +19171,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
18176
19171
 
18177
19172
  // src/server/client-proof/contract-bundle.ts
18178
19173
  init_wire_headers();
18179
- var CONTRACT_VERSION = "0.13.0";
19174
+ var CONTRACT_VERSION = "0.13.2";
18180
19175
  var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
18181
19176
  function required(name, type) {
18182
19177
  return { name, type, optional: false };
@@ -18510,7 +19505,8 @@ var CONTRACT_TYPES = [
18510
19505
  {
18511
19506
  name: "PollDeviceAuthRequest",
18512
19507
  fields: [
18513
- required("deviceCode", "string")
19508
+ required("deviceCode", "string"),
19509
+ optional("waitMillis", "integer")
18514
19510
  ]
18515
19511
  },
18516
19512
  /**
@@ -18571,6 +19567,39 @@ var CONTRACT_TYPES = [
18571
19567
  fields: [
18572
19568
  required("userCode", "string")
18573
19569
  ]
19570
+ },
19571
+ /**
19572
+ * `StartDeviceAuthRequest` with the code it redeems in front: the same key
19573
+ * material under the same bounds, since this is the other place a caller
19574
+ * with nothing to authenticate parks a key before anyone agreed to it.
19575
+ */
19576
+ {
19577
+ name: "RedeemDeviceLinkRequest",
19578
+ fields: [
19579
+ required("userCode", "string"),
19580
+ required("publicKey", "string"),
19581
+ required("keyId", "string"),
19582
+ required("fingerprint", "string"),
19583
+ optional("algorithm", "KeyAlgorithm"),
19584
+ optional("deviceName", "string"),
19585
+ optional("platform", "KeyPlatform")
19586
+ ]
19587
+ },
19588
+ {
19589
+ name: "RedeemDeviceLinkResponse",
19590
+ fields: [
19591
+ required("deviceCode", "string"),
19592
+ required("matchNumber", "integer"),
19593
+ required("expiresAtMillis", "integer"),
19594
+ required("intervalMillis", "integer")
19595
+ ]
19596
+ },
19597
+ {
19598
+ name: "PollDeviceLinkRequest",
19599
+ fields: [
19600
+ required("deviceCode", "string"),
19601
+ optional("waitMillis", "integer")
19602
+ ]
18574
19603
  }
18575
19604
  ];
18576
19605
  var CONTRACT_ENUMS = [
@@ -19638,16 +20667,16 @@ function extractBearer2(header) {
19638
20667
  }
19639
20668
 
19640
20669
  // src/server/middleware/ops-or-user.ts
19641
- function opsOrUser(config4) {
19642
- const roles2 = config4.roles ?? [];
19643
- const permissions2 = config4.permissions ?? [];
19644
- if (!config4.opsScopes || config4.opsScopes.length === 0) {
20670
+ function opsOrUser(config5) {
20671
+ const roles2 = config5.roles ?? [];
20672
+ const permissions2 = config5.permissions ?? [];
20673
+ if (!config5.opsScopes || config5.opsScopes.length === 0) {
19645
20674
  throw new Error("opsOrUser: opsScopes must name at least one scope \u2014 an ops token would otherwise be admitted unchecked.");
19646
20675
  }
19647
20676
  if (roles2.length === 0 && permissions2.length === 0) {
19648
20677
  throw new Error("opsOrUser: give roles, permissions, or both \u2014 a user session would otherwise be admitted unchecked.");
19649
20678
  }
19650
- const ops = chain([opsTokenAuth.handler, requireOpsScope(...config4.opsScopes)]);
20679
+ const ops = chain([opsTokenAuth.handler, requireOpsScope(...config5.opsScopes)]);
19651
20680
  const user = chain([
19652
20681
  authenticate.handler,
19653
20682
  ...roles2.length > 0 ? [requireRole(...roles2)] : [],
@@ -19662,8 +20691,8 @@ function bearerOf(header) {
19662
20691
  function chain(handlers) {
19663
20692
  return async (c, next) => {
19664
20693
  let answer;
19665
- const runFrom = async (index21) => {
19666
- const produced = index21 < handlers.length ? await handlers[index21](c, () => runFrom(index21 + 1)) : await next();
20694
+ const runFrom = async (index22) => {
20695
+ const produced = index22 < handlers.length ? await handlers[index22](c, () => runFrom(index22 + 1)) : await next();
19667
20696
  if (produced instanceof Response) {
19668
20697
  answer = produced;
19669
20698
  }
@@ -20690,13 +21719,13 @@ var NO_STORE_HEADERS = {
20690
21719
  Pragma: "no-cache"
20691
21720
  };
20692
21721
  function requireAuthorizationServer() {
20693
- const config4 = getAuthorizationServerConfig();
20694
- if (!config4) {
21722
+ const config5 = getAuthorizationServerConfig();
21723
+ if (!config5) {
20695
21724
  throw new NotFoundError6({
20696
21725
  message: "This application does not run an OAuth 2.1 authorization server. Pass `authorizationServer` to createAuthLifecycle() to enable one."
20697
21726
  });
20698
21727
  }
20699
- return config4;
21728
+ return config5;
20700
21729
  }
20701
21730
  function oauth2ErrorResponse(c, status, error, description) {
20702
21731
  return c.json({ error, error_description: description }, status, NO_STORE_HEADERS);
@@ -20767,18 +21796,18 @@ var revokeOAuth2Grant = route14.delete("/_auth/oauth2/grants/:id").input({ param
20767
21796
  return { revoked: true };
20768
21797
  });
20769
21798
  var oauth2AuthorizationServerMetadata = route14.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
20770
- const config4 = requireAuthorizationServer();
21799
+ const config5 = requireAuthorizationServer();
20771
21800
  return c.json({
20772
- issuer: config4.issuer,
20773
- authorization_endpoint: config4.authorizeUrl,
20774
- token_endpoint: new URL("/_auth/oauth2/token", config4.issuer).toString(),
20775
- registration_endpoint: new URL("/_auth/oauth2/register", config4.issuer).toString(),
20776
- revocation_endpoint: new URL("/_auth/oauth2/revoke", config4.issuer).toString(),
21801
+ issuer: config5.issuer,
21802
+ authorization_endpoint: config5.authorizeUrl,
21803
+ token_endpoint: new URL("/_auth/oauth2/token", config5.issuer).toString(),
21804
+ registration_endpoint: new URL("/_auth/oauth2/register", config5.issuer).toString(),
21805
+ revocation_endpoint: new URL("/_auth/oauth2/revoke", config5.issuer).toString(),
20777
21806
  response_types_supported: SUPPORTED_RESPONSE_TYPES,
20778
21807
  grant_types_supported: SUPPORTED_GRANT_TYPES,
20779
21808
  token_endpoint_auth_methods_supported: ["none"],
20780
21809
  code_challenge_methods_supported: ["S256"],
20781
- scopes_supported: Object.keys(config4.scopes)
21810
+ scopes_supported: Object.keys(config5.scopes)
20782
21811
  });
20783
21812
  });
20784
21813
 
@@ -20878,6 +21907,14 @@ var mainAuthRouter = defineRouter6({
20878
21907
  getDeviceAuthInfo,
20879
21908
  approveDeviceAuth,
20880
21909
  denyDeviceAuth,
21910
+ // Device link routes
21911
+ issueDeviceLink,
21912
+ redeemDeviceLink,
21913
+ getDeviceLinkStatus,
21914
+ confirmDeviceLink,
21915
+ denyDeviceLink,
21916
+ cancelDeviceLink,
21917
+ pollDeviceLink,
20881
21918
  // Passkey routes (WebAuthn)
20882
21919
  passkeyRegisterOptions,
20883
21920
  passkeyRegisterVerify,
@@ -21365,6 +22402,7 @@ function assertOAuthRedirectUris(env21 = process.env) {
21365
22402
  function createAuthLifecycle(options = {}) {
21366
22403
  configureDeletion(options.deletion);
21367
22404
  configureDeviceAuth(options.deviceAuth);
22405
+ configureDeviceLink(options.deviceLink);
21368
22406
  configureAuthorizationServer(options.authorizationServer);
21369
22407
  return {
21370
22408
  /**
@@ -21479,12 +22517,16 @@ export {
21479
22517
  DEFAULT_DELETION_PURGE_STRATEGY,
21480
22518
  DEFAULT_DELETION_SEND_NOTIFICATIONS,
21481
22519
  DEFAULT_DEVICE_AUTH_INTERVAL_MS,
22520
+ DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
21482
22521
  DEFAULT_DEVICE_AUTH_TTL_MS,
22522
+ DEFAULT_DEVICE_LINK_TTL_MS,
21483
22523
  DEFAULT_REFRESH_TOKEN_TTL_MS,
21484
22524
  DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,
21485
22525
  DEVICE_AUTH_STATUSES,
22526
+ DEVICE_LINK_STATUSES,
21486
22527
  DeviceAuthPollResponseSchema,
21487
22528
  DeviceAuthorizationsRepository,
22529
+ DeviceLinksRepository,
21488
22530
  DeviceNameSchema,
21489
22531
  EmailSchema,
21490
22532
  EnvironmentKeyringTokenCipher,
@@ -21498,11 +22540,13 @@ export {
21498
22540
  KeyIdSchema,
21499
22541
  KeyRevokeAllTokensRepository,
21500
22542
  KeysRepository,
22543
+ LinkIdSchema,
21501
22544
  MAX_UNGRANTED_CLIENTS_PER_IP,
21502
22545
  MFA_CHALLENGE_ATTEMPT_LIMIT,
21503
22546
  MFA_CHALLENGE_CHANNELS,
21504
22547
  MFA_CONFIRM_ATTEMPT_LIMIT,
21505
22548
  MFA_VERIFICATION_METHODS,
22549
+ MatchChoiceSchema,
21506
22550
  MfaChallengesRepository,
21507
22551
  MfaEnrolmentRepository,
21508
22552
  MfaRecoveryCodesRepository,
@@ -21587,6 +22631,7 @@ export {
21587
22631
  bearerAuthContext,
21588
22632
  buildOAuthErrorUrl,
21589
22633
  cancelAccountDeletionService,
22634
+ cancelDeviceLinkService,
21590
22635
  cancelInvitation,
21591
22636
  carryStepUpVerification,
21592
22637
  changePasswordService,
@@ -21597,7 +22642,9 @@ export {
21597
22642
  configureAuthorizationServer,
21598
22643
  configureDeletion,
21599
22644
  configureDeviceAuth,
22645
+ configureDeviceLink,
21600
22646
  configureOAuthTokenCipher,
22647
+ confirmDeviceLinkService,
21601
22648
  confirmPasswordResetService,
21602
22649
  confirmSignupLinkService,
21603
22650
  confirmTotpEnrolmentService,
@@ -21616,12 +22663,15 @@ export {
21616
22663
  deleteInvitation,
21617
22664
  deleteRole,
21618
22665
  denyDeviceAuthService,
22666
+ denyDeviceLinkService,
21619
22667
  denyOAuth2AuthorizeService,
21620
22668
  deriveCsrfToken,
21621
22669
  describeOAuth2AuthorizeRequestService,
21622
22670
  describeRevokeAllLink,
21623
22671
  deviceAuthorizations,
21624
22672
  deviceAuthorizationsRepository,
22673
+ deviceLinks,
22674
+ deviceLinksRepository,
21625
22675
  disableMfaService,
21626
22676
  disableSessionBindingService,
21627
22677
  enableSessionBindingService,
@@ -21656,6 +22706,8 @@ export {
21656
22706
  getDeletionConfig,
21657
22707
  getDeviceAuthConfig,
21658
22708
  getDeviceAuthInfoService,
22709
+ getDeviceLinkConfig,
22710
+ getDeviceLinkStatusService,
21659
22711
  getDummyPasswordHash,
21660
22712
  getEnabledOAuthProviders,
21661
22713
  getEncryptionKeyring,
@@ -21716,6 +22768,7 @@ export {
21716
22768
  isPkceS256ChallengeShaped,
21717
22769
  isPkceVerifierShaped,
21718
22770
  isSafeReturnPath,
22771
+ issueDeviceLinkService,
21719
22772
  issueOneTimeTokenService,
21720
22773
  issueOpsTokenService,
21721
22774
  kakaoProvider,
@@ -21783,9 +22836,11 @@ export {
21783
22836
  permissionsRepository,
21784
22837
  pkceChallengeFor,
21785
22838
  pollDeviceAuthService,
22839
+ pollDeviceLinkService,
21786
22840
  purgeRevokeAllTokensService,
21787
22841
  purgeStaleOAuth2ClientsService,
21788
22842
  purgeUserService,
22843
+ redeemDeviceLinkService,
21789
22844
  redirectHostOf,
21790
22845
  refreshAccessToken,
21791
22846
  refuseRedirectUriRegistration,