@spfn/auth 0.3.0-beta.27 → 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,74 +8605,104 @@ var init_mfa_enrolment_repository = __esm({
8505
8605
  }
8506
8606
  });
8507
8607
 
8508
- // src/server/lib/device-auth-waiters.ts
8608
+ // src/server/lib/answer-waiters.ts
8509
8609
  import { onAfterCommit } from "@spfn/core/db";
8510
- function announceDeviceAuthAnswered(ids) {
8511
- if (ids.length === 0) {
8512
- return;
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
+ }
8513
8622
  }
8514
- onAfterCommit(() => {
8515
- for (const id26 of ids) {
8516
- wake(id26);
8623
+ function unpark(id27, parked, resolver) {
8624
+ parked.delete(resolver);
8625
+ if (parked.size === 0 && parkedById.get(id27) === parked) {
8626
+ parkedById.delete(id27);
8517
8627
  }
8518
- });
8519
- }
8520
- function wake(id26) {
8521
- const parked = waiters.get(id26);
8522
- if (!parked) {
8523
- return;
8524
8628
  }
8525
- waiters.delete(id26);
8526
- for (const resolve of parked) {
8527
- resolve();
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
+ });
8528
8638
  }
8529
- }
8530
- function waitForDeviceAuthAnswer(id26, timeoutMs, signal) {
8531
- if (signal?.aborted) {
8532
- return Promise.resolve();
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
+ });
8533
8656
  }
8534
- return new Promise((resolve) => {
8535
- const parked = waiters.get(id26) ?? /* @__PURE__ */ new Set();
8536
- const done = () => {
8537
- clearTimeout(timer);
8538
- signal?.removeEventListener("abort", done);
8539
- unpark(id26, parked, done);
8540
- resolve();
8541
- };
8542
- const timer = setTimeout(done, timeoutMs);
8543
- signal?.addEventListener("abort", done, { once: true });
8544
- parked.add(done);
8545
- waiters.set(id26, parked);
8546
- });
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
+ };
8547
8680
  }
8548
- function unpark(id26, parked, resolver) {
8549
- parked.delete(resolver);
8550
- if (parked.size === 0 && waiters.get(id26) === parked) {
8551
- waiters.delete(id26);
8681
+ var init_answer_waiters = __esm({
8682
+ "src/server/lib/answer-waiters.ts"() {
8683
+ "use strict";
8552
8684
  }
8685
+ });
8686
+
8687
+ // src/server/lib/device-auth-waiters.ts
8688
+ function announceDeviceAuthAnswered(ids) {
8689
+ waiters.announce(ids);
8553
8690
  }
8554
- function waitingOnDeviceAuth(id26) {
8555
- return waiting.get(id26) ?? 0;
8691
+ function waitForDeviceAuthAnswer(id27, timeoutMs, signal) {
8692
+ return waiters.wait(id27, timeoutMs, signal);
8556
8693
  }
8557
- async function holdDeviceAuthWait(id26, wait) {
8558
- waiting.set(id26, waitingOnDeviceAuth(id26) + 1);
8559
- try {
8560
- return await wait();
8561
- } finally {
8562
- const left = waitingOnDeviceAuth(id26) - 1;
8563
- if (left > 0) {
8564
- waiting.set(id26, left);
8565
- } else {
8566
- waiting.delete(id26);
8567
- }
8568
- }
8694
+ function waitingOnDeviceAuth(id27) {
8695
+ return waiters.waiting(id27);
8696
+ }
8697
+ function holdDeviceAuthWait(id27, wait) {
8698
+ return waiters.hold(id27, wait);
8569
8699
  }
8570
- var waiters, waiting;
8700
+ var waiters;
8571
8701
  var init_device_auth_waiters = __esm({
8572
8702
  "src/server/lib/device-auth-waiters.ts"() {
8573
8703
  "use strict";
8574
- waiters = /* @__PURE__ */ new Map();
8575
- waiting = /* @__PURE__ */ new Map();
8704
+ init_answer_waiters();
8705
+ waiters = createAnswerWaiters();
8576
8706
  }
8577
8707
  });
8578
8708
 
@@ -8649,10 +8779,10 @@ var init_device_authorizations_repository = __esm({
8649
8779
  *
8650
8780
  * @returns the updated row, or null if it was no longer pending, or expired
8651
8781
  */
8652
- async approve(id26, userId) {
8782
+ async approve(id27, userId) {
8653
8783
  const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
8654
8784
  and14(
8655
- eq14(deviceAuthorizations.id, id26),
8785
+ eq14(deviceAuthorizations.id, id27),
8656
8786
  eq14(deviceAuthorizations.status, "pending"),
8657
8787
  notExpired()
8658
8788
  )
@@ -8666,10 +8796,10 @@ var init_device_authorizations_repository = __esm({
8666
8796
  *
8667
8797
  * @returns the updated row, or null if it was no longer pending, or expired
8668
8798
  */
8669
- async deny(id26) {
8799
+ async deny(id27) {
8670
8800
  const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8671
8801
  and14(
8672
- eq14(deviceAuthorizations.id, id26),
8802
+ eq14(deviceAuthorizations.id, id27),
8673
8803
  eq14(deviceAuthorizations.status, "pending"),
8674
8804
  notExpired()
8675
8805
  )
@@ -8744,27 +8874,243 @@ var init_device_authorizations_repository = __esm({
8744
8874
  }
8745
8875
  });
8746
8876
 
8747
- // 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
8748
8900
  import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
8749
- 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";
8750
9096
  var RolesRepository, rolesRepository;
8751
9097
  var init_roles_repository = __esm({
8752
9098
  "src/server/repositories/roles.repository.ts"() {
8753
9099
  "use strict";
8754
9100
  init_roles();
8755
- RolesRepository = class extends BaseRepository15 {
9101
+ RolesRepository = class extends BaseRepository16 {
8756
9102
  /**
8757
9103
  * ID로 역할 조회
8758
9104
  */
8759
- async findById(id26) {
8760
- 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);
8761
9107
  return result[0] ?? null;
8762
9108
  }
8763
9109
  /**
8764
9110
  * Name으로 역할 조회
8765
9111
  */
8766
9112
  async findByName(name) {
8767
- 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);
8768
9114
  return result[0] ?? null;
8769
9115
  }
8770
9116
  /**
@@ -8777,7 +9123,7 @@ var init_roles_repository = __esm({
8777
9123
  * 활성 역할만 조회
8778
9124
  */
8779
9125
  async findActive() {
8780
- 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));
8781
9127
  }
8782
9128
  /**
8783
9129
  * 역할 생성
@@ -8788,15 +9134,15 @@ var init_roles_repository = __esm({
8788
9134
  /**
8789
9135
  * 역할 업데이트
8790
9136
  */
8791
- async updateById(id26, data) {
8792
- 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();
8793
9139
  return result[0] ?? null;
8794
9140
  }
8795
9141
  /**
8796
9142
  * 역할 삭제
8797
9143
  */
8798
- async deleteById(id26) {
8799
- 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();
8800
9146
  return result[0] ?? null;
8801
9147
  }
8802
9148
  };
@@ -8805,26 +9151,26 @@ var init_roles_repository = __esm({
8805
9151
  });
8806
9152
 
8807
9153
  // src/server/repositories/permissions.repository.ts
8808
- import { BaseRepository as BaseRepository16 } from "@spfn/core/db";
8809
- 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";
8810
9156
  var PermissionsRepository, permissionsRepository;
8811
9157
  var init_permissions_repository = __esm({
8812
9158
  "src/server/repositories/permissions.repository.ts"() {
8813
9159
  "use strict";
8814
9160
  init_permissions();
8815
- PermissionsRepository = class extends BaseRepository16 {
9161
+ PermissionsRepository = class extends BaseRepository17 {
8816
9162
  /**
8817
9163
  * ID로 권한 조회
8818
9164
  */
8819
- async findById(id26) {
8820
- 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);
8821
9167
  return result[0] ?? null;
8822
9168
  }
8823
9169
  /**
8824
9170
  * Name으로 권한 조회
8825
9171
  */
8826
9172
  async findByName(name) {
8827
- 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);
8828
9174
  return result[0] ?? null;
8829
9175
  }
8830
9176
  /**
@@ -8832,7 +9178,7 @@ var init_permissions_repository = __esm({
8832
9178
  */
8833
9179
  async findByNames(names) {
8834
9180
  if (names.length === 0) return [];
8835
- return this.readDb.select().from(permissions).where(inArray3(permissions.name, names));
9181
+ return this.readDb.select().from(permissions).where(inArray4(permissions.name, names));
8836
9182
  }
8837
9183
  /**
8838
9184
  * 모든 권한 조회
@@ -8844,13 +9190,13 @@ var init_permissions_repository = __esm({
8844
9190
  * 활성 권한만 조회
8845
9191
  */
8846
9192
  async findActive() {
8847
- 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));
8848
9194
  }
8849
9195
  /**
8850
9196
  * 카테고리별 권한 조회
8851
9197
  */
8852
9198
  async findByCategory(category) {
8853
- 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));
8854
9200
  }
8855
9201
  /**
8856
9202
  * 권한 생성
@@ -8868,15 +9214,15 @@ var init_permissions_repository = __esm({
8868
9214
  /**
8869
9215
  * 권한 업데이트
8870
9216
  */
8871
- async updateById(id26, data) {
8872
- 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();
8873
9219
  return result[0] ?? null;
8874
9220
  }
8875
9221
  /**
8876
9222
  * 권한 삭제
8877
9223
  */
8878
- async deleteById(id26) {
8879
- 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();
8880
9226
  return result[0] ?? null;
8881
9227
  }
8882
9228
  };
@@ -8885,25 +9231,25 @@ var init_permissions_repository = __esm({
8885
9231
  });
8886
9232
 
8887
9233
  // src/server/repositories/role-permissions.repository.ts
8888
- import { BaseRepository as BaseRepository17 } from "@spfn/core/db";
8889
- 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";
8890
9236
  var RolePermissionsRepository, rolePermissionsRepository;
8891
9237
  var init_role_permissions_repository = __esm({
8892
9238
  "src/server/repositories/role-permissions.repository.ts"() {
8893
9239
  "use strict";
8894
9240
  init_role_permissions();
8895
- RolePermissionsRepository = class extends BaseRepository17 {
9241
+ RolePermissionsRepository = class extends BaseRepository18 {
8896
9242
  /**
8897
9243
  * 역할 ID로 모든 권한 조회
8898
9244
  */
8899
9245
  async findByRoleId(roleId) {
8900
- return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.roleId, roleId));
9246
+ return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.roleId, roleId));
8901
9247
  }
8902
9248
  /**
8903
9249
  * 권한 ID로 모든 역할 조회
8904
9250
  */
8905
9251
  async findByPermissionId(permissionId) {
8906
- return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.permissionId, permissionId));
9252
+ return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.permissionId, permissionId));
8907
9253
  }
8908
9254
  /**
8909
9255
  * 역할-권한 매핑 생성
@@ -8923,9 +9269,9 @@ var init_role_permissions_repository = __esm({
8923
9269
  */
8924
9270
  async deleteByRoleIdAndPermissionId(roleId, permissionId) {
8925
9271
  const result = await this.db.delete(rolePermissions).where(
8926
- and15(
8927
- eq17(rolePermissions.roleId, roleId),
8928
- eq17(rolePermissions.permissionId, permissionId)
9272
+ and16(
9273
+ eq18(rolePermissions.roleId, roleId),
9274
+ eq18(rolePermissions.permissionId, permissionId)
8929
9275
  )
8930
9276
  ).returning();
8931
9277
  return result[0] ?? null;
@@ -8934,7 +9280,7 @@ var init_role_permissions_repository = __esm({
8934
9280
  * 역할의 모든 권한 매핑 삭제
8935
9281
  */
8936
9282
  async deleteByRoleId(roleId) {
8937
- 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();
8938
9284
  return result.length;
8939
9285
  }
8940
9286
  /**
@@ -8955,19 +9301,19 @@ var init_role_permissions_repository = __esm({
8955
9301
  });
8956
9302
 
8957
9303
  // src/server/repositories/user-permissions.repository.ts
8958
- import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
8959
- 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";
8960
9306
  var UserPermissionsRepository, userPermissionsRepository;
8961
9307
  var init_user_permissions_repository = __esm({
8962
9308
  "src/server/repositories/user-permissions.repository.ts"() {
8963
9309
  "use strict";
8964
9310
  init_user_permissions();
8965
- UserPermissionsRepository = class extends BaseRepository18 {
9311
+ UserPermissionsRepository = class extends BaseRepository19 {
8966
9312
  /**
8967
9313
  * 사용자 ID로 모든 권한 오버라이드 조회
8968
9314
  */
8969
9315
  async findByUserId(userId) {
8970
- return this.readDb.select().from(userPermissions).where(eq18(userPermissions.userId, userId));
9316
+ return this.readDb.select().from(userPermissions).where(eq19(userPermissions.userId, userId));
8971
9317
  }
8972
9318
  /**
8973
9319
  * 사용자 ID로 유효한 권한 오버라이드만 조회
@@ -8976,11 +9322,11 @@ var init_user_permissions_repository = __esm({
8976
9322
  async findValidByUserId(userId) {
8977
9323
  const now = /* @__PURE__ */ new Date();
8978
9324
  return this.readDb.select().from(userPermissions).where(
8979
- and16(
8980
- eq18(userPermissions.userId, userId),
8981
- or4(
8982
- isNull12(userPermissions.expiresAt),
8983
- gt8(userPermissions.expiresAt, now)
9325
+ and17(
9326
+ eq19(userPermissions.userId, userId),
9327
+ or5(
9328
+ isNull13(userPermissions.expiresAt),
9329
+ gt9(userPermissions.expiresAt, now)
8984
9330
  )
8985
9331
  )
8986
9332
  );
@@ -8990,9 +9336,9 @@ var init_user_permissions_repository = __esm({
8990
9336
  */
8991
9337
  async findByUserIdAndPermissionId(userId, permissionId) {
8992
9338
  const result = await this.readDb.select().from(userPermissions).where(
8993
- and16(
8994
- eq18(userPermissions.userId, userId),
8995
- eq18(userPermissions.permissionId, permissionId)
9339
+ and17(
9340
+ eq19(userPermissions.userId, userId),
9341
+ eq19(userPermissions.permissionId, permissionId)
8996
9342
  )
8997
9343
  ).limit(1);
8998
9344
  return result[0] ?? null;
@@ -9006,8 +9352,8 @@ var init_user_permissions_repository = __esm({
9006
9352
  /**
9007
9353
  * 사용자 권한 오버라이드 업데이트
9008
9354
  */
9009
- async updateById(id26, data) {
9010
- 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();
9011
9357
  return result[0] ?? null;
9012
9358
  }
9013
9359
  /**
@@ -9015,9 +9361,9 @@ var init_user_permissions_repository = __esm({
9015
9361
  */
9016
9362
  async deleteByUserIdAndPermissionId(userId, permissionId) {
9017
9363
  const result = await this.db.delete(userPermissions).where(
9018
- and16(
9019
- eq18(userPermissions.userId, userId),
9020
- eq18(userPermissions.permissionId, permissionId)
9364
+ and17(
9365
+ eq19(userPermissions.userId, userId),
9366
+ eq19(userPermissions.permissionId, permissionId)
9021
9367
  )
9022
9368
  ).returning();
9023
9369
  return result[0] ?? null;
@@ -9026,7 +9372,7 @@ var init_user_permissions_repository = __esm({
9026
9372
  * 사용자의 모든 권한 오버라이드 삭제
9027
9373
  */
9028
9374
  async deleteByUserId(userId) {
9029
- 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();
9030
9376
  return result.length;
9031
9377
  }
9032
9378
  /**
@@ -9035,7 +9381,7 @@ var init_user_permissions_repository = __esm({
9035
9381
  async deleteExpired() {
9036
9382
  const now = /* @__PURE__ */ new Date();
9037
9383
  const result = await this.db.delete(userPermissions).where(
9038
- and16(
9384
+ and17(
9039
9385
  isNotNull3(userPermissions.expiresAt),
9040
9386
  lt6(userPermissions.expiresAt, now)
9041
9387
  )
@@ -9048,33 +9394,33 @@ var init_user_permissions_repository = __esm({
9048
9394
  });
9049
9395
 
9050
9396
  // src/server/repositories/user-profiles.repository.ts
9051
- import { BaseRepository as BaseRepository19 } from "@spfn/core/db";
9052
- import { eq as eq19 } from "drizzle-orm";
9397
+ import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
9398
+ import { eq as eq20 } from "drizzle-orm";
9053
9399
  var UserProfilesRepository, userProfilesRepository;
9054
9400
  var init_user_profiles_repository = __esm({
9055
9401
  "src/server/repositories/user-profiles.repository.ts"() {
9056
9402
  "use strict";
9057
9403
  init_user_profiles();
9058
- UserProfilesRepository = class extends BaseRepository19 {
9404
+ UserProfilesRepository = class extends BaseRepository20 {
9059
9405
  /**
9060
9406
  * ID로 프로필 조회
9061
9407
  */
9062
- async findById(id26) {
9063
- 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);
9064
9410
  return result[0] ?? null;
9065
9411
  }
9066
9412
  /**
9067
9413
  * User ID로 locale만 조회 (경량)
9068
9414
  */
9069
9415
  async findLocaleByUserId(userId) {
9070
- 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);
9071
9417
  return result[0]?.locale || "en";
9072
9418
  }
9073
9419
  /**
9074
9420
  * User ID로 프로필 조회
9075
9421
  */
9076
9422
  async findByUserId(userId) {
9077
- 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);
9078
9424
  return result[0] ?? null;
9079
9425
  }
9080
9426
  /**
@@ -9086,29 +9432,29 @@ var init_user_profiles_repository = __esm({
9086
9432
  /**
9087
9433
  * 프로필 업데이트 (by ID)
9088
9434
  */
9089
- async updateById(id26, data) {
9090
- 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();
9091
9437
  return result[0] ?? null;
9092
9438
  }
9093
9439
  /**
9094
9440
  * 프로필 업데이트 (by User ID)
9095
9441
  */
9096
9442
  async updateByUserId(userId, data) {
9097
- 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();
9098
9444
  return result[0] ?? null;
9099
9445
  }
9100
9446
  /**
9101
9447
  * 프로필 삭제 (by ID)
9102
9448
  */
9103
- async deleteById(id26) {
9104
- 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();
9105
9451
  return result[0] ?? null;
9106
9452
  }
9107
9453
  /**
9108
9454
  * 프로필 삭제 (by User ID)
9109
9455
  */
9110
9456
  async deleteByUserId(userId) {
9111
- 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();
9112
9458
  return result[0] ?? null;
9113
9459
  }
9114
9460
  /**
@@ -9150,7 +9496,7 @@ var init_user_profiles_repository = __esm({
9150
9496
  metadata: userProfiles.metadata,
9151
9497
  createdAt: userProfiles.createdAt,
9152
9498
  updatedAt: userProfiles.updatedAt
9153
- }).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);
9154
9500
  if (!profile) {
9155
9501
  return null;
9156
9502
  }
@@ -9178,8 +9524,8 @@ var init_user_profiles_repository = __esm({
9178
9524
  });
9179
9525
 
9180
9526
  // src/server/repositories/invitations.repository.ts
9181
- import { eq as eq20, and as and17, lt as lt7, desc as desc3, sql as sql10 } from "drizzle-orm";
9182
- 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";
9183
9529
  var InvitationsRepository, invitationsRepository;
9184
9530
  var init_invitations_repository = __esm({
9185
9531
  "src/server/repositories/invitations.repository.ts"() {
@@ -9188,19 +9534,19 @@ var init_invitations_repository = __esm({
9188
9534
  init_roles();
9189
9535
  init_user_invitations();
9190
9536
  init_email();
9191
- InvitationsRepository = class extends BaseRepository20 {
9537
+ InvitationsRepository = class extends BaseRepository21 {
9192
9538
  /**
9193
9539
  * ID로 초대 조회
9194
9540
  */
9195
- async findById(id26) {
9196
- 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);
9197
9543
  return result[0] ?? null;
9198
9544
  }
9199
9545
  /**
9200
9546
  * Token으로 초대 조회
9201
9547
  */
9202
9548
  async findByToken(token) {
9203
- 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);
9204
9550
  return result[0] ?? null;
9205
9551
  }
9206
9552
  /**
@@ -9208,9 +9554,9 @@ var init_invitations_repository = __esm({
9208
9554
  */
9209
9555
  async findPendingByEmail(email) {
9210
9556
  const result = await this.readDb.select().from(userInvitations).where(
9211
- and17(
9212
- eq20(userInvitations.email, normalizeEmail(email)),
9213
- eq20(userInvitations.status, "pending")
9557
+ and18(
9558
+ eq21(userInvitations.email, normalizeEmail(email)),
9559
+ eq21(userInvitations.status, "pending")
9214
9560
  )
9215
9561
  ).limit(1);
9216
9562
  return result[0] ?? null;
@@ -9219,13 +9565,13 @@ var init_invitations_repository = __esm({
9219
9565
  * 초대자 ID로 모든 초대 조회
9220
9566
  */
9221
9567
  async findByInvitedBy(invitedBy) {
9222
- return this.readDb.select().from(userInvitations).where(eq20(userInvitations.invitedBy, invitedBy));
9568
+ return this.readDb.select().from(userInvitations).where(eq21(userInvitations.invitedBy, invitedBy));
9223
9569
  }
9224
9570
  /**
9225
9571
  * 상태별 초대 조회
9226
9572
  */
9227
9573
  async findByStatus(status) {
9228
- return this.readDb.select().from(userInvitations).where(eq20(userInvitations.status, status));
9574
+ return this.readDb.select().from(userInvitations).where(eq21(userInvitations.status, status));
9229
9575
  }
9230
9576
  /**
9231
9577
  * 초대 생성
@@ -9236,7 +9582,7 @@ var init_invitations_repository = __esm({
9236
9582
  /**
9237
9583
  * 초대 상태 업데이트
9238
9584
  */
9239
- async updateStatus(id26, status, timestamp2) {
9585
+ async updateStatus(id27, status, timestamp2) {
9240
9586
  const updates = {
9241
9587
  status
9242
9588
  };
@@ -9247,14 +9593,14 @@ var init_invitations_repository = __esm({
9247
9593
  updates.cancelledAt = timestamp2;
9248
9594
  }
9249
9595
  }
9250
- 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();
9251
9597
  return result[0] ?? null;
9252
9598
  }
9253
9599
  /**
9254
9600
  * 초대 삭제
9255
9601
  */
9256
- async deleteById(id26) {
9257
- 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();
9258
9604
  return result[0] ?? null;
9259
9605
  }
9260
9606
  /**
@@ -9263,8 +9609,8 @@ var init_invitations_repository = __esm({
9263
9609
  async updateExpiredInvitations() {
9264
9610
  const now = /* @__PURE__ */ new Date();
9265
9611
  const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
9266
- and17(
9267
- eq20(userInvitations.status, "pending"),
9612
+ and18(
9613
+ eq21(userInvitations.status, "pending"),
9268
9614
  lt7(userInvitations.expiresAt, now)
9269
9615
  )
9270
9616
  ).returning();
@@ -9296,7 +9642,7 @@ var init_invitations_repository = __esm({
9296
9642
  id: users.id,
9297
9643
  email: users.email
9298
9644
  }
9299
- }).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);
9300
9646
  return result[0] ?? null;
9301
9647
  }
9302
9648
  /**
@@ -9307,13 +9653,13 @@ var init_invitations_repository = __esm({
9307
9653
  const offset = (page - 1) * limit;
9308
9654
  const conditions = [];
9309
9655
  if (status) {
9310
- conditions.push(eq20(userInvitations.status, status));
9656
+ conditions.push(eq21(userInvitations.status, status));
9311
9657
  }
9312
9658
  if (invitedBy) {
9313
- conditions.push(eq20(userInvitations.invitedBy, invitedBy));
9659
+ conditions.push(eq21(userInvitations.invitedBy, invitedBy));
9314
9660
  }
9315
- const whereClause = conditions.length > 0 ? and17(...conditions) : void 0;
9316
- 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);
9317
9663
  const total = Number(countResult[0]?.count || 0);
9318
9664
  const results = await this.readDb.select({
9319
9665
  id: userInvitations.id,
@@ -9337,7 +9683,7 @@ var init_invitations_repository = __esm({
9337
9683
  id: users.id,
9338
9684
  email: users.email
9339
9685
  }
9340
- }).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);
9341
9687
  return {
9342
9688
  invitations: results,
9343
9689
  total,
@@ -9349,31 +9695,31 @@ var init_invitations_repository = __esm({
9349
9695
  /**
9350
9696
  * 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
9351
9697
  */
9352
- async updateById(id26, data) {
9698
+ async updateById(id27, data) {
9353
9699
  const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
9354
- 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();
9355
9701
  return result[0] ?? null;
9356
9702
  }
9357
9703
  /**
9358
9704
  * 초대 재전송 (status와 expiresAt 동시 업데이트)
9359
9705
  */
9360
- async resend(id26, newExpiresAt) {
9706
+ async resend(id27, newExpiresAt) {
9361
9707
  const result = await this.db.update(userInvitations).set({
9362
9708
  status: "pending",
9363
9709
  expiresAt: newExpiresAt
9364
- }).where(eq20(userInvitations.id, id26)).returning();
9710
+ }).where(eq21(userInvitations.id, id27)).returning();
9365
9711
  return result[0] ?? null;
9366
9712
  }
9367
9713
  /**
9368
9714
  * 초대 취소 (status, metadata 동시 업데이트)
9369
9715
  */
9370
- async cancel(id26, cancelledBy, reason, currentMetadata) {
9716
+ async cancel(id27, cancelledBy, reason, currentMetadata) {
9371
9717
  const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
9372
9718
  const result = await this.db.update(userInvitations).set({
9373
9719
  status: "cancelled",
9374
9720
  cancelledAt: /* @__PURE__ */ new Date(),
9375
9721
  metadata: newMetadata
9376
- }).where(eq20(userInvitations.id, id26)).returning();
9722
+ }).where(eq21(userInvitations.id, id27)).returning();
9377
9723
  return result[0] ?? null;
9378
9724
  }
9379
9725
  };
@@ -10213,15 +10559,15 @@ var init_token_cipher = __esm({
10213
10559
  });
10214
10560
 
10215
10561
  // src/server/repositories/social-accounts.repository.ts
10216
- import { eq as eq21, and as and18 } from "drizzle-orm";
10217
- 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";
10218
10564
  var SocialAccountsRepository, socialAccountsRepository;
10219
10565
  var init_social_accounts_repository = __esm({
10220
10566
  "src/server/repositories/social-accounts.repository.ts"() {
10221
10567
  "use strict";
10222
10568
  init_entities();
10223
10569
  init_token_cipher();
10224
- SocialAccountsRepository = class extends BaseRepository21 {
10570
+ SocialAccountsRepository = class extends BaseRepository22 {
10225
10571
  /**
10226
10572
  * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
10227
10573
  *
@@ -10249,10 +10595,10 @@ var init_social_accounts_repository = __esm({
10249
10595
  if (refresh?.needsRotation) {
10250
10596
  heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
10251
10597
  }
10252
- await this.db.update(userSocialAccounts).set(heal).where(and18(
10253
- eq21(userSocialAccounts.id, account.id),
10254
- access?.needsRotation && account.accessToken !== null ? eq21(userSocialAccounts.accessToken, account.accessToken) : void 0,
10255
- 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
10256
10602
  ));
10257
10603
  } catch {
10258
10604
  }
@@ -10269,9 +10615,9 @@ var init_social_accounts_repository = __esm({
10269
10615
  */
10270
10616
  async findByProviderAndProviderId(provider, providerUserId) {
10271
10617
  const result = await this.readDb.select().from(userSocialAccounts).where(
10272
- and18(
10273
- eq21(userSocialAccounts.provider, provider),
10274
- eq21(userSocialAccounts.providerUserId, providerUserId)
10618
+ and19(
10619
+ eq22(userSocialAccounts.provider, provider),
10620
+ eq22(userSocialAccounts.providerUserId, providerUserId)
10275
10621
  )
10276
10622
  ).limit(1);
10277
10623
  return this.decryptAccount(result[0] ?? null);
@@ -10281,7 +10627,7 @@ var init_social_accounts_repository = __esm({
10281
10627
  * Read replica 사용
10282
10628
  */
10283
10629
  async findByUserId(userId) {
10284
- 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));
10285
10631
  return Promise.all(result.map((account) => this.decryptAccount(account)));
10286
10632
  }
10287
10633
  /**
@@ -10290,9 +10636,9 @@ var init_social_accounts_repository = __esm({
10290
10636
  */
10291
10637
  async findByUserIdAndProvider(userId, provider) {
10292
10638
  const result = await this.readDb.select().from(userSocialAccounts).where(
10293
- and18(
10294
- eq21(userSocialAccounts.userId, userId),
10295
- eq21(userSocialAccounts.provider, provider)
10639
+ and19(
10640
+ eq22(userSocialAccounts.userId, userId),
10641
+ eq22(userSocialAccounts.provider, provider)
10296
10642
  )
10297
10643
  ).limit(1);
10298
10644
  return this.decryptAccount(result[0] ?? null);
@@ -10318,11 +10664,11 @@ var init_social_accounts_repository = __esm({
10318
10664
  * 토큰 정보 업데이트
10319
10665
  * Write primary 사용
10320
10666
  */
10321
- async updateTokens(id26, data) {
10667
+ async updateTokens(id27, data) {
10322
10668
  const accounts = await this.db.select({
10323
10669
  provider: userSocialAccounts.provider,
10324
10670
  providerUserId: userSocialAccounts.providerUserId
10325
- }).from(userSocialAccounts).where(eq21(userSocialAccounts.id, id26)).limit(1);
10671
+ }).from(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).limit(1);
10326
10672
  const account = accounts[0];
10327
10673
  if (!account) {
10328
10674
  return null;
@@ -10336,15 +10682,15 @@ var init_social_accounts_repository = __esm({
10336
10682
  ...data,
10337
10683
  accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
10338
10684
  refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
10339
- }).where(eq21(userSocialAccounts.id, id26)).returning();
10685
+ }).where(eq22(userSocialAccounts.id, id27)).returning();
10340
10686
  return this.decryptAccount(result[0] ?? null);
10341
10687
  }
10342
10688
  /**
10343
10689
  * 소셜 계정 삭제
10344
10690
  * Write primary 사용
10345
10691
  */
10346
- async deleteById(id26) {
10347
- 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();
10348
10694
  return result[0] ?? null;
10349
10695
  }
10350
10696
  /**
@@ -10353,9 +10699,9 @@ var init_social_accounts_repository = __esm({
10353
10699
  */
10354
10700
  async deleteByUserIdAndProvider(userId, provider) {
10355
10701
  const result = await this.db.delete(userSocialAccounts).where(
10356
- and18(
10357
- eq21(userSocialAccounts.userId, userId),
10358
- eq21(userSocialAccounts.provider, provider)
10702
+ and19(
10703
+ eq22(userSocialAccounts.userId, userId),
10704
+ eq22(userSocialAccounts.provider, provider)
10359
10705
  )
10360
10706
  ).returning();
10361
10707
  return result[0] ?? null;
@@ -10368,7 +10714,7 @@ var init_social_accounts_repository = __esm({
10368
10714
  * Write primary 사용
10369
10715
  */
10370
10716
  async deleteAllByUserId(userId) {
10371
- 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();
10372
10718
  return result.length;
10373
10719
  }
10374
10720
  };
@@ -10377,19 +10723,19 @@ var init_social_accounts_repository = __esm({
10377
10723
  });
10378
10724
 
10379
10725
  // src/server/repositories/auth-metadata.repository.ts
10380
- import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
10381
- import { eq as eq22 } from "drizzle-orm";
10726
+ import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
10727
+ import { eq as eq23 } from "drizzle-orm";
10382
10728
  var AuthMetadataRepository, authMetadataRepository;
10383
10729
  var init_auth_metadata_repository = __esm({
10384
10730
  "src/server/repositories/auth-metadata.repository.ts"() {
10385
10731
  "use strict";
10386
10732
  init_auth_metadata();
10387
- AuthMetadataRepository = class extends BaseRepository22 {
10733
+ AuthMetadataRepository = class extends BaseRepository23 {
10388
10734
  /**
10389
10735
  * 키로 값 조회
10390
10736
  */
10391
10737
  async get(key) {
10392
- 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);
10393
10739
  return result[0]?.value ?? null;
10394
10740
  }
10395
10741
  /**
@@ -10412,20 +10758,20 @@ var init_auth_metadata_repository = __esm({
10412
10758
  });
10413
10759
 
10414
10760
  // src/server/repositories/account-deletion-requests.repository.ts
10415
- import { eq as eq23, and as and19, lte as lte2 } from "drizzle-orm";
10416
- 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";
10417
10763
  var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
10418
10764
  var init_account_deletion_requests_repository = __esm({
10419
10765
  "src/server/repositories/account-deletion-requests.repository.ts"() {
10420
10766
  "use strict";
10421
10767
  init_account_deletion_requests();
10422
- AccountDeletionRequestsRepository = class extends BaseRepository23 {
10768
+ AccountDeletionRequestsRepository = class extends BaseRepository24 {
10423
10769
  /**
10424
10770
  * ID로 요청 조회
10425
10771
  * Read replica 사용
10426
10772
  */
10427
- async findById(id26) {
10428
- 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);
10429
10775
  return result[0] ?? null;
10430
10776
  }
10431
10777
  /**
@@ -10434,9 +10780,9 @@ var init_account_deletion_requests_repository = __esm({
10434
10780
  */
10435
10781
  async findPendingByUserId(userId) {
10436
10782
  const result = await this.readDb.select().from(accountDeletionRequests).where(
10437
- and19(
10438
- eq23(accountDeletionRequests.userId, userId),
10439
- eq23(accountDeletionRequests.status, "pending")
10783
+ and20(
10784
+ eq24(accountDeletionRequests.userId, userId),
10785
+ eq24(accountDeletionRequests.status, "pending")
10440
10786
  )
10441
10787
  ).limit(1);
10442
10788
  return result[0] ?? null;
@@ -10450,9 +10796,9 @@ var init_account_deletion_requests_repository = __esm({
10450
10796
  */
10451
10797
  async findPendingByUserIdOnPrimary(userId) {
10452
10798
  const result = await this.db.select().from(accountDeletionRequests).where(
10453
- and19(
10454
- eq23(accountDeletionRequests.userId, userId),
10455
- eq23(accountDeletionRequests.status, "pending")
10799
+ and20(
10800
+ eq24(accountDeletionRequests.userId, userId),
10801
+ eq24(accountDeletionRequests.status, "pending")
10456
10802
  )
10457
10803
  ).limit(1);
10458
10804
  return result[0] ?? null;
@@ -10463,8 +10809,8 @@ var init_account_deletion_requests_repository = __esm({
10463
10809
  */
10464
10810
  async findDueForPurge(now) {
10465
10811
  return this.readDb.select().from(accountDeletionRequests).where(
10466
- and19(
10467
- eq23(accountDeletionRequests.status, "pending"),
10812
+ and20(
10813
+ eq24(accountDeletionRequests.status, "pending"),
10468
10814
  lte2(accountDeletionRequests.purgeScheduledAt, now)
10469
10815
  )
10470
10816
  );
@@ -10484,14 +10830,14 @@ var init_account_deletion_requests_repository = __esm({
10484
10830
  * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
10485
10831
  * Write primary 사용
10486
10832
  */
10487
- async markCancelled(id26) {
10833
+ async markCancelled(id27) {
10488
10834
  const result = await this.db.update(accountDeletionRequests).set({
10489
10835
  status: "cancelled",
10490
10836
  cancelledAt: /* @__PURE__ */ new Date()
10491
10837
  }).where(
10492
- and19(
10493
- eq23(accountDeletionRequests.id, id26),
10494
- eq23(accountDeletionRequests.status, "pending")
10838
+ and20(
10839
+ eq24(accountDeletionRequests.id, id27),
10840
+ eq24(accountDeletionRequests.status, "pending")
10495
10841
  )
10496
10842
  ).returning();
10497
10843
  return result[0] ?? null;
@@ -10506,15 +10852,15 @@ var init_account_deletion_requests_repository = __esm({
10506
10852
  * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
10507
10853
  * Write primary 사용
10508
10854
  */
10509
- async markCompleted(id26, purgeStrategy) {
10855
+ async markCompleted(id27, purgeStrategy) {
10510
10856
  const result = await this.db.update(accountDeletionRequests).set({
10511
10857
  status: "completed",
10512
10858
  completedAt: /* @__PURE__ */ new Date(),
10513
10859
  purgeStrategy
10514
10860
  }).where(
10515
- and19(
10516
- eq23(accountDeletionRequests.id, id26),
10517
- eq23(accountDeletionRequests.status, "pending")
10861
+ and20(
10862
+ eq24(accountDeletionRequests.id, id27),
10863
+ eq24(accountDeletionRequests.status, "pending")
10518
10864
  )
10519
10865
  ).returning();
10520
10866
  return result[0] ?? null;
@@ -10525,14 +10871,14 @@ var init_account_deletion_requests_repository = __esm({
10525
10871
  });
10526
10872
 
10527
10873
  // src/server/repositories/ops-tokens.repository.ts
10528
- import { and as and20, desc as desc4, eq as eq24, isNull as isNull13 } from "drizzle-orm";
10529
- 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";
10530
10876
  var OpsTokensRepository, opsTokensRepository;
10531
10877
  var init_ops_tokens_repository = __esm({
10532
10878
  "src/server/repositories/ops-tokens.repository.ts"() {
10533
10879
  "use strict";
10534
10880
  init_ops_tokens();
10535
- OpsTokensRepository = class extends BaseRepository24 {
10881
+ OpsTokensRepository = class extends BaseRepository25 {
10536
10882
  /**
10537
10883
  * Lookup by the secret's hash — the verification path.
10538
10884
  *
@@ -10542,7 +10888,7 @@ var init_ops_tokens_repository = __esm({
10542
10888
  * and revocation is documented as taking effect immediately.
10543
10889
  */
10544
10890
  async findByTokenHash(tokenHash) {
10545
- 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);
10546
10892
  return result[0] ?? null;
10547
10893
  }
10548
10894
  async create(data) {
@@ -10557,13 +10903,13 @@ var init_ops_tokens_repository = __esm({
10557
10903
  * token is already revoked — the first revocation's timestamp is never
10558
10904
  * overwritten.
10559
10905
  */
10560
- async revokeById(id26) {
10561
- 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();
10562
10908
  return result[0] ?? null;
10563
10909
  }
10564
10910
  /** Fire-and-forget from the verification path. */
10565
- async updateLastUsedById(id26) {
10566
- 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));
10567
10913
  }
10568
10914
  };
10569
10915
  opsTokensRepository = new OpsTokensRepository();
@@ -10571,15 +10917,15 @@ var init_ops_tokens_repository = __esm({
10571
10917
  });
10572
10918
 
10573
10919
  // src/server/repositories/oauth2-clients.repository.ts
10574
- import { and as and21, eq as eq25, gt as gt9, lt as lt8, sql as sql11 } from "drizzle-orm";
10575
- 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";
10576
10922
  var OAuth2ClientsRepository, oauth2ClientsRepository;
10577
10923
  var init_oauth2_clients_repository = __esm({
10578
10924
  "src/server/repositories/oauth2-clients.repository.ts"() {
10579
10925
  "use strict";
10580
10926
  init_oauth2_clients();
10581
10927
  init_oauth2_grants();
10582
- OAuth2ClientsRepository = class extends BaseRepository25 {
10928
+ OAuth2ClientsRepository = class extends BaseRepository26 {
10583
10929
  async create(data) {
10584
10930
  const result = await this.db.insert(oauth2Clients).values(data).returning();
10585
10931
  return result[0];
@@ -10593,12 +10939,12 @@ var init_oauth2_clients_repository = __esm({
10593
10939
  * at the one moment a CLI is being connected.
10594
10940
  */
10595
10941
  async findByClientId(clientId) {
10596
- 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);
10597
10943
  return result[0] ?? null;
10598
10944
  }
10599
10945
  /** Fire-and-forget from the token-issuing path. */
10600
- async updateLastUsedById(id26) {
10601
- 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));
10602
10948
  }
10603
10949
  /**
10604
10950
  * Register a client unless this address is already at its standing cap.
@@ -10620,7 +10966,7 @@ var init_oauth2_clients_repository = __esm({
10620
10966
  */
10621
10967
  async createWithinStandingCap(data, limit) {
10622
10968
  return await runInTransaction(async () => {
10623
- 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}))`);
10624
10970
  const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
10625
10971
  return standing < limit.max ? await this.create(data) : null;
10626
10972
  });
@@ -10642,11 +10988,11 @@ var init_oauth2_clients_repository = __esm({
10642
10988
  * count exists to see.
10643
10989
  */
10644
10990
  async countRecentUngrantedByIp(ip, windowMs) {
10645
- const result = await this.db.select({ count: sql11`count(*)::int` }).from(oauth2Clients).where(
10646
- and21(
10647
- eq25(oauth2Clients.createdIp, ip),
10648
- gt9(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
10649
- 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})`
10650
10996
  )
10651
10997
  );
10652
10998
  return result[0]?.count ?? 0;
@@ -10663,9 +11009,9 @@ var init_oauth2_clients_repository = __esm({
10663
11009
  */
10664
11010
  async deleteStaleUngranted(before) {
10665
11011
  const deleted = await this.db.delete(oauth2Clients).where(
10666
- and21(
11012
+ and22(
10667
11013
  lt8(oauth2Clients.createdAt, before),
10668
- 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})`
10669
11015
  )
10670
11016
  ).returning({ id: oauth2Clients.id });
10671
11017
  return deleted.length;
@@ -10676,8 +11022,8 @@ var init_oauth2_clients_repository = __esm({
10676
11022
  });
10677
11023
 
10678
11024
  // src/server/repositories/oauth2-grants.repository.ts
10679
- import { and as and22, desc as desc5, eq as eq26, inArray as inArray4, isNull as isNull14, sql as sql12 } from "drizzle-orm";
10680
- 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";
10681
11027
  var OAuth2GrantsRepository, oauth2GrantsRepository;
10682
11028
  var init_oauth2_grants_repository = __esm({
10683
11029
  "src/server/repositories/oauth2-grants.repository.ts"() {
@@ -10685,7 +11031,7 @@ var init_oauth2_grants_repository = __esm({
10685
11031
  init_oauth2_grants();
10686
11032
  init_oauth2_clients();
10687
11033
  init_oauth2_tokens();
10688
- OAuth2GrantsRepository = class extends BaseRepository26 {
11034
+ OAuth2GrantsRepository = class extends BaseRepository27 {
10689
11035
  /**
10690
11036
  * Record a consent, or refresh the scopes of the one already there.
10691
11037
  *
@@ -10712,13 +11058,13 @@ var init_oauth2_grants_repository = __esm({
10712
11058
  *
10713
11059
  * Read primary: revocation is documented as taking effect immediately.
10714
11060
  */
10715
- async findWithClientById(id26) {
10716
- 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);
10717
11063
  return result[0] ?? null;
10718
11064
  }
10719
11065
  /** What the user's "connected apps" screen lists. Read replica. */
10720
11066
  async listActiveByUserId(userId) {
10721
- 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));
10722
11068
  }
10723
11069
  /**
10724
11070
  * Revoke one live grant belonging to one user.
@@ -10731,19 +11077,19 @@ var init_oauth2_grants_repository = __esm({
10731
11077
  * @returns the revoked row, or null if there was no live grant of that id
10732
11078
  * for that user
10733
11079
  */
10734
- async revokeByIdForUser(id26, userId) {
11080
+ async revokeByIdForUser(id27, userId) {
10735
11081
  const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
10736
- and22(
10737
- eq26(oauth2Grants.id, id26),
10738
- eq26(oauth2Grants.user, userId),
10739
- isNull14(oauth2Grants.revokedAt)
11082
+ and23(
11083
+ eq27(oauth2Grants.id, id27),
11084
+ eq27(oauth2Grants.user, userId),
11085
+ isNull15(oauth2Grants.revokedAt)
10740
11086
  )
10741
11087
  ).returning();
10742
11088
  return result[0] ?? null;
10743
11089
  }
10744
11090
  /** Revoke one grant by id, whoever it belongs to — the replay detections. */
10745
- async revokeById(id26) {
10746
- 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();
10747
11093
  return result[0] ?? null;
10748
11094
  }
10749
11095
  /**
@@ -10753,7 +11099,7 @@ var init_oauth2_grants_repository = __esm({
10753
11099
  * @returns the grant ids this call revoked
10754
11100
  */
10755
11101
  async revokeAllActiveByUserId(userId) {
10756
- 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 });
10757
11103
  return revoked.map((row) => row.id);
10758
11104
  }
10759
11105
  /**
@@ -10769,7 +11115,7 @@ var init_oauth2_grants_repository = __esm({
10769
11115
  if (grantIds.length === 0) {
10770
11116
  return 0;
10771
11117
  }
10772
- 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 });
10773
11119
  return revoked.length;
10774
11120
  }
10775
11121
  };
@@ -10778,14 +11124,14 @@ var init_oauth2_grants_repository = __esm({
10778
11124
  });
10779
11125
 
10780
11126
  // src/server/repositories/oauth2-authorization-codes.repository.ts
10781
- import { and as and23, eq as eq27, gt as gt10, isNull as isNull15, sql as sql13 } from "drizzle-orm";
10782
- 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";
10783
11129
  var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
10784
11130
  var init_oauth2_authorization_codes_repository = __esm({
10785
11131
  "src/server/repositories/oauth2-authorization-codes.repository.ts"() {
10786
11132
  "use strict";
10787
11133
  init_oauth2_authorization_codes();
10788
- OAuth2AuthorizationCodesRepository = class extends BaseRepository27 {
11134
+ OAuth2AuthorizationCodesRepository = class extends BaseRepository28 {
10789
11135
  async create(data) {
10790
11136
  const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
10791
11137
  return result[0];
@@ -10797,11 +11143,11 @@ var init_oauth2_authorization_codes_repository = __esm({
10797
11143
  * spent, or past its 60 seconds
10798
11144
  */
10799
11145
  async consume(codeHash) {
10800
- const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql13`now()` }).where(
10801
- and23(
10802
- eq27(oauth2AuthorizationCodes.codeHash, codeHash),
10803
- isNull15(oauth2AuthorizationCodes.usedAt),
10804
- 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()`)
10805
11151
  )
10806
11152
  ).returning();
10807
11153
  return result[0] ?? null;
@@ -10812,7 +11158,7 @@ var init_oauth2_authorization_codes_repository = __esm({
10812
11158
  * would be indistinguishable from a code that never existed.
10813
11159
  */
10814
11160
  async findByCodeHash(codeHash) {
10815
- 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);
10816
11162
  return result[0] ?? null;
10817
11163
  }
10818
11164
  };
@@ -10821,14 +11167,14 @@ var init_oauth2_authorization_codes_repository = __esm({
10821
11167
  });
10822
11168
 
10823
11169
  // src/server/repositories/oauth2-tokens.repository.ts
10824
- import { and as and24, eq as eq28, gt as gt11, isNull as isNull16, sql as sql14 } from "drizzle-orm";
10825
- 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";
10826
11172
  var OAuth2TokensRepository, oauth2TokensRepository;
10827
11173
  var init_oauth2_tokens_repository = __esm({
10828
11174
  "src/server/repositories/oauth2-tokens.repository.ts"() {
10829
11175
  "use strict";
10830
11176
  init_oauth2_tokens();
10831
- OAuth2TokensRepository = class extends BaseRepository28 {
11177
+ OAuth2TokensRepository = class extends BaseRepository29 {
10832
11178
  async create(data) {
10833
11179
  const result = await this.db.insert(oauth2Tokens).values(data).returning();
10834
11180
  return result[0];
@@ -10842,7 +11188,7 @@ var init_oauth2_tokens_repository = __esm({
10842
11188
  * Unfiltered, so the service can tell revoked from expired from unknown.
10843
11189
  */
10844
11190
  async findByTokenHash(tokenHash) {
10845
- 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);
10846
11192
  return result[0] ?? null;
10847
11193
  }
10848
11194
  /**
@@ -10852,13 +11198,13 @@ var init_oauth2_tokens_repository = __esm({
10852
11198
  * rotated, revoked, or expired
10853
11199
  */
10854
11200
  async rotate(tokenHash) {
10855
- const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql14`now()` }).where(
10856
- and24(
10857
- eq28(oauth2Tokens.tokenHash, tokenHash),
10858
- eq28(oauth2Tokens.kind, "refresh"),
10859
- isNull16(oauth2Tokens.replacedAt),
10860
- isNull16(oauth2Tokens.revokedAt),
10861
- 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()`)
10862
11208
  )
10863
11209
  ).returning();
10864
11210
  return result[0] ?? null;
@@ -10871,12 +11217,12 @@ var init_oauth2_tokens_repository = __esm({
10871
11217
  * about whether the value it presented ever existed.
10872
11218
  */
10873
11219
  async revokeByTokenHash(tokenHash) {
10874
- 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();
10875
11221
  return result[0] ?? null;
10876
11222
  }
10877
11223
  /** Fire-and-forget from the verification path, as ops tokens do. */
10878
- async updateLastUsedById(id26) {
10879
- 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));
10880
11226
  }
10881
11227
  };
10882
11228
  oauth2TokensRepository = new OAuth2TokensRepository();
@@ -10901,6 +11247,7 @@ var init_repositories = __esm({
10901
11247
  init_mfa_challenges_repository();
10902
11248
  init_mfa_enrolment_repository();
10903
11249
  init_device_authorizations_repository();
11250
+ init_device_links_repository();
10904
11251
  init_roles_repository();
10905
11252
  init_permissions_repository();
10906
11253
  init_role_permissions_repository();
@@ -11006,7 +11353,7 @@ async function removePermissionFromRole(roleId, permissionId) {
11006
11353
  }
11007
11354
  async function setRolePermissions(roleId, permissionIds) {
11008
11355
  const roleIdNum = Number(roleId);
11009
- const permissionIdNums = permissionIds.map((id26) => Number(id26));
11356
+ const permissionIdNums = permissionIds.map((id27) => Number(id27));
11010
11357
  await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
11011
11358
  }
11012
11359
  async function getAllRoles(includeInactive = false) {
@@ -11026,7 +11373,7 @@ async function getRolePermissions(roleId) {
11026
11373
  }
11027
11374
  const permissionIds = mappings.map((m) => m.permissionId);
11028
11375
  const perms = await Promise.all(
11029
- permissionIds.map((id26) => permissionsRepository.findById(id26))
11376
+ permissionIds.map((id27) => permissionsRepository.findById(id27))
11030
11377
  );
11031
11378
  return perms.filter((p) => p !== null).map((p) => p.name);
11032
11379
  }
@@ -11290,8 +11637,8 @@ async function listOAuth2GrantsService(userId) {
11290
11637
  lastUsedAtMillis: client.lastUsedAt?.getTime()
11291
11638
  }));
11292
11639
  }
11293
- async function revokeOAuth2GrantService(id26, userId) {
11294
- const revoked = await oauth2GrantsRepository.revokeByIdForUser(id26, userId);
11640
+ async function revokeOAuth2GrantService(id27, userId) {
11641
+ const revoked = await oauth2GrantsRepository.revokeByIdForUser(id27, userId);
11295
11642
  if (!revoked) {
11296
11643
  throw new OAuth2GrantNotFoundError();
11297
11644
  }
@@ -11717,6 +12064,7 @@ var DeviceRegistrationChannelSchema = Type.Union([
11717
12064
  Type.Literal("oauth"),
11718
12065
  Type.Literal("oauth-native"),
11719
12066
  Type.Literal("device-code"),
12067
+ Type.Literal("device-link"),
11720
12068
  Type.Literal("password-reset"),
11721
12069
  Type.Literal("passkey"),
11722
12070
  Type.Literal("renewal")
@@ -11949,16 +12297,16 @@ function encodeBase32(bytes) {
11949
12297
  }
11950
12298
  return bits > 0 ? output + BASE32_ALPHABET[value << 5 - bits & 31] : output;
11951
12299
  }
11952
- function decodeBase32(text27) {
12300
+ function decodeBase32(text28) {
11953
12301
  let bits = 0;
11954
12302
  let value = 0;
11955
12303
  const bytes = [];
11956
- for (const character of text27.replace(/=+$/, "").toUpperCase()) {
11957
- const index21 = BASE32_ALPHABET.indexOf(character);
11958
- if (index21 < 0) {
12304
+ for (const character of text28.replace(/=+$/, "").toUpperCase()) {
12305
+ const index22 = BASE32_ALPHABET.indexOf(character);
12306
+ if (index22 < 0) {
11959
12307
  throw new Error("Value is not RFC 4648 base32");
11960
12308
  }
11961
- value = value << 5 | index21;
12309
+ value = value << 5 | index22;
11962
12310
  bits += 5;
11963
12311
  if (bits >= 8) {
11964
12312
  bytes.push(value >>> bits - 8 & 255);
@@ -12248,9 +12596,9 @@ function presentedChallenge(clientDataJSON) {
12248
12596
  const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
12249
12597
  return typeof decoded?.challenge === "string" ? decoded.challenge : "";
12250
12598
  }
12251
- function parseJson(text27) {
12599
+ function parseJson(text28) {
12252
12600
  try {
12253
- return JSON.parse(text27);
12601
+ return JSON.parse(text28);
12254
12602
  } catch {
12255
12603
  return null;
12256
12604
  }
@@ -12717,6 +13065,7 @@ async function revokeAllKeysService(params) {
12717
13065
  }
12718
13066
  const revoked = !includeCurrent && currentKeyId ? await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason) : await keysRepository.revokeAllActiveByUserId(userId, reason);
12719
13067
  await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
13068
+ await deviceLinksRepository.expireAllLiveByUserId(userId);
12720
13069
  await revokeAllOAuth2GrantsForUser(userId);
12721
13070
  return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
12722
13071
  }
@@ -12802,8 +13151,8 @@ async function verifyReauthCredential(user, params) {
12802
13151
  throw new VerificationTokenTargetMismatchError();
12803
13152
  }
12804
13153
  }
12805
- async function sendDeletionEmail(to, subject, text27) {
12806
- const result = await sendEmail3({ to, subject, text: text27 });
13154
+ async function sendDeletionEmail(to, subject, text28) {
13155
+ const result = await sendEmail3({ to, subject, text: text28 });
12807
13156
  if (!result.success) {
12808
13157
  authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
12809
13158
  }
@@ -12855,12 +13204,12 @@ async function requestAccountDeletionService(userId, params) {
12855
13204
  if (requestedBy === "self") {
12856
13205
  await verifyReauthCredential(user, { password, verificationToken });
12857
13206
  }
12858
- const config4 = getDeletionConfig();
13207
+ const config5 = getDeletionConfig();
12859
13208
  const wantsImmediate = immediate === true;
12860
- if (wantsImmediate && requestedBy === "self" && !config4.allowSelfImmediate) {
13209
+ if (wantsImmediate && requestedBy === "self" && !config5.allowSelfImmediate) {
12861
13210
  throw new ImmediateDeletionNotAllowedError();
12862
13211
  }
12863
- const gracePeriodDays = wantsImmediate ? 0 : config4.gracePeriodDays;
13212
+ const gracePeriodDays = wantsImmediate ? 0 : config5.gracePeriodDays;
12864
13213
  const requestedAt = /* @__PURE__ */ new Date();
12865
13214
  const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
12866
13215
  await usersRepository.updateById(user.id, { status: "pending_deletion" });
@@ -12883,6 +13232,7 @@ async function requestAccountDeletionService(userId, params) {
12883
13232
  }
12884
13233
  await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
12885
13234
  await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
13235
+ await deviceLinksRepository.expireAllLiveByUserId(user.id);
12886
13236
  await revokeAllOAuth2GrantsForUser(user.id);
12887
13237
  onAfterCommit4(() => authDeletionRequestedEvent.emit({
12888
13238
  userId: String(user.id),
@@ -12967,10 +13317,10 @@ async function purgePendingRequest(request) {
12967
13317
  if (!precheckUser || precheckUser.status !== "pending_deletion") {
12968
13318
  return { outcome: "skipped" };
12969
13319
  }
12970
- const config4 = getDeletionConfig();
12971
- if (config4.onBeforePurge) {
13320
+ const config5 = getDeletionConfig();
13321
+ if (config5.onBeforePurge) {
12972
13322
  try {
12973
- await config4.onBeforePurge({
13323
+ await config5.onBeforePurge({
12974
13324
  id: precheckUser.id,
12975
13325
  publicId: precheckUser.publicId,
12976
13326
  email: precheckUser.email,
@@ -12984,7 +13334,7 @@ async function purgePendingRequest(request) {
12984
13334
  return { outcome: "skipped" };
12985
13335
  }
12986
13336
  }
12987
- const purgeStrategy = config4.purgeStrategy;
13337
+ const purgeStrategy = config5.purgeStrategy;
12988
13338
  let purgedUser = null;
12989
13339
  await runInTransaction3(async () => {
12990
13340
  const user = await usersRepository.findById(userId);
@@ -13229,6 +13579,7 @@ async function changePasswordService(params) {
13229
13579
  const newPasswordHash = await hashPassword(newPassword);
13230
13580
  await usersRepository.updatePassword(userId, newPasswordHash, true);
13231
13581
  await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
13582
+ await deviceLinksRepository.expireAllLiveByUserId(userId);
13232
13583
  await revokeAllOAuth2GrantsForUser(userId);
13233
13584
  await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
13234
13585
  }
@@ -13430,7 +13781,9 @@ async function replaceCredentials(row, user, params) {
13430
13781
  // for accounts created before the register flows stamped it.
13431
13782
  ...user.emailVerifiedAt ? {} : { emailVerifiedAt: /* @__PURE__ */ new Date() }
13432
13783
  });
13784
+ await keysRepository.lockActiveByUserId(user.id);
13433
13785
  await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
13786
+ await deviceLinksRepository.expireAllLiveByUserId(user.id);
13434
13787
  await revokeAllOAuth2GrantsForUser(user.id);
13435
13788
  await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
13436
13789
  const registered = await registerPublicKeyService({
@@ -13739,13 +14092,13 @@ async function waitForDeviceAuthAnswerService(params) {
13739
14092
  await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
13740
14093
  return Date.now() - startedAt;
13741
14094
  }
13742
- async function waitUntil(id26, deviceCodeHash, deadline, signal) {
14095
+ async function waitUntil(id27, deviceCodeHash, deadline, signal) {
13743
14096
  while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
13744
14097
  const remaining = deadline - Date.now();
13745
14098
  if (remaining <= 0) {
13746
14099
  return;
13747
14100
  }
13748
- await waitForDeviceAuthAnswer(id26, Math.min(remaining, WAIT_RECHECK_MS), signal);
14101
+ await waitForDeviceAuthAnswer(id27, Math.min(remaining, WAIT_RECHECK_MS), signal);
13749
14102
  if (!await readWaitable(deviceCodeHash)) {
13750
14103
  return;
13751
14104
  }
@@ -13780,15 +14133,25 @@ async function pollDeviceAuthService(params) {
13780
14133
  () => new DeviceAuthNotFoundError()
13781
14134
  );
13782
14135
  }
13783
- return { status: "approved", ...await completeDeviceLogin(consumed, params) };
13784
- }
13785
- async function completeDeviceLogin(record, provenance) {
13786
- if (record.userId === null) {
14136
+ if (consumed.userId === null) {
13787
14137
  throw new DeviceAuthNotFoundError();
13788
14138
  }
13789
- 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);
13790
14153
  if (!user) {
13791
- throw new DeviceAuthNotFoundError();
14154
+ throw params.missingAccount();
13792
14155
  }
13793
14156
  if (user.status !== "active") {
13794
14157
  if (user.status === "pending_deletion") {
@@ -13801,22 +14164,22 @@ async function completeDeviceLogin(record, provenance) {
13801
14164
  }
13802
14165
  const registered = await registerPublicKeyService({
13803
14166
  userId: user.id,
13804
- keyId: record.keyId,
13805
- publicKey: record.publicKey,
13806
- fingerprint: record.fingerprint,
13807
- algorithm: record.algorithm,
13808
- deviceName: record.deviceName ?? void 0,
13809
- platform: record.platform ?? void 0,
13810
- 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,
13811
14174
  ip: provenance.ip,
13812
14175
  userAgent: provenance.userAgent,
13813
14176
  binding: decideKeyBinding(user.sessionBinding, provenance.webProxy)
13814
14177
  });
13815
14178
  await updateLastLoginService(user.id);
13816
14179
  const result = {
13817
- // A device-code approval is itself a second factor: the owner read the
13818
- // code on a device that is already signed in and said yes, so this
13819
- // 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).
13820
14183
  mfaRequired: false,
13821
14184
  userId: String(user.id),
13822
14185
  publicId: user.publicId,
@@ -13836,6 +14199,320 @@ async function completeDeviceLogin(record, provenance) {
13836
14199
  return result;
13837
14200
  }
13838
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
+
13839
14516
  // src/server/services/passkey.service.ts
13840
14517
  init_logger();
13841
14518
  init_config();
@@ -14223,51 +14900,51 @@ async function initializeAuth(options = {}) {
14223
14900
  authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
14224
14901
  }
14225
14902
  async function syncRoles(configs, existingByName) {
14226
- for (const config4 of configs) {
14227
- const existing = existingByName.get(config4.name);
14903
+ for (const config5 of configs) {
14904
+ const existing = existingByName.get(config5.name);
14228
14905
  if (!existing) {
14229
14906
  await rolesRepository.create({
14230
- name: config4.name,
14231
- displayName: config4.displayName,
14232
- description: config4.description || null,
14233
- priority: config4.priority ?? 10,
14234
- isSystem: config4.isSystem ?? false,
14235
- 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,
14236
14913
  isActive: true
14237
14914
  });
14238
- authLogger.service.info(` \u2705 Created role: ${config4.name}`);
14915
+ authLogger.service.info(` \u2705 Created role: ${config5.name}`);
14239
14916
  } else {
14240
14917
  const updateData = {
14241
- displayName: config4.displayName,
14242
- description: config4.description || null
14918
+ displayName: config5.displayName,
14919
+ description: config5.description || null
14243
14920
  };
14244
14921
  if (!existing.isBuiltin) {
14245
- updateData.priority = config4.priority ?? existing.priority;
14922
+ updateData.priority = config5.priority ?? existing.priority;
14246
14923
  }
14247
14924
  await rolesRepository.updateById(existing.id, updateData);
14248
14925
  }
14249
14926
  }
14250
14927
  }
14251
14928
  async function syncPermissions(configs, existingByName) {
14252
- for (const config4 of configs) {
14253
- const existing = existingByName.get(config4.name);
14929
+ for (const config5 of configs) {
14930
+ const existing = existingByName.get(config5.name);
14254
14931
  if (!existing) {
14255
14932
  await permissionsRepository.create({
14256
- name: config4.name,
14257
- displayName: config4.displayName,
14258
- description: config4.description || null,
14259
- category: config4.category || null,
14260
- isSystem: config4.isSystem ?? false,
14261
- 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,
14262
14939
  isActive: true,
14263
14940
  metadata: null
14264
14941
  });
14265
- authLogger.service.info(` \u2705 Created permission: ${config4.name}`);
14942
+ authLogger.service.info(` \u2705 Created permission: ${config5.name}`);
14266
14943
  } else {
14267
14944
  await permissionsRepository.updateById(existing.id, {
14268
- displayName: config4.displayName,
14269
- description: config4.description || null,
14270
- category: config4.category || null
14945
+ displayName: config5.displayName,
14946
+ description: config5.description || null,
14947
+ category: config5.category || null
14271
14948
  });
14272
14949
  }
14273
14950
  }
@@ -14329,7 +15006,7 @@ async function getUserPermissions(userId) {
14329
15006
  const permIds = rolePermMappings.map((rp) => rp.permissionId);
14330
15007
  if (permIds.length > 0) {
14331
15008
  const rolePerms = await Promise.all(
14332
- permIds.map((id26) => permissionsRepository.findById(id26))
15009
+ permIds.map((id27) => permissionsRepository.findById(id27))
14333
15010
  );
14334
15011
  for (const perm of rolePerms) {
14335
15012
  if (perm && perm.isActive) {
@@ -14547,20 +15224,20 @@ async function acceptInvitation(params) {
14547
15224
  async function listInvitations(params) {
14548
15225
  return await invitationsRepository.list(params);
14549
15226
  }
14550
- async function cancelInvitation(id26, cancelledBy, reason) {
14551
- const invitation = await invitationsRepository.findById(id26);
15227
+ async function cancelInvitation(id27, cancelledBy, reason) {
15228
+ const invitation = await invitationsRepository.findById(id27);
14552
15229
  if (!invitation) {
14553
15230
  throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
14554
15231
  }
14555
15232
  if (invitation.status !== "pending") {
14556
15233
  throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
14557
15234
  }
14558
- await invitationsRepository.cancel(id26, cancelledBy, reason, invitation.metadata);
15235
+ await invitationsRepository.cancel(id27, cancelledBy, reason, invitation.metadata);
14559
15236
  console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
14560
15237
  }
14561
- async function deleteInvitation(id26) {
14562
- await invitationsRepository.deleteById(id26);
14563
- 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}`);
14564
15241
  }
14565
15242
  async function expireOldInvitations() {
14566
15243
  const count2 = await invitationsRepository.updateExpiredInvitations();
@@ -14569,8 +15246,8 @@ async function expireOldInvitations() {
14569
15246
  }
14570
15247
  return count2;
14571
15248
  }
14572
- async function resendInvitation(id26, expiresInDays = 7) {
14573
- const invitation = await invitationsRepository.findById(id26);
15249
+ async function resendInvitation(id27, expiresInDays = 7) {
15250
+ const invitation = await invitationsRepository.findById(id27);
14574
15251
  if (!invitation) {
14575
15252
  throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
14576
15253
  }
@@ -14578,7 +15255,7 @@ async function resendInvitation(id26, expiresInDays = 7) {
14578
15255
  throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
14579
15256
  }
14580
15257
  const newExpiresAt = calculateExpiresAt(expiresInDays);
14581
- const updated = await invitationsRepository.resend(id26, newExpiresAt);
15258
+ const updated = await invitationsRepository.resend(id27, newExpiresAt);
14582
15259
  if (!updated) {
14583
15260
  throw new Error("Failed to update invitation");
14584
15261
  }
@@ -14617,13 +15294,13 @@ async function getAuthSessionService(userId) {
14617
15294
  // src/server/lib/one-time-token.ts
14618
15295
  import { SSETokenManager } from "@spfn/core/event/sse";
14619
15296
  var manager = null;
14620
- function initOneTimeTokenManager(config4) {
15297
+ function initOneTimeTokenManager(config5) {
14621
15298
  if (manager) {
14622
15299
  manager.destroy();
14623
15300
  }
14624
15301
  manager = new SSETokenManager({
14625
- ttl: config4?.ttl,
14626
- store: config4?.store
15302
+ ttl: config5?.ttl,
15303
+ store: config5?.store
14627
15304
  });
14628
15305
  }
14629
15306
  function getOneTimeTokenManager() {
@@ -14773,10 +15450,10 @@ function getDefaultScopes() {
14773
15450
  }
14774
15451
  function getGoogleAuthUrl(state, scopes) {
14775
15452
  const resolvedScopes = scopes ?? getDefaultScopes();
14776
- const config4 = getGoogleOAuthConfig();
15453
+ const config5 = getGoogleOAuthConfig();
14777
15454
  const params = new URLSearchParams({
14778
- client_id: config4.clientId,
14779
- redirect_uri: config4.redirectUri,
15455
+ client_id: config5.clientId,
15456
+ redirect_uri: config5.redirectUri,
14780
15457
  response_type: "code",
14781
15458
  scope: resolvedScopes.join(" "),
14782
15459
  state,
@@ -14788,16 +15465,16 @@ function getGoogleAuthUrl(state, scopes) {
14788
15465
  return `${GOOGLE_AUTH_URL}?${params.toString()}`;
14789
15466
  }
14790
15467
  async function exchangeCodeForTokens(code) {
14791
- const config4 = getGoogleOAuthConfig();
15468
+ const config5 = getGoogleOAuthConfig();
14792
15469
  const response = await fetch(GOOGLE_TOKEN_URL, {
14793
15470
  method: "POST",
14794
15471
  headers: {
14795
15472
  "Content-Type": "application/x-www-form-urlencoded"
14796
15473
  },
14797
15474
  body: new URLSearchParams({
14798
- client_id: config4.clientId,
14799
- client_secret: config4.clientSecret,
14800
- redirect_uri: config4.redirectUri,
15475
+ client_id: config5.clientId,
15476
+ client_secret: config5.clientSecret,
15477
+ redirect_uri: config5.redirectUri,
14801
15478
  grant_type: "authorization_code",
14802
15479
  code
14803
15480
  })
@@ -14821,15 +15498,15 @@ async function getGoogleUserInfo(accessToken) {
14821
15498
  return response.json();
14822
15499
  }
14823
15500
  async function refreshAccessToken(refreshToken) {
14824
- const config4 = getGoogleOAuthConfig();
15501
+ const config5 = getGoogleOAuthConfig();
14825
15502
  const response = await fetch(GOOGLE_TOKEN_URL, {
14826
15503
  method: "POST",
14827
15504
  headers: {
14828
15505
  "Content-Type": "application/x-www-form-urlencoded"
14829
15506
  },
14830
15507
  body: new URLSearchParams({
14831
- client_id: config4.clientId,
14832
- client_secret: config4.clientSecret,
15508
+ client_id: config5.clientId,
15509
+ client_secret: config5.clientSecret,
14833
15510
  refresh_token: refreshToken,
14834
15511
  grant_type: "refresh_token"
14835
15512
  })
@@ -14894,8 +15571,8 @@ var registry2 = /* @__PURE__ */ new Map();
14894
15571
  function registerOAuthProvider(provider) {
14895
15572
  registry2.set(provider.id, provider);
14896
15573
  }
14897
- function getOAuthProvider(id26) {
14898
- return registry2.get(id26);
15574
+ function getOAuthProvider(id27) {
15575
+ return registry2.get(id27);
14899
15576
  }
14900
15577
  function getRegisteredProviders() {
14901
15578
  return [...registry2.values()];
@@ -15150,21 +15827,21 @@ var githubProvider = {
15150
15827
  return !!(env4.SPFN_AUTH_GITHUB_CLIENT_ID && env4.SPFN_AUTH_GITHUB_CLIENT_SECRET);
15151
15828
  },
15152
15829
  getAuthUrl(state, scopes) {
15153
- const config4 = getGithubConfig();
15830
+ const config5 = getGithubConfig();
15154
15831
  const params = new URLSearchParams({
15155
- client_id: config4.clientId,
15156
- redirect_uri: config4.redirectUri,
15832
+ client_id: config5.clientId,
15833
+ redirect_uri: config5.redirectUri,
15157
15834
  state,
15158
15835
  scope: (scopes ?? getGithubScopes()).join(" ")
15159
15836
  });
15160
15837
  return `${GITHUB_AUTH_URL}?${params.toString()}`;
15161
15838
  },
15162
15839
  async exchangeCodeForTokens(code) {
15163
- const config4 = getGithubConfig();
15840
+ const config5 = getGithubConfig();
15164
15841
  return requestGithubTokens(new URLSearchParams({
15165
- client_id: config4.clientId,
15166
- client_secret: config4.clientSecret,
15167
- redirect_uri: config4.redirectUri,
15842
+ client_id: config5.clientId,
15843
+ client_secret: config5.clientSecret,
15844
+ redirect_uri: config5.redirectUri,
15168
15845
  code
15169
15846
  }));
15170
15847
  },
@@ -15194,11 +15871,11 @@ var githubProvider = {
15194
15871
  };
15195
15872
  },
15196
15873
  async refreshTokens(refreshToken) {
15197
- const config4 = getGithubConfig();
15874
+ const config5 = getGithubConfig();
15198
15875
  return requestGithubTokens(new URLSearchParams({
15199
15876
  grant_type: "refresh_token",
15200
- client_id: config4.clientId,
15201
- client_secret: config4.clientSecret,
15877
+ client_id: config5.clientId,
15878
+ client_secret: config5.clientSecret,
15202
15879
  refresh_token: refreshToken
15203
15880
  }));
15204
15881
  }
@@ -15322,26 +15999,26 @@ var kakaoProvider = {
15322
15999
  return !!env4.SPFN_AUTH_KAKAO_CLIENT_ID;
15323
16000
  },
15324
16001
  getAuthUrl(state, scopes) {
15325
- const config4 = getKakaoConfig();
16002
+ const config5 = getKakaoConfig();
15326
16003
  const params = new URLSearchParams({
15327
16004
  response_type: "code",
15328
- client_id: config4.clientId,
15329
- redirect_uri: config4.redirectUri,
16005
+ client_id: config5.clientId,
16006
+ redirect_uri: config5.redirectUri,
15330
16007
  state,
15331
16008
  scope: (scopes ?? getKakaoScopes()).join(",")
15332
16009
  });
15333
16010
  return `${KAKAO_AUTH_URL}?${params.toString()}`;
15334
16011
  },
15335
16012
  async exchangeCodeForTokens(code) {
15336
- const config4 = getKakaoConfig();
16013
+ const config5 = getKakaoConfig();
15337
16014
  const params = new URLSearchParams({
15338
16015
  grant_type: "authorization_code",
15339
- client_id: config4.clientId,
15340
- redirect_uri: config4.redirectUri,
16016
+ client_id: config5.clientId,
16017
+ redirect_uri: config5.redirectUri,
15341
16018
  code
15342
16019
  });
15343
- if (config4.clientSecret) {
15344
- params.set("client_secret", config4.clientSecret);
16020
+ if (config5.clientSecret) {
16021
+ params.set("client_secret", config5.clientSecret);
15345
16022
  }
15346
16023
  return requestKakaoTokens(params);
15347
16024
  },
@@ -15383,14 +16060,14 @@ var kakaoProvider = {
15383
16060
  return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
15384
16061
  },
15385
16062
  async refreshTokens(refreshToken) {
15386
- const config4 = getKakaoConfig();
16063
+ const config5 = getKakaoConfig();
15387
16064
  const params = new URLSearchParams({
15388
16065
  grant_type: "refresh_token",
15389
- client_id: config4.clientId,
16066
+ client_id: config5.clientId,
15390
16067
  refresh_token: refreshToken
15391
16068
  });
15392
- if (config4.clientSecret) {
15393
- params.set("client_secret", config4.clientSecret);
16069
+ if (config5.clientSecret) {
16070
+ params.set("client_secret", config5.clientSecret);
15394
16071
  }
15395
16072
  return requestKakaoTokens(params);
15396
16073
  },
@@ -15549,22 +16226,22 @@ var naverProvider = {
15549
16226
  return !!(env4.SPFN_AUTH_NAVER_CLIENT_ID && env4.SPFN_AUTH_NAVER_CLIENT_SECRET);
15550
16227
  },
15551
16228
  getAuthUrl(state) {
15552
- const config4 = getNaverConfig();
16229
+ const config5 = getNaverConfig();
15553
16230
  const params = new URLSearchParams({
15554
16231
  response_type: "code",
15555
- client_id: config4.clientId,
15556
- redirect_uri: config4.redirectUri,
16232
+ client_id: config5.clientId,
16233
+ redirect_uri: config5.redirectUri,
15557
16234
  state
15558
16235
  });
15559
16236
  return `${NAVER_AUTH_URL}?${params.toString()}`;
15560
16237
  },
15561
16238
  async exchangeCodeForTokens(code, options) {
15562
- const config4 = getNaverConfig();
16239
+ const config5 = getNaverConfig();
15563
16240
  return requestNaverTokens(new URLSearchParams({
15564
16241
  grant_type: "authorization_code",
15565
- client_id: config4.clientId,
15566
- client_secret: config4.clientSecret,
15567
- redirect_uri: config4.redirectUri,
16242
+ client_id: config5.clientId,
16243
+ client_secret: config5.clientSecret,
16244
+ redirect_uri: config5.redirectUri,
15568
16245
  code,
15569
16246
  state: options.state
15570
16247
  }));
@@ -15605,11 +16282,11 @@ var naverProvider = {
15605
16282
  return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
15606
16283
  },
15607
16284
  async refreshTokens(refreshToken) {
15608
- const config4 = getNaverConfig();
16285
+ const config5 = getNaverConfig();
15609
16286
  return requestNaverTokens(new URLSearchParams({
15610
16287
  grant_type: "refresh_token",
15611
- client_id: config4.clientId,
15612
- client_secret: config4.clientSecret,
16288
+ client_id: config5.clientId,
16289
+ client_secret: config5.clientSecret,
15613
16290
  refresh_token: refreshToken
15614
16291
  }));
15615
16292
  },
@@ -15970,7 +16647,7 @@ async function oauthUnlinkNotifyService(provider, notification) {
15970
16647
  // src/server/services/oauth-native.service.ts
15971
16648
  import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
15972
16649
  import {
15973
- InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
16650
+ InvalidKeyFingerprintError as InvalidKeyFingerprintError4,
15974
16651
  NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
15975
16652
  NonceKeyBindingError
15976
16653
  } from "@spfn/auth/errors";
@@ -15997,7 +16674,7 @@ function assertNonceBindsPublicKey(params) {
15997
16674
  throw new NonceKeyBindingError();
15998
16675
  }
15999
16676
  if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
16000
- throw new InvalidKeyFingerprintError3();
16677
+ throw new InvalidKeyFingerprintError4();
16001
16678
  }
16002
16679
  }
16003
16680
  async function persistNativeLogin(identity, params) {
@@ -16091,8 +16768,8 @@ async function verifyOpsTokenService(token) {
16091
16768
  scopes: record.scopes
16092
16769
  };
16093
16770
  }
16094
- async function revokeOpsTokenService(id26) {
16095
- return await opsTokensRepository.revokeById(id26);
16771
+ async function revokeOpsTokenService(id27) {
16772
+ return await opsTokensRepository.revokeById(id27);
16096
16773
  }
16097
16774
  async function listOpsTokensService() {
16098
16775
  return await opsTokensRepository.list();
@@ -16108,7 +16785,7 @@ var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
16108
16785
  var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
16109
16786
  var DEFAULT_CODE_TTL_MS = 60 * 1e3;
16110
16787
  var AUTHORIZE_PATH = "/oauth/authorize";
16111
- var config3 = null;
16788
+ var config4 = null;
16112
16789
  function resolveIssuerSource(env21 = process.env) {
16113
16790
  return { value: env21.SPFN_API_URL, variable: "SPFN_API_URL" };
16114
16791
  }
@@ -16118,7 +16795,7 @@ function resolveAuthorizeUrl(env21) {
16118
16795
  }
16119
16796
  function configureAuthorizationServer(options, env21 = process.env) {
16120
16797
  if (!options) {
16121
- config3 = null;
16798
+ config4 = null;
16122
16799
  return;
16123
16800
  }
16124
16801
  const scopeNames = Object.keys(options.scopes ?? {});
@@ -16127,7 +16804,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
16127
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."
16128
16805
  );
16129
16806
  }
16130
- config3 = {
16807
+ config4 = {
16131
16808
  issuer: canonicalIssuer(options.issuer ?? resolveIssuerSource(env21).value ?? ""),
16132
16809
  issuerSource: options.issuer ? "authorizationServer.issuer" : resolveIssuerSource(env21).variable,
16133
16810
  authorizeUrl: options.authorizeUrl ?? resolveAuthorizeUrl(env21),
@@ -16138,7 +16815,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
16138
16815
  refreshTokenTtlMs: options.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS,
16139
16816
  codeTtlMs: options.codeTtlMs ?? DEFAULT_CODE_TTL_MS
16140
16817
  };
16141
- assertKnownDefaultScopes(config3);
16818
+ assertKnownDefaultScopes(config4);
16142
16819
  }
16143
16820
  function assertKnownDefaultScopes(resolved) {
16144
16821
  const unknown = resolved.defaultScopes.filter((scope) => !(scope in resolved.scopes));
@@ -16149,7 +16826,7 @@ function assertKnownDefaultScopes(resolved) {
16149
16826
  }
16150
16827
  }
16151
16828
  function getAuthorizationServerConfig() {
16152
- return config3;
16829
+ return config4;
16153
16830
  }
16154
16831
  function isLoopbackHostname(hostname) {
16155
16832
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
@@ -16372,15 +17049,15 @@ var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
16372
17049
  var SUPPORTED_AUTH_METHOD = "none";
16373
17050
  var SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
16374
17051
  var SUPPORTED_RESPONSE_TYPES = ["code"];
16375
- function refuse(status, error, description) {
17052
+ function refuse2(status, error, description) {
16376
17053
  return { ok: false, status, error, description };
16377
17054
  }
16378
17055
  function requireConfig() {
16379
- const config4 = getAuthorizationServerConfig();
16380
- if (!config4) {
17056
+ const config5 = getAuthorizationServerConfig();
17057
+ if (!config5) {
16381
17058
  throw new Error("OAuth2 client service called with no authorization server configured.");
16382
17059
  }
16383
- return config4;
17060
+ return config5;
16384
17061
  }
16385
17062
  function asStringArray(value) {
16386
17063
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
@@ -16391,7 +17068,7 @@ function asStringArray(value) {
16391
17068
  function refuseDeclaredMetadata(request) {
16392
17069
  const { token_endpoint_auth_method: authMethod } = request;
16393
17070
  if (authMethod !== void 0 && authMethod !== SUPPORTED_AUTH_METHOD) {
16394
- return refuse(
17071
+ return refuse2(
16395
17072
  400,
16396
17073
  "invalid_client_metadata",
16397
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.`
@@ -16405,7 +17082,7 @@ function refuseListedMetadata(field, value, supported) {
16405
17082
  }
16406
17083
  const declared = asStringArray(value);
16407
17084
  if (!declared || declared.length === 0 || declared.some((entry) => !supported.includes(entry))) {
16408
- return refuse(
17085
+ return refuse2(
16409
17086
  400,
16410
17087
  "invalid_client_metadata",
16411
17088
  `${field} must be a non-empty subset of ${supported.join(", ")}.`
@@ -16416,7 +17093,7 @@ function refuseListedMetadata(field, value, supported) {
16416
17093
  function refuseRedirectUris(uris, allowedRedirectOrigins) {
16417
17094
  const declared = asStringArray(uris);
16418
17095
  if (!declared || declared.length === 0) {
16419
- return refuse(
17096
+ return refuse2(
16420
17097
  400,
16421
17098
  "invalid_client_metadata",
16422
17099
  "redirect_uris must list at least one absolute URI. There is nowhere to send an authorization code without one."
@@ -16425,20 +17102,20 @@ function refuseRedirectUris(uris, allowedRedirectOrigins) {
16425
17102
  for (const uri of declared) {
16426
17103
  const detail = refuseRedirectUriRegistration(uri, allowedRedirectOrigins);
16427
17104
  if (detail) {
16428
- return refuse(400, "invalid_redirect_uri", detail);
17105
+ return refuse2(400, "invalid_redirect_uri", detail);
16429
17106
  }
16430
17107
  }
16431
17108
  return null;
16432
17109
  }
16433
17110
  async function registerOAuth2ClientService(request, clientIp) {
16434
- const config4 = requireConfig();
16435
- 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);
16436
17113
  if (refusal) {
16437
17114
  return refusal;
16438
17115
  }
16439
17116
  const record = await createUnderStandingCap(newClientRow(request, clientIp), clientIp);
16440
17117
  if (!record) {
16441
- return refuse(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
17118
+ return refuse2(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
16442
17119
  }
16443
17120
  authLogger.service.info("OAuth2 client registered", { clientName: record.clientName });
16444
17121
  return { ok: true, client: describeClient(record) };
@@ -16517,11 +17194,11 @@ function sameResource(presented, granted) {
16517
17194
 
16518
17195
  // src/server/services/oauth2-authorize.service.ts
16519
17196
  function requireConfig2() {
16520
- const config4 = getAuthorizationServerConfig();
16521
- if (!config4) {
17197
+ const config5 = getAuthorizationServerConfig();
17198
+ if (!config5) {
16522
17199
  throw new Error("OAuth2 authorize service called with no authorization server configured.");
16523
17200
  }
16524
- return config4;
17201
+ return config5;
16525
17202
  }
16526
17203
  async function resolveRedirectTarget(params) {
16527
17204
  const client = await oauth2ClientsRepository.findByClientId(params.clientId);
@@ -16533,10 +17210,10 @@ async function resolveRedirectTarget(params) {
16533
17210
  }
16534
17211
  return { client, redirectUri: params.redirectUri };
16535
17212
  }
16536
- function resolveScopes(params, config4) {
17213
+ function resolveScopes(params, config5) {
16537
17214
  const requested = params.scope?.trim();
16538
17215
  if (!requested) {
16539
- return config4.defaultScopes;
17216
+ return config5.defaultScopes;
16540
17217
  }
16541
17218
  return requested.split(/\s+/);
16542
17219
  }
@@ -16548,7 +17225,7 @@ function refuseRedirectable(params, redirectUri, error, message) {
16548
17225
  function hasUsableS256Challenge(params) {
16549
17226
  return params.codeChallengeMethod === "S256" && !!params.codeChallenge && isPkceS256ChallengeShaped(params.codeChallenge);
16550
17227
  }
16551
- function assertRedirectableRules(params, redirectUri, config4) {
17228
+ function assertRedirectableRules(params, redirectUri, config5) {
16552
17229
  if (!hasUsableS256Challenge(params)) {
16553
17230
  refuseRedirectable(params, redirectUri, "invalid_request", PKCE_REQUIRED_MESSAGE);
16554
17231
  }
@@ -16556,31 +17233,31 @@ function assertRedirectableRules(params, redirectUri, config4) {
16556
17233
  if (!resource) {
16557
17234
  refuseRedirectable(params, redirectUri, "invalid_target", RESOURCE_REQUIRED_MESSAGE);
16558
17235
  }
16559
- const scopes = resolveScopes(params, config4);
16560
- 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));
16561
17238
  if (unknown.length > 0) {
16562
17239
  refuseRedirectable(params, redirectUri, "invalid_scope", `Unknown scope: ${unknown.join(", ")}.`);
16563
17240
  }
16564
17241
  return { resource, scopes, codeChallenge: params.codeChallenge };
16565
17242
  }
16566
17243
  async function validate(params) {
16567
- const config4 = requireConfig2();
17244
+ const config5 = requireConfig2();
16568
17245
  const { client, redirectUri } = await resolveRedirectTarget(params);
16569
- const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config4);
17246
+ const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config5);
16570
17247
  return { client, redirectUri, resource, scopes, codeChallenge, state: params.state };
16571
17248
  }
16572
17249
  async function describeOAuth2AuthorizeRequestService(params) {
16573
- const config4 = requireConfig2();
17250
+ const config5 = requireConfig2();
16574
17251
  const validated = await validate(params);
16575
17252
  return {
16576
17253
  clientName: validated.client.clientName,
16577
17254
  redirectHost: redirectHostOf(validated.redirectUri),
16578
- scopes: validated.scopes.map((name) => ({ name, description: config4.scopes[name] })),
17255
+ scopes: validated.scopes.map((name) => ({ name, description: config5.scopes[name] })),
16579
17256
  resource: validated.resource
16580
17257
  };
16581
17258
  }
16582
17259
  async function approveOAuth2AuthorizeService(params, userId) {
16583
- const config4 = requireConfig2();
17260
+ const config5 = requireConfig2();
16584
17261
  const validated = await validate(params);
16585
17262
  const grant = await oauth2GrantsRepository.upsert({
16586
17263
  client: validated.client.id,
@@ -16594,7 +17271,7 @@ async function approveOAuth2AuthorizeService(params, userId) {
16594
17271
  grant: grant.id,
16595
17272
  redirectUri: validated.redirectUri,
16596
17273
  codeChallenge: validated.codeChallenge,
16597
- expiresAt: new Date(Date.now() + config4.codeTtlMs)
17274
+ expiresAt: new Date(Date.now() + config5.codeTtlMs)
16598
17275
  });
16599
17276
  return { code, redirectUri: validated.redirectUri, state: validated.state };
16600
17277
  }
@@ -16622,15 +17299,15 @@ function invalidGrant() {
16622
17299
  description: "The authorization code or refresh token is invalid, expired, already used, or was issued to another client."
16623
17300
  };
16624
17301
  }
16625
- function refuse2(error, description) {
17302
+ function refuse3(error, description) {
16626
17303
  return { ok: false, error, description };
16627
17304
  }
16628
17305
  function requireConfig3() {
16629
- const config4 = getAuthorizationServerConfig();
16630
- if (!config4) {
17306
+ const config5 = getAuthorizationServerConfig();
17307
+ if (!config5) {
16631
17308
  throw new Error("OAuth2 token service called with no authorization server configured.");
16632
17309
  }
16633
- return config4;
17310
+ return config5;
16634
17311
  }
16635
17312
  async function oauth2TokenService(request) {
16636
17313
  if (request.grant_type === "authorization_code") {
@@ -16639,14 +17316,14 @@ async function oauth2TokenService(request) {
16639
17316
  if (request.grant_type === "refresh_token") {
16640
17317
  return await refreshTokens(request);
16641
17318
  }
16642
- return refuse2(
17319
+ return refuse3(
16643
17320
  "unsupported_grant_type",
16644
17321
  "grant_type must be authorization_code or refresh_token."
16645
17322
  );
16646
17323
  }
16647
17324
  async function exchangeAuthorizationCode(request) {
16648
17325
  if (!request.code || !request.code_verifier || !request.client_id || !request.redirect_uri) {
16649
- return refuse2(
17326
+ return refuse3(
16650
17327
  "invalid_request",
16651
17328
  "authorization_code requires code, code_verifier, client_id and redirect_uri."
16652
17329
  );
@@ -16680,13 +17357,13 @@ async function spendBoundCode(request, record, pair) {
16680
17357
  return invalidGrant();
16681
17358
  }
16682
17359
  if (resolveResource(request.resource, pair.grant) === null) {
16683
- 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.");
16684
17361
  }
16685
17362
  return { ok: true, tokens: await issueTokenPair(pair.grant, pair.grant.scopes) };
16686
17363
  }
16687
17364
  async function refreshTokens(request) {
16688
17365
  if (!request.refresh_token || !request.client_id) {
16689
- 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.");
16690
17367
  }
16691
17368
  const tokenHash = hashOAuth2Secret(request.refresh_token);
16692
17369
  const presented = await oauth2TokensRepository.findByTokenHash(tokenHash);
@@ -16708,11 +17385,11 @@ async function rotateRefresh(request, tokenHash, grantId, presentedScopes) {
16708
17385
  return invalidGrant();
16709
17386
  }
16710
17387
  if (resolveResource(request.resource, pair.grant) === null) {
16711
- 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.");
16712
17389
  }
16713
17390
  const scopes = resolveRefreshScopes(request.scope, pair.grant, presentedScopes);
16714
17391
  if (!scopes) {
16715
- 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.");
16716
17393
  }
16717
17394
  if (!await oauth2TokensRepository.rotate(tokenHash)) {
16718
17395
  return await refuseLostRotation(tokenHash, grantId);
@@ -16741,10 +17418,10 @@ function resolveResource(requested, grant) {
16741
17418
  return sameResource(requested, grant.resource) ? grant.resource : null;
16742
17419
  }
16743
17420
  async function issueTokenPair(grant, scopes) {
16744
- const config4 = requireConfig3();
17421
+ const config5 = requireConfig3();
16745
17422
  const accessToken = generateAccessToken();
16746
17423
  const refreshToken = generateRefreshToken();
16747
- const expiresAt = new Date(Date.now() + config4.accessTokenTtlMs);
17424
+ const expiresAt = new Date(Date.now() + config5.accessTokenTtlMs);
16748
17425
  await runInTransaction6(async () => {
16749
17426
  await storeToken(accessToken, "access", grant.id, scopes, expiresAt);
16750
17427
  await storeToken(
@@ -16752,7 +17429,7 @@ async function issueTokenPair(grant, scopes) {
16752
17429
  "refresh",
16753
17430
  grant.id,
16754
17431
  scopes,
16755
- new Date(Date.now() + config4.refreshTokenTtlMs)
17432
+ new Date(Date.now() + config5.refreshTokenTtlMs)
16756
17433
  );
16757
17434
  });
16758
17435
  oauth2ClientsRepository.updateLastUsedById(grant.client).catch((err) => authLogger.service.error("Failed to update OAuth2 client lastUsedAt", err));
@@ -17100,6 +17777,47 @@ function waitTarget(body) {
17100
17777
  return { deviceCode, waitMillis };
17101
17778
  }
17102
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
+
17103
17821
  // src/server/routes/auth/index.ts
17104
17822
  var sendVerificationCode = route.post("/_auth/codes").input({
17105
17823
  body: Type.Object({
@@ -17311,6 +18029,101 @@ var denyDeviceAuth = route.post("/_auth/device/deny").input({
17311
18029
  await denyDeviceAuthService(body);
17312
18030
  return c.noContent();
17313
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
+ });
17314
18127
  var logout = route.post("/_auth/logout").handler(async (c) => {
17315
18128
  const auth = getAuth(c);
17316
18129
  if (!auth) {
@@ -17426,6 +18239,13 @@ var authRouter = defineRouter({
17426
18239
  getDeviceAuthInfo,
17427
18240
  approveDeviceAuth,
17428
18241
  denyDeviceAuth,
18242
+ issueDeviceLink,
18243
+ redeemDeviceLink,
18244
+ getDeviceLinkStatus,
18245
+ confirmDeviceLink,
18246
+ denyDeviceLink,
18247
+ cancelDeviceLink,
18248
+ pollDeviceLink,
17429
18249
  logout,
17430
18250
  rotateKey,
17431
18251
  listKeys,
@@ -17733,13 +18553,13 @@ var CanonicalJsonError = class extends Error {
17733
18553
  var INT64_MIN = -(2n ** 63n);
17734
18554
  var INT64_MAX = 2n ** 63n - 1n;
17735
18555
  function parseCanonicalJson(bytes) {
17736
- let text27;
18556
+ let text28;
17737
18557
  try {
17738
- text27 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
18558
+ text28 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
17739
18559
  } catch {
17740
18560
  throw new CanonicalJsonError("INVALID_UTF8");
17741
18561
  }
17742
- const parser = new Parser(text27);
18562
+ const parser = new Parser(text28);
17743
18563
  const value = parser.parseValue();
17744
18564
  parser.skipWhitespace();
17745
18565
  if (!parser.atEnd()) {
@@ -17760,8 +18580,8 @@ function isCanonicalBytes(bytes, value) {
17760
18580
  return true;
17761
18581
  }
17762
18582
  var Parser = class {
17763
- constructor(text27) {
17764
- this.text = text27;
18583
+ constructor(text28) {
18584
+ this.text = text28;
17765
18585
  }
17766
18586
  pos = 0;
17767
18587
  atEnd() {
@@ -18351,7 +19171,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
18351
19171
 
18352
19172
  // src/server/client-proof/contract-bundle.ts
18353
19173
  init_wire_headers();
18354
- var CONTRACT_VERSION = "0.13.1";
19174
+ var CONTRACT_VERSION = "0.13.2";
18355
19175
  var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
18356
19176
  function required(name, type) {
18357
19177
  return { name, type, optional: false };
@@ -18747,6 +19567,39 @@ var CONTRACT_TYPES = [
18747
19567
  fields: [
18748
19568
  required("userCode", "string")
18749
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
+ ]
18750
19603
  }
18751
19604
  ];
18752
19605
  var CONTRACT_ENUMS = [
@@ -19814,16 +20667,16 @@ function extractBearer2(header) {
19814
20667
  }
19815
20668
 
19816
20669
  // src/server/middleware/ops-or-user.ts
19817
- function opsOrUser(config4) {
19818
- const roles2 = config4.roles ?? [];
19819
- const permissions2 = config4.permissions ?? [];
19820
- 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) {
19821
20674
  throw new Error("opsOrUser: opsScopes must name at least one scope \u2014 an ops token would otherwise be admitted unchecked.");
19822
20675
  }
19823
20676
  if (roles2.length === 0 && permissions2.length === 0) {
19824
20677
  throw new Error("opsOrUser: give roles, permissions, or both \u2014 a user session would otherwise be admitted unchecked.");
19825
20678
  }
19826
- const ops = chain([opsTokenAuth.handler, requireOpsScope(...config4.opsScopes)]);
20679
+ const ops = chain([opsTokenAuth.handler, requireOpsScope(...config5.opsScopes)]);
19827
20680
  const user = chain([
19828
20681
  authenticate.handler,
19829
20682
  ...roles2.length > 0 ? [requireRole(...roles2)] : [],
@@ -19838,8 +20691,8 @@ function bearerOf(header) {
19838
20691
  function chain(handlers) {
19839
20692
  return async (c, next) => {
19840
20693
  let answer;
19841
- const runFrom = async (index21) => {
19842
- 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();
19843
20696
  if (produced instanceof Response) {
19844
20697
  answer = produced;
19845
20698
  }
@@ -20866,13 +21719,13 @@ var NO_STORE_HEADERS = {
20866
21719
  Pragma: "no-cache"
20867
21720
  };
20868
21721
  function requireAuthorizationServer() {
20869
- const config4 = getAuthorizationServerConfig();
20870
- if (!config4) {
21722
+ const config5 = getAuthorizationServerConfig();
21723
+ if (!config5) {
20871
21724
  throw new NotFoundError6({
20872
21725
  message: "This application does not run an OAuth 2.1 authorization server. Pass `authorizationServer` to createAuthLifecycle() to enable one."
20873
21726
  });
20874
21727
  }
20875
- return config4;
21728
+ return config5;
20876
21729
  }
20877
21730
  function oauth2ErrorResponse(c, status, error, description) {
20878
21731
  return c.json({ error, error_description: description }, status, NO_STORE_HEADERS);
@@ -20943,18 +21796,18 @@ var revokeOAuth2Grant = route14.delete("/_auth/oauth2/grants/:id").input({ param
20943
21796
  return { revoked: true };
20944
21797
  });
20945
21798
  var oauth2AuthorizationServerMetadata = route14.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
20946
- const config4 = requireAuthorizationServer();
21799
+ const config5 = requireAuthorizationServer();
20947
21800
  return c.json({
20948
- issuer: config4.issuer,
20949
- authorization_endpoint: config4.authorizeUrl,
20950
- token_endpoint: new URL("/_auth/oauth2/token", config4.issuer).toString(),
20951
- registration_endpoint: new URL("/_auth/oauth2/register", config4.issuer).toString(),
20952
- 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(),
20953
21806
  response_types_supported: SUPPORTED_RESPONSE_TYPES,
20954
21807
  grant_types_supported: SUPPORTED_GRANT_TYPES,
20955
21808
  token_endpoint_auth_methods_supported: ["none"],
20956
21809
  code_challenge_methods_supported: ["S256"],
20957
- scopes_supported: Object.keys(config4.scopes)
21810
+ scopes_supported: Object.keys(config5.scopes)
20958
21811
  });
20959
21812
  });
20960
21813
 
@@ -21054,6 +21907,14 @@ var mainAuthRouter = defineRouter6({
21054
21907
  getDeviceAuthInfo,
21055
21908
  approveDeviceAuth,
21056
21909
  denyDeviceAuth,
21910
+ // Device link routes
21911
+ issueDeviceLink,
21912
+ redeemDeviceLink,
21913
+ getDeviceLinkStatus,
21914
+ confirmDeviceLink,
21915
+ denyDeviceLink,
21916
+ cancelDeviceLink,
21917
+ pollDeviceLink,
21057
21918
  // Passkey routes (WebAuthn)
21058
21919
  passkeyRegisterOptions,
21059
21920
  passkeyRegisterVerify,
@@ -21541,6 +22402,7 @@ function assertOAuthRedirectUris(env21 = process.env) {
21541
22402
  function createAuthLifecycle(options = {}) {
21542
22403
  configureDeletion(options.deletion);
21543
22404
  configureDeviceAuth(options.deviceAuth);
22405
+ configureDeviceLink(options.deviceLink);
21544
22406
  configureAuthorizationServer(options.authorizationServer);
21545
22407
  return {
21546
22408
  /**
@@ -21657,11 +22519,14 @@ export {
21657
22519
  DEFAULT_DEVICE_AUTH_INTERVAL_MS,
21658
22520
  DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
21659
22521
  DEFAULT_DEVICE_AUTH_TTL_MS,
22522
+ DEFAULT_DEVICE_LINK_TTL_MS,
21660
22523
  DEFAULT_REFRESH_TOKEN_TTL_MS,
21661
22524
  DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,
21662
22525
  DEVICE_AUTH_STATUSES,
22526
+ DEVICE_LINK_STATUSES,
21663
22527
  DeviceAuthPollResponseSchema,
21664
22528
  DeviceAuthorizationsRepository,
22529
+ DeviceLinksRepository,
21665
22530
  DeviceNameSchema,
21666
22531
  EmailSchema,
21667
22532
  EnvironmentKeyringTokenCipher,
@@ -21675,11 +22540,13 @@ export {
21675
22540
  KeyIdSchema,
21676
22541
  KeyRevokeAllTokensRepository,
21677
22542
  KeysRepository,
22543
+ LinkIdSchema,
21678
22544
  MAX_UNGRANTED_CLIENTS_PER_IP,
21679
22545
  MFA_CHALLENGE_ATTEMPT_LIMIT,
21680
22546
  MFA_CHALLENGE_CHANNELS,
21681
22547
  MFA_CONFIRM_ATTEMPT_LIMIT,
21682
22548
  MFA_VERIFICATION_METHODS,
22549
+ MatchChoiceSchema,
21683
22550
  MfaChallengesRepository,
21684
22551
  MfaEnrolmentRepository,
21685
22552
  MfaRecoveryCodesRepository,
@@ -21764,6 +22631,7 @@ export {
21764
22631
  bearerAuthContext,
21765
22632
  buildOAuthErrorUrl,
21766
22633
  cancelAccountDeletionService,
22634
+ cancelDeviceLinkService,
21767
22635
  cancelInvitation,
21768
22636
  carryStepUpVerification,
21769
22637
  changePasswordService,
@@ -21774,7 +22642,9 @@ export {
21774
22642
  configureAuthorizationServer,
21775
22643
  configureDeletion,
21776
22644
  configureDeviceAuth,
22645
+ configureDeviceLink,
21777
22646
  configureOAuthTokenCipher,
22647
+ confirmDeviceLinkService,
21778
22648
  confirmPasswordResetService,
21779
22649
  confirmSignupLinkService,
21780
22650
  confirmTotpEnrolmentService,
@@ -21793,12 +22663,15 @@ export {
21793
22663
  deleteInvitation,
21794
22664
  deleteRole,
21795
22665
  denyDeviceAuthService,
22666
+ denyDeviceLinkService,
21796
22667
  denyOAuth2AuthorizeService,
21797
22668
  deriveCsrfToken,
21798
22669
  describeOAuth2AuthorizeRequestService,
21799
22670
  describeRevokeAllLink,
21800
22671
  deviceAuthorizations,
21801
22672
  deviceAuthorizationsRepository,
22673
+ deviceLinks,
22674
+ deviceLinksRepository,
21802
22675
  disableMfaService,
21803
22676
  disableSessionBindingService,
21804
22677
  enableSessionBindingService,
@@ -21833,6 +22706,8 @@ export {
21833
22706
  getDeletionConfig,
21834
22707
  getDeviceAuthConfig,
21835
22708
  getDeviceAuthInfoService,
22709
+ getDeviceLinkConfig,
22710
+ getDeviceLinkStatusService,
21836
22711
  getDummyPasswordHash,
21837
22712
  getEnabledOAuthProviders,
21838
22713
  getEncryptionKeyring,
@@ -21893,6 +22768,7 @@ export {
21893
22768
  isPkceS256ChallengeShaped,
21894
22769
  isPkceVerifierShaped,
21895
22770
  isSafeReturnPath,
22771
+ issueDeviceLinkService,
21896
22772
  issueOneTimeTokenService,
21897
22773
  issueOpsTokenService,
21898
22774
  kakaoProvider,
@@ -21960,9 +22836,11 @@ export {
21960
22836
  permissionsRepository,
21961
22837
  pkceChallengeFor,
21962
22838
  pollDeviceAuthService,
22839
+ pollDeviceLinkService,
21963
22840
  purgeRevokeAllTokensService,
21964
22841
  purgeStaleOAuth2ClientsService,
21965
22842
  purgeUserService,
22843
+ redeemDeviceLinkService,
21966
22844
  redirectHostOf,
21967
22845
  refreshAccessToken,
21968
22846
  refuseRedirectUriRegistration,