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

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(index12) {
901
- return CreateType({ [Kind]: "Argument", index: index12 });
900
+ function Argument(index13) {
901
+ return CreateType({ [Kind]: "Argument", index: index13 });
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, index12, char) {
1140
- return pattern[index12] === char && pattern.charCodeAt(index12 - 1) !== 92;
1139
+ function IsNonEscaped(pattern, index13, char) {
1140
+ return pattern[index13] === char && pattern.charCodeAt(index13 - 1) !== 92;
1141
1141
  }
1142
- function IsOpenParen(pattern, index12) {
1143
- return IsNonEscaped(pattern, index12, "(");
1142
+ function IsOpenParen(pattern, index13) {
1143
+ return IsNonEscaped(pattern, index13, "(");
1144
1144
  }
1145
- function IsCloseParen(pattern, index12) {
1146
- return IsNonEscaped(pattern, index12, ")");
1145
+ function IsCloseParen(pattern, index13) {
1146
+ return IsNonEscaped(pattern, index13, ")");
1147
1147
  }
1148
- function IsSeparator(pattern, index12) {
1149
- return IsNonEscaped(pattern, index12, "|");
1148
+ function IsSeparator(pattern, index13) {
1149
+ return IsNonEscaped(pattern, index13, "|");
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 count = 0;
1155
- for (let index12 = 0; index12 < pattern.length; index12++) {
1156
- if (IsOpenParen(pattern, index12))
1155
+ for (let index13 = 0; index13 < pattern.length; index13++) {
1156
+ if (IsOpenParen(pattern, index13))
1157
1157
  count += 1;
1158
- if (IsCloseParen(pattern, index12))
1158
+ if (IsCloseParen(pattern, index13))
1159
1159
  count -= 1;
1160
- if (count === 0 && index12 !== pattern.length - 1)
1160
+ if (count === 0 && index13 !== 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 count = 0;
1170
- for (let index12 = 0; index12 < pattern.length; index12++) {
1171
- if (IsOpenParen(pattern, index12))
1170
+ for (let index13 = 0; index13 < pattern.length; index13++) {
1171
+ if (IsOpenParen(pattern, index13))
1172
1172
  count += 1;
1173
- if (IsCloseParen(pattern, index12))
1173
+ if (IsCloseParen(pattern, index13))
1174
1174
  count -= 1;
1175
- if (IsSeparator(pattern, index12) && count === 0)
1175
+ if (IsSeparator(pattern, index13) && count === 0)
1176
1176
  return true;
1177
1177
  }
1178
1178
  return false;
1179
1179
  }
1180
1180
  function IsPrecedenceAnd(pattern) {
1181
- for (let index12 = 0; index12 < pattern.length; index12++) {
1182
- if (IsOpenParen(pattern, index12))
1181
+ for (let index13 = 0; index13 < pattern.length; index13++) {
1182
+ if (IsOpenParen(pattern, index13))
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 [count, start] = [0, 0];
1189
1189
  const expressions = [];
1190
- for (let index12 = 0; index12 < pattern.length; index12++) {
1191
- if (IsOpenParen(pattern, index12))
1190
+ for (let index13 = 0; index13 < pattern.length; index13++) {
1191
+ if (IsOpenParen(pattern, index13))
1192
1192
  count += 1;
1193
- if (IsCloseParen(pattern, index12))
1193
+ if (IsCloseParen(pattern, index13))
1194
1194
  count -= 1;
1195
- if (IsSeparator(pattern, index12) && count === 0) {
1196
- const range2 = pattern.slice(start, index12);
1195
+ if (IsSeparator(pattern, index13) && count === 0) {
1196
+ const range2 = pattern.slice(start, index13);
1197
1197
  if (range2.length > 0)
1198
1198
  expressions.push(TemplateLiteralParse(range2));
1199
- start = index12 + 1;
1199
+ start = index13 + 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, index12) {
1213
- if (!IsOpenParen(value, index12))
1212
+ function Group(value, index13) {
1213
+ if (!IsOpenParen(value, index13))
1214
1214
  throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
1215
1215
  let count = 0;
1216
- for (let scan = index12; scan < value.length; scan++) {
1216
+ for (let scan = index13; scan < value.length; scan++) {
1217
1217
  if (IsOpenParen(value, scan))
1218
1218
  count += 1;
1219
1219
  if (IsCloseParen(value, scan))
1220
1220
  count -= 1;
1221
1221
  if (count === 0)
1222
- return [index12, scan];
1222
+ return [index13, scan];
1223
1223
  }
1224
1224
  throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
1225
1225
  }
1226
- function Range(pattern2, index12) {
1227
- for (let scan = index12; scan < pattern2.length; scan++) {
1226
+ function Range(pattern2, index13) {
1227
+ for (let scan = index13; scan < pattern2.length; scan++) {
1228
1228
  if (IsOpenParen(pattern2, scan))
1229
- return [index12, scan];
1229
+ return [index13, scan];
1230
1230
  }
1231
- return [index12, pattern2.length];
1231
+ return [index13, pattern2.length];
1232
1232
  }
1233
1233
  const expressions = [];
1234
- for (let index12 = 0; index12 < pattern.length; index12++) {
1235
- if (IsOpenParen(pattern, index12)) {
1236
- const [start, end] = Group(pattern, index12);
1234
+ for (let index13 = 0; index13 < pattern.length; index13++) {
1235
+ if (IsOpenParen(pattern, index13)) {
1236
+ const [start, end] = Group(pattern, index13);
1237
1237
  const range = pattern.slice(start, end + 1);
1238
1238
  expressions.push(TemplateLiteralParse(range));
1239
- index12 = end;
1239
+ index13 = end;
1240
1240
  } else {
1241
- const [start, end] = Range(pattern, index12);
1241
+ const [start, end] = Range(pattern, index13);
1242
1242
  const range = pattern.slice(start, end);
1243
1243
  if (range.length > 0)
1244
1244
  expressions.push(TemplateLiteralParse(range));
1245
- index12 = end - 1;
1245
+ index13 = 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, index12) => IntoBooleanResult(Visit3(right.parameters[index12], 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, index13) => IntoBooleanResult(Visit3(right.parameters[index13], 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, index12) => IntoBooleanResult(Visit3(right.parameters[index12], 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, index13) => IntoBooleanResult(Visit3(right.parameters[index13], 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, index12) => Visit3(schema, right.items[index12]) === 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, index13) => Visit3(schema, right.items[index13]) === 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;
@@ -4978,9 +4978,59 @@ var init_verification_codes = __esm({
4978
4978
  }
4979
4979
  });
4980
4980
 
4981
+ // src/server/entities/signup-link-tokens.ts
4982
+ import { text as text7, index as index7, uniqueIndex as uniqueIndex2 } from "drizzle-orm/pg-core";
4983
+ import { id as id7, timestamps as timestamps6, utcTimestamp as utcTimestamp5 } from "@spfn/core/db";
4984
+ var signupLinkTokens;
4985
+ var init_signup_link_tokens = __esm({
4986
+ "src/server/entities/signup-link-tokens.ts"() {
4987
+ "use strict";
4988
+ init_schema4();
4989
+ signupLinkTokens = authSchema.table(
4990
+ "signup_link_tokens",
4991
+ {
4992
+ id: id7(),
4993
+ // Normalized email address the link was issued for
4994
+ email: text7("email").notNull(),
4995
+ // SHA-256 of the emailed token, base64url
4996
+ // The token itself (32 random bytes) is never stored
4997
+ tokenHash: text7("token_hash").notNull(),
4998
+ // Relative path to return the user to after signup completes
4999
+ // Validated as a relative path on the way in; never an absolute URL
5000
+ returnPath: text7("return_path"),
5001
+ // Link expiry — SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES from creation
5002
+ expiresAt: utcTimestamp5("expires_at").notNull(),
5003
+ // Set when the link is exchanged for a setup session
5004
+ // Non-null means the link is spent; it is one-time regardless of expiry
5005
+ consumedAt: utcTimestamp5("consumed_at"),
5006
+ // Set when a newer link for the same email replaced this one
5007
+ // A superseded row is refused even if it has not expired or been consumed
5008
+ supersededAt: utcTimestamp5("superseded_at"),
5009
+ // SHA-256 of the setup session secret, written at consume time
5010
+ // The secret lives only in the caller's HttpOnly cookie
5011
+ setupSecretHash: text7("setup_secret_hash"),
5012
+ // Setup session expiry — SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES from consume
5013
+ setupExpiresAt: utcTimestamp5("setup_expires_at"),
5014
+ // Terminal: the password was set and the account created
5015
+ completedAt: utcTimestamp5("completed_at"),
5016
+ ...timestamps6()
5017
+ },
5018
+ (table) => [
5019
+ // Lookup path for confirming a link
5020
+ uniqueIndex2("signup_link_token_hash_idx").on(table.tokenHash),
5021
+ // Lookup path for setting a password
5022
+ // Unique so a setup secret can never address two rows
5023
+ uniqueIndex2("signup_link_setup_secret_hash_idx").on(table.setupSecretHash),
5024
+ // Supersede-on-resend scans the live rows for one address
5025
+ index7("signup_link_email_idx").on(table.email, table.expiresAt)
5026
+ ]
5027
+ );
5028
+ }
5029
+ });
5030
+
4981
5031
  // src/server/entities/user-invitations.ts
4982
- import { text as text7, index as index7 } from "drizzle-orm/pg-core";
4983
- import { id as id7, timestamps as timestamps6, enumText as enumText5, utcTimestamp as utcTimestamp5, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
5032
+ import { text as text8, index as index8 } from "drizzle-orm/pg-core";
5033
+ import { id as id8, timestamps as timestamps7, enumText as enumText5, utcTimestamp as utcTimestamp6, typedJsonb as typedJsonb2, foreignKey as foreignKey5 } from "@spfn/core/db";
4984
5034
  var userInvitations;
4985
5035
  var init_user_invitations = __esm({
4986
5036
  "src/server/entities/user-invitations.ts"() {
@@ -4993,14 +5043,14 @@ var init_user_invitations = __esm({
4993
5043
  "user_invitations",
4994
5044
  {
4995
5045
  // Primary key
4996
- id: id7(),
5046
+ id: id8(),
4997
5047
  // Target email address for the invitation
4998
5048
  // Will become the user's email upon acceptance
4999
- email: text7("email").notNull(),
5049
+ email: text8("email").notNull(),
5000
5050
  // Unique invitation token (UUID v4)
5001
5051
  // Used in invitation URL: /auth/invite/{token}
5002
5052
  // Single-use token that expires after acceptance
5003
- token: text7("token").notNull().unique(),
5053
+ token: text8("token").notNull().unique(),
5004
5054
  // Role to be assigned when invitation is accepted
5005
5055
  // Foreign key to roles table
5006
5056
  roleId: foreignKey5("role", () => roles.id),
@@ -5017,15 +5067,15 @@ var init_user_invitations = __esm({
5017
5067
  // Expiration timestamp (default: 7 days from creation)
5018
5068
  // Invitation cannot be accepted after this time
5019
5069
  // Background job should update status to 'expired'
5020
- expiresAt: utcTimestamp5("expires_at").notNull(),
5070
+ expiresAt: utcTimestamp6("expires_at").notNull(),
5021
5071
  // Timestamp when invitation was accepted
5022
5072
  // null = not yet accepted
5023
5073
  // Used for: audit trail, analytics
5024
- acceptedAt: utcTimestamp5("accepted_at"),
5074
+ acceptedAt: utcTimestamp6("accepted_at"),
5025
5075
  // Timestamp when invitation was cancelled
5026
5076
  // null = not cancelled
5027
5077
  // Used for: audit trail
5028
- cancelledAt: utcTimestamp5("cancelled_at"),
5078
+ cancelledAt: utcTimestamp6("cancelled_at"),
5029
5079
  // Additional metadata (JSONB)
5030
5080
  // Use cases:
5031
5081
  // - Custom welcome message
@@ -5034,26 +5084,26 @@ var init_user_invitations = __esm({
5034
5084
  // - Custom fields for app-specific data
5035
5085
  // Example: { message: "Welcome!", department: "Engineering" }
5036
5086
  metadata: typedJsonb2("metadata"),
5037
- ...timestamps6()
5087
+ ...timestamps7()
5038
5088
  },
5039
5089
  (table) => [
5040
5090
  // Indexes for query optimization
5041
- index7("invitations_token_idx").on(table.token),
5042
- index7("invitations_email_idx").on(table.email),
5043
- index7("invitations_status_idx").on(table.status),
5044
- index7("invitations_invited_by_idx").on(table.invitedBy),
5045
- index7("invitations_expires_at_idx").on(table.expiresAt),
5091
+ index8("invitations_token_idx").on(table.token),
5092
+ index8("invitations_email_idx").on(table.email),
5093
+ index8("invitations_status_idx").on(table.status),
5094
+ index8("invitations_invited_by_idx").on(table.invitedBy),
5095
+ index8("invitations_expires_at_idx").on(table.expiresAt),
5046
5096
  // For cleanup jobs
5047
- index7("invitations_role_id_idx").on(table.roleId)
5097
+ index8("invitations_role_id_idx").on(table.roleId)
5048
5098
  ]
5049
5099
  );
5050
5100
  }
5051
5101
  });
5052
5102
 
5053
5103
  // src/server/entities/account-deletion-requests.ts
5054
- import { text as text8, index as index8, uniqueIndex as uniqueIndex2 } from "drizzle-orm/pg-core";
5104
+ import { text as text9, index as index9, uniqueIndex as uniqueIndex3 } from "drizzle-orm/pg-core";
5055
5105
  import { sql as sql2 } from "drizzle-orm";
5056
- import { id as id8, timestamps as timestamps7, enumText as enumText6, utcTimestamp as utcTimestamp6, optionalForeignKey } from "@spfn/core/db";
5106
+ import { id as id9, timestamps as timestamps8, enumText as enumText6, utcTimestamp as utcTimestamp7, optionalForeignKey } from "@spfn/core/db";
5057
5107
  var accountDeletionRequests;
5058
5108
  var init_account_deletion_requests = __esm({
5059
5109
  "src/server/entities/account-deletion-requests.ts"() {
@@ -5064,18 +5114,18 @@ var init_account_deletion_requests = __esm({
5064
5114
  accountDeletionRequests = authSchema.table(
5065
5115
  "account_deletion_requests",
5066
5116
  {
5067
- id: id8(),
5117
+ id: id9(),
5068
5118
  // Foreign key to users table. `set null` (optionalForeignKey default) so this
5069
5119
  // row survives a hard-delete purge of the user it refers to.
5070
5120
  userId: optionalForeignKey("user", () => users.id),
5071
5121
  // Snapshot of the user's public UUID at request time — stays readable even
5072
5122
  // after userId is nulled out or the account is anonymized.
5073
- userPublicId: text8("user_public_id").notNull(),
5123
+ userPublicId: text9("user_public_id").notNull(),
5074
5124
  // When the deletion was requested
5075
- requestedAt: utcTimestamp6("requested_at").notNull().defaultNow(),
5125
+ requestedAt: utcTimestamp7("requested_at").notNull().defaultNow(),
5076
5126
  // When the purge job is allowed to run (requestedAt + grace period; equals
5077
5127
  // requestedAt itself for immediate/zero-grace deletions)
5078
- purgeScheduledAt: utcTimestamp6("purge_scheduled_at").notNull(),
5128
+ purgeScheduledAt: utcTimestamp7("purge_scheduled_at").notNull(),
5079
5129
  // Request lifecycle status
5080
5130
  // - pending: awaiting purgeScheduledAt (or immediate purge)
5081
5131
  // - cancelled: recovered before purge
@@ -5084,20 +5134,20 @@ var init_account_deletion_requests = __esm({
5084
5134
  // Who initiated the request
5085
5135
  requestedBy: enumText6("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
5086
5136
  // Optional free-text reason (self-service UI, admin note, DSR reference, ...)
5087
- reason: text8("reason"),
5088
- cancelledAt: utcTimestamp6("cancelled_at"),
5089
- completedAt: utcTimestamp6("completed_at"),
5137
+ reason: text9("reason"),
5138
+ cancelledAt: utcTimestamp7("cancelled_at"),
5139
+ completedAt: utcTimestamp7("completed_at"),
5090
5140
  // Purge strategy actually executed (set on completion; null while pending)
5091
5141
  purgeStrategy: enumText6("purge_strategy", PURGE_STRATEGIES),
5092
- ...timestamps7()
5142
+ ...timestamps8()
5093
5143
  },
5094
5144
  (table) => [
5095
- index8("account_deletion_requests_user_id_idx").on(table.userId),
5096
- index8("account_deletion_requests_status_idx").on(table.status),
5097
- index8("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
5098
- index8("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
5145
+ index9("account_deletion_requests_user_id_idx").on(table.userId),
5146
+ index9("account_deletion_requests_status_idx").on(table.status),
5147
+ index9("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
5148
+ index9("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
5099
5149
  // Partial unique index: at most one pending request per user at a time.
5100
- uniqueIndex2("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5150
+ uniqueIndex3("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
5101
5151
  ]
5102
5152
  );
5103
5153
  }
@@ -5249,8 +5299,8 @@ var init_rbac = __esm({
5249
5299
  });
5250
5300
 
5251
5301
  // src/server/entities/permissions.ts
5252
- import { text as text9, boolean as boolean4, index as index9 } from "drizzle-orm/pg-core";
5253
- import { id as id9, timestamps as timestamps8, enumText as enumText7, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5302
+ import { text as text10, boolean as boolean4, index as index10 } from "drizzle-orm/pg-core";
5303
+ import { id as id10, timestamps as timestamps9, enumText as enumText7, typedJsonb as typedJsonb3 } from "@spfn/core/db";
5254
5304
  var permissions;
5255
5305
  var init_permissions = __esm({
5256
5306
  "src/server/entities/permissions.ts"() {
@@ -5261,7 +5311,7 @@ var init_permissions = __esm({
5261
5311
  "permissions",
5262
5312
  {
5263
5313
  // Primary key
5264
- id: id9(),
5314
+ id: id10(),
5265
5315
  // Permission identifier
5266
5316
  // Format: resource:action or namespace:resource:action
5267
5317
  // Examples:
@@ -5269,15 +5319,15 @@ var init_permissions = __esm({
5269
5319
  // - Namespaced: 'auth:user:delete', 'cms:post:publish'
5270
5320
  // Must be unique across all permissions
5271
5321
  // Used in: permission checks, role assignments, API guards
5272
- name: text9("name").notNull().unique(),
5322
+ name: text10("name").notNull().unique(),
5273
5323
  // Display name for UI
5274
5324
  // Human-readable name shown in admin panels
5275
5325
  // Example: "Delete Users", "Publish Posts"
5276
- displayName: text9("display_name").notNull(),
5326
+ displayName: text10("display_name").notNull(),
5277
5327
  // Permission description
5278
5328
  // Detailed explanation of what this permission allows
5279
5329
  // Example: "Allows deletion of user accounts from the system"
5280
- description: text9("description"),
5330
+ description: text10("description"),
5281
5331
  // Category for grouping
5282
5332
  // Used for: organizing permissions in UI, filtering
5283
5333
  // Built-in categories: auth, user, rbac, system
@@ -5313,22 +5363,22 @@ var init_permissions = __esm({
5313
5363
  // - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
5314
5364
  // Example: { icon: 'trash', color: 'red', requiresMfa: true }
5315
5365
  metadata: typedJsonb3("metadata"),
5316
- ...timestamps8()
5366
+ ...timestamps9()
5317
5367
  },
5318
5368
  (table) => [
5319
- index9("permissions_name_idx").on(table.name),
5320
- index9("permissions_category_idx").on(table.category),
5321
- index9("permissions_is_system_idx").on(table.isSystem),
5322
- index9("permissions_is_active_idx").on(table.isActive),
5323
- index9("permissions_is_builtin_idx").on(table.isBuiltin)
5369
+ index10("permissions_name_idx").on(table.name),
5370
+ index10("permissions_category_idx").on(table.category),
5371
+ index10("permissions_is_system_idx").on(table.isSystem),
5372
+ index10("permissions_is_active_idx").on(table.isActive),
5373
+ index10("permissions_is_builtin_idx").on(table.isBuiltin)
5324
5374
  ]
5325
5375
  );
5326
5376
  }
5327
5377
  });
5328
5378
 
5329
5379
  // src/server/entities/role-permissions.ts
5330
- import { index as index10, unique } from "drizzle-orm/pg-core";
5331
- import { id as id10, timestamps as timestamps9, foreignKey as foreignKey6 } from "@spfn/core/db";
5380
+ import { index as index11, unique } from "drizzle-orm/pg-core";
5381
+ import { id as id11, timestamps as timestamps10, foreignKey as foreignKey6 } from "@spfn/core/db";
5332
5382
  var rolePermissions;
5333
5383
  var init_role_permissions = __esm({
5334
5384
  "src/server/entities/role-permissions.ts"() {
@@ -5340,7 +5390,7 @@ var init_role_permissions = __esm({
5340
5390
  "role_permissions",
5341
5391
  {
5342
5392
  // Primary key
5343
- id: id10(),
5393
+ id: id11(),
5344
5394
  // Role reference
5345
5395
  // Foreign key to roles table
5346
5396
  // Cascade delete: when role is deleted, all role-permission mappings are removed
@@ -5353,12 +5403,12 @@ var init_role_permissions = __esm({
5353
5403
  // Used for: granting permissions to roles
5354
5404
  // Example: user:delete permission → [Admin, Superadmin]
5355
5405
  permissionId: foreignKey6("permission", () => permissions.id, { onDelete: "cascade" }),
5356
- ...timestamps9()
5406
+ ...timestamps10()
5357
5407
  },
5358
5408
  (table) => [
5359
5409
  // Indexes for query performance
5360
- index10("role_permissions_role_id_idx").on(table.roleId),
5361
- index10("role_permissions_permission_id_idx").on(table.permissionId),
5410
+ index11("role_permissions_role_id_idx").on(table.roleId),
5411
+ index11("role_permissions_permission_id_idx").on(table.permissionId),
5362
5412
  // Unique constraint: one role-permission pair only
5363
5413
  unique("role_permissions_unique").on(table.roleId, table.permissionId)
5364
5414
  ]
@@ -5367,8 +5417,8 @@ var init_role_permissions = __esm({
5367
5417
  });
5368
5418
 
5369
5419
  // src/server/entities/user-permissions.ts
5370
- import { boolean as boolean5, text as text10, index as index11, unique as unique2 } from "drizzle-orm/pg-core";
5371
- import { id as id11, timestamps as timestamps10, utcTimestamp as utcTimestamp7, foreignKey as foreignKey7 } from "@spfn/core/db";
5420
+ import { boolean as boolean5, text as text11, index as index12, unique as unique2 } from "drizzle-orm/pg-core";
5421
+ import { id as id12, timestamps as timestamps11, utcTimestamp as utcTimestamp8, foreignKey as foreignKey7 } from "@spfn/core/db";
5372
5422
  var userPermissions;
5373
5423
  var init_user_permissions = __esm({
5374
5424
  "src/server/entities/user-permissions.ts"() {
@@ -5380,7 +5430,7 @@ var init_user_permissions = __esm({
5380
5430
  "user_permissions",
5381
5431
  {
5382
5432
  // Primary key
5383
- id: id11(),
5433
+ id: id12(),
5384
5434
  // User reference
5385
5435
  // Foreign key to users table
5386
5436
  // Cascade delete: when user is deleted, all overrides are removed
@@ -5403,19 +5453,19 @@ var init_user_permissions = __esm({
5403
5453
  // Reason for grant/revocation
5404
5454
  // Used for: audit trail, compliance documentation
5405
5455
  // Example: "Temporary access for project X", "Security incident - restricted"
5406
- reason: text10("reason"),
5456
+ reason: text11("reason"),
5407
5457
  // Expiration timestamp (optional)
5408
5458
  // null: Permanent override (remains until manually removed)
5409
5459
  // timestamp: Permission expires at this time (auto-revoked by background job)
5410
5460
  // Use case: Time-limited elevated access, temporary restrictions
5411
- expiresAt: utcTimestamp7("expires_at"),
5412
- ...timestamps10()
5461
+ expiresAt: utcTimestamp8("expires_at"),
5462
+ ...timestamps11()
5413
5463
  },
5414
5464
  (table) => [
5415
5465
  // Indexes for query performance
5416
- index11("user_permissions_user_id_idx").on(table.userId),
5417
- index11("user_permissions_permission_id_idx").on(table.permissionId),
5418
- index11("user_permissions_expires_at_idx").on(table.expiresAt),
5466
+ index12("user_permissions_user_id_idx").on(table.userId),
5467
+ index12("user_permissions_permission_id_idx").on(table.permissionId),
5468
+ index12("user_permissions_expires_at_idx").on(table.expiresAt),
5419
5469
  // Unique constraint: one user-permission pair only
5420
5470
  unique2("user_permissions_unique").on(table.userId, table.permissionId)
5421
5471
  ]
@@ -5425,7 +5475,7 @@ var init_user_permissions = __esm({
5425
5475
 
5426
5476
  // src/server/entities/auth-metadata.ts
5427
5477
  import { sql as sql3 } from "drizzle-orm";
5428
- import { text as text11, timestamp } from "drizzle-orm/pg-core";
5478
+ import { text as text12, timestamp } from "drizzle-orm/pg-core";
5429
5479
  var authMetadata;
5430
5480
  var init_auth_metadata = __esm({
5431
5481
  "src/server/entities/auth-metadata.ts"() {
@@ -5435,9 +5485,9 @@ var init_auth_metadata = __esm({
5435
5485
  "auth_metadata",
5436
5486
  {
5437
5487
  // Metadata key (primary key)
5438
- key: text11("key").primaryKey(),
5488
+ key: text12("key").primaryKey(),
5439
5489
  // Metadata value
5440
- value: text11("value").notNull(),
5490
+ value: text12("value").notNull(),
5441
5491
  // Last updated timestamp — stamped by the database on insert and on update
5442
5492
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
5443
5493
  }
@@ -5446,8 +5496,8 @@ var init_auth_metadata = __esm({
5446
5496
  });
5447
5497
 
5448
5498
  // src/server/entities/ops-tokens.ts
5449
- import { text as text12 } from "drizzle-orm/pg-core";
5450
- import { id as id12, timestamps as timestamps11, utcTimestamp as utcTimestamp8 } from "@spfn/core/db";
5499
+ import { text as text13 } from "drizzle-orm/pg-core";
5500
+ import { id as id13, timestamps as timestamps12, utcTimestamp as utcTimestamp9 } from "@spfn/core/db";
5451
5501
  var opsTokens;
5452
5502
  var init_ops_tokens = __esm({
5453
5503
  "src/server/entities/ops-tokens.ts"() {
@@ -5456,22 +5506,22 @@ var init_ops_tokens = __esm({
5456
5506
  opsTokens = authSchema.table(
5457
5507
  "ops_tokens",
5458
5508
  {
5459
- id: id12(),
5509
+ id: id13(),
5460
5510
  // Operator-facing label ("ci-deploy", "rayim-laptop")
5461
- name: text12("name").notNull(),
5511
+ name: text13("name").notNull(),
5462
5512
  // SHA-256 hex of the token secret. Lookup key — the secret never lands
5463
5513
  // here, and the unique constraint doubles as the lookup index.
5464
- tokenHash: text12("token_hash").notNull().unique(),
5514
+ tokenHash: text13("token_hash").notNull().unique(),
5465
5515
  // Granted scopes as permission strings ('waitlist:read', ...).
5466
5516
  // '*' grants every scope.
5467
- scopes: text12("scopes").array().notNull(),
5517
+ scopes: text13("scopes").array().notNull(),
5468
5518
  // null = the token does not expire
5469
- expiresAt: utcTimestamp8("expires_at"),
5519
+ expiresAt: utcTimestamp9("expires_at"),
5470
5520
  // null = active; a timestamp revokes the token permanently
5471
- revokedAt: utcTimestamp8("revoked_at"),
5521
+ revokedAt: utcTimestamp9("revoked_at"),
5472
5522
  // Last successful verification, updated fire-and-forget
5473
- lastUsedAt: utcTimestamp8("last_used_at"),
5474
- ...timestamps11()
5523
+ lastUsedAt: utcTimestamp9("last_used_at"),
5524
+ ...timestamps12()
5475
5525
  }
5476
5526
  );
5477
5527
  }
@@ -5487,6 +5537,7 @@ var init_entities = __esm({
5487
5537
  init_user_public_keys();
5488
5538
  init_user_social_accounts();
5489
5539
  init_verification_codes();
5540
+ init_signup_link_tokens();
5490
5541
  init_user_invitations();
5491
5542
  init_account_deletion_requests();
5492
5543
  init_roles();
@@ -5513,8 +5564,8 @@ var init_users_repository = __esm({
5513
5564
  * ID로 사용자 조회
5514
5565
  * Read replica 사용
5515
5566
  */
5516
- async findById(id13) {
5517
- const result = await this.readDb.select().from(users).where(eq(users.id, id13)).limit(1);
5567
+ async findById(id14) {
5568
+ const result = await this.readDb.select().from(users).where(eq(users.id, id14)).limit(1);
5518
5569
  return result[0] ?? null;
5519
5570
  }
5520
5571
  /**
@@ -5524,8 +5575,8 @@ var init_users_repository = __esm({
5524
5575
  * 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
5525
5576
  * 계속 사용할 것.
5526
5577
  */
5527
- async findByIdOnPrimary(id13) {
5528
- const result = await this.db.select().from(users).where(eq(users.id, id13)).limit(1);
5578
+ async findByIdOnPrimary(id14) {
5579
+ const result = await this.db.select().from(users).where(eq(users.id, id14)).limit(1);
5529
5580
  return result[0] ?? null;
5530
5581
  }
5531
5582
  /**
@@ -5596,13 +5647,13 @@ var init_users_repository = __esm({
5596
5647
  *
5597
5648
  * roleId가 null인 유저는 role: null 반환
5598
5649
  */
5599
- async findByIdWithRole(id13) {
5650
+ async findByIdWithRole(id14) {
5600
5651
  const result = await this.readDb.select({
5601
5652
  user: users,
5602
5653
  roleName: roles.name,
5603
5654
  roleDisplayName: roles.displayName,
5604
5655
  rolePriority: roles.priority
5605
- }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id13)).limit(1);
5656
+ }).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id14)).limit(1);
5606
5657
  const row = result[0];
5607
5658
  if (!row) {
5608
5659
  return null;
@@ -5677,9 +5728,9 @@ var init_users_repository = __esm({
5677
5728
  * 사용자 정보 업데이트
5678
5729
  * Write primary 사용
5679
5730
  */
5680
- async updateById(id13, data) {
5731
+ async updateById(id14, data) {
5681
5732
  const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
5682
- const result = await this.db.update(users).set(patch).where(eq(users.id, id13)).returning();
5733
+ const result = await this.db.update(users).set(patch).where(eq(users.id, id14)).returning();
5683
5734
  return result[0] ?? null;
5684
5735
  }
5685
5736
  /**
@@ -5691,10 +5742,10 @@ var init_users_repository = __esm({
5691
5742
  * status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
5692
5743
  * Write primary 사용
5693
5744
  */
5694
- async reactivateFromPendingDeletion(id13) {
5745
+ async reactivateFromPendingDeletion(id14) {
5695
5746
  const result = await this.db.update(users).set({ status: "active" }).where(
5696
5747
  and(
5697
- eq(users.id, id13),
5748
+ eq(users.id, id14),
5698
5749
  eq(users.status, "pending_deletion")
5699
5750
  )
5700
5751
  ).returning();
@@ -5704,32 +5755,32 @@ var init_users_repository = __esm({
5704
5755
  * 비밀번호 업데이트
5705
5756
  * Write primary 사용
5706
5757
  */
5707
- async updatePassword(id13, passwordHash, clearPasswordChangeRequired = true) {
5758
+ async updatePassword(id14, passwordHash, clearPasswordChangeRequired = true) {
5708
5759
  const updateData = {
5709
5760
  passwordHash
5710
5761
  };
5711
5762
  if (clearPasswordChangeRequired) {
5712
5763
  updateData.passwordChangeRequired = false;
5713
5764
  }
5714
- const result = await this.db.update(users).set(updateData).where(eq(users.id, id13)).returning();
5765
+ const result = await this.db.update(users).set(updateData).where(eq(users.id, id14)).returning();
5715
5766
  return result[0] ?? null;
5716
5767
  }
5717
5768
  /**
5718
5769
  * 마지막 로그인 시간 업데이트
5719
5770
  * Write primary 사용
5720
5771
  */
5721
- async updateLastLogin(id13) {
5772
+ async updateLastLogin(id14) {
5722
5773
  const result = await this.db.update(users).set({
5723
5774
  lastLoginAt: /* @__PURE__ */ new Date()
5724
- }).where(eq(users.id, id13)).returning();
5775
+ }).where(eq(users.id, id14)).returning();
5725
5776
  return result[0] ?? null;
5726
5777
  }
5727
5778
  /**
5728
5779
  * 사용자 삭제
5729
5780
  * Write primary 사용
5730
5781
  */
5731
- async deleteById(id13) {
5732
- const result = await this.db.delete(users).where(eq(users.id, id13)).returning();
5782
+ async deleteById(id14) {
5783
+ const result = await this.db.delete(users).where(eq(users.id, id14)).returning();
5733
5784
  return result[0] ?? null;
5734
5785
  }
5735
5786
  /**
@@ -6077,14 +6128,14 @@ var init_keys_repository = __esm({
6077
6128
  * stored, so it answers "since when has this device been on this release"
6078
6129
  * rather than "when was it last seen", which lastUsedAt already answers.
6079
6130
  */
6080
- async updateLastUsedById(id13, identity) {
6131
+ async updateLastUsedById(id14, identity) {
6081
6132
  const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
6082
6133
  const lastUsedIsStale = or(
6083
6134
  isNull(userPublicKeys.lastUsedAt),
6084
6135
  lt(userPublicKeys.lastUsedAt, staleBefore)
6085
6136
  );
6086
6137
  if (!identity) {
6087
- await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id13), lastUsedIsStale));
6138
+ await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id14), lastUsedIsStale));
6088
6139
  return;
6089
6140
  }
6090
6141
  const identityChanged = sql5`(
@@ -6101,7 +6152,7 @@ var init_keys_repository = __esm({
6101
6152
  clientContractVersion: identity.contractVersion,
6102
6153
  clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
6103
6154
  }).where(and2(
6104
- eq2(userPublicKeys.id, id13),
6155
+ eq2(userPublicKeys.id, id14),
6105
6156
  or(lastUsedIsStale, identityChanged)
6106
6157
  ));
6107
6158
  }
@@ -6140,8 +6191,8 @@ var init_verification_codes_repository = __esm({
6140
6191
  * ID로 인증 코드 조회
6141
6192
  * Read replica 사용
6142
6193
  */
6143
- async findById(id13) {
6144
- const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id13)).limit(1);
6194
+ async findById(id14) {
6195
+ const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id14)).limit(1);
6145
6196
  return result[0] ?? null;
6146
6197
  }
6147
6198
  /**
@@ -6155,22 +6206,22 @@ var init_verification_codes_repository = __esm({
6155
6206
  * 인증 코드 사용 처리
6156
6207
  * Write primary 사용
6157
6208
  */
6158
- async markAsUsed(id13) {
6209
+ async markAsUsed(id14) {
6159
6210
  const result = await this.db.update(verificationCodes).set({
6160
6211
  usedAt: /* @__PURE__ */ new Date()
6161
- }).where(eq3(verificationCodes.id, id13)).returning();
6212
+ }).where(eq3(verificationCodes.id, id14)).returning();
6162
6213
  return result[0] ?? null;
6163
6214
  }
6164
6215
  /**
6165
6216
  * 시도 횟수 증가
6166
6217
  * Write primary 사용
6167
6218
  */
6168
- async incrementAttempts(id13) {
6169
- const code = await this.findById(id13);
6219
+ async incrementAttempts(id14) {
6220
+ const code = await this.findById(id14);
6170
6221
  if (!code) return null;
6171
6222
  const result = await this.db.update(verificationCodes).set({
6172
6223
  attempts: code.attempts + 1
6173
- }).where(eq3(verificationCodes.id, id13)).returning();
6224
+ }).where(eq3(verificationCodes.id, id14)).returning();
6174
6225
  return result[0] ?? null;
6175
6226
  }
6176
6227
  /**
@@ -6214,27 +6265,136 @@ var init_verification_codes_repository = __esm({
6214
6265
  }
6215
6266
  });
6216
6267
 
6217
- // src/server/repositories/roles.repository.ts
6268
+ // src/server/repositories/signup-link-tokens.repository.ts
6218
6269
  import { BaseRepository as BaseRepository4 } from "@spfn/core/db";
6219
- import { eq as eq4, asc } from "drizzle-orm";
6270
+ import { eq as eq4, and as and4, isNull as isNull3 } from "drizzle-orm";
6271
+ var SignupLinkTokensRepository, signupLinkTokensRepository;
6272
+ var init_signup_link_tokens_repository = __esm({
6273
+ "src/server/repositories/signup-link-tokens.repository.ts"() {
6274
+ "use strict";
6275
+ init_signup_link_tokens();
6276
+ SignupLinkTokensRepository = class extends BaseRepository4 {
6277
+ /**
6278
+ * Create a signup link row.
6279
+ * Write primary.
6280
+ */
6281
+ async create(data) {
6282
+ const result = await this.db.insert(signupLinkTokens).values(data).returning();
6283
+ return result[0];
6284
+ }
6285
+ /**
6286
+ * Find a row by the hash of an emailed token, in any state.
6287
+ *
6288
+ * Deliberately unfiltered: the caller decides why a link is refused, and a
6289
+ * row filtered out here would be indistinguishable from an unknown token in
6290
+ * the logs.
6291
+ *
6292
+ * Read replica.
6293
+ */
6294
+ async findByTokenHash(tokenHash) {
6295
+ const result = await this.readDb.select().from(signupLinkTokens).where(eq4(signupLinkTokens.tokenHash, tokenHash)).limit(1);
6296
+ return result[0] ?? null;
6297
+ }
6298
+ /**
6299
+ * Find a row by the hash of a setup session secret, in any state.
6300
+ * Read replica.
6301
+ */
6302
+ async findBySetupSecretHash(setupSecretHash) {
6303
+ const result = await this.readDb.select().from(signupLinkTokens).where(eq4(signupLinkTokens.setupSecretHash, setupSecretHash)).limit(1);
6304
+ return result[0] ?? null;
6305
+ }
6306
+ /**
6307
+ * Consume a link and open a setup session on it, but only if it is still
6308
+ * unconsumed and not superseded.
6309
+ *
6310
+ * @returns the updated row, or null if another request claimed it first
6311
+ */
6312
+ async claimLink(id14, setupSecretHash, setupExpiresAt) {
6313
+ const result = await this.db.update(signupLinkTokens).set({
6314
+ consumedAt: /* @__PURE__ */ new Date(),
6315
+ setupSecretHash,
6316
+ setupExpiresAt
6317
+ }).where(
6318
+ and4(
6319
+ eq4(signupLinkTokens.id, id14),
6320
+ isNull3(signupLinkTokens.consumedAt),
6321
+ isNull3(signupLinkTokens.supersededAt)
6322
+ )
6323
+ ).returning();
6324
+ return result[0] ?? null;
6325
+ }
6326
+ /**
6327
+ * Mark a setup session as completed, but only if it has not completed
6328
+ * already.
6329
+ *
6330
+ * @returns the updated row, or null if another request completed it first
6331
+ */
6332
+ async claimSetupSession(id14) {
6333
+ const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
6334
+ and4(
6335
+ eq4(signupLinkTokens.id, id14),
6336
+ isNull3(signupLinkTokens.completedAt),
6337
+ isNull3(signupLinkTokens.supersededAt)
6338
+ )
6339
+ ).returning();
6340
+ return result[0] ?? null;
6341
+ }
6342
+ /**
6343
+ * Supersede every live link for an address, so requesting a new one
6344
+ * invalidates the previous one and any setup session opened from it.
6345
+ *
6346
+ * Write primary.
6347
+ *
6348
+ * @returns number of rows superseded
6349
+ */
6350
+ async supersedeLiveForEmail(email) {
6351
+ const result = await this.db.update(signupLinkTokens).set({ supersededAt: /* @__PURE__ */ new Date() }).where(
6352
+ and4(
6353
+ eq4(signupLinkTokens.email, email),
6354
+ isNull3(signupLinkTokens.supersededAt),
6355
+ isNull3(signupLinkTokens.completedAt)
6356
+ )
6357
+ ).returning();
6358
+ return result.length;
6359
+ }
6360
+ /**
6361
+ * Delete every signup link row for an address (account destruction).
6362
+ *
6363
+ * Rows key on the email text and carry no user FK, so destruction has to
6364
+ * clean them up by address the way verification codes are cleaned up.
6365
+ *
6366
+ * Write primary.
6367
+ */
6368
+ async deleteByEmail(email) {
6369
+ const result = await this.db.delete(signupLinkTokens).where(eq4(signupLinkTokens.email, email)).returning();
6370
+ return result.length;
6371
+ }
6372
+ };
6373
+ signupLinkTokensRepository = new SignupLinkTokensRepository();
6374
+ }
6375
+ });
6376
+
6377
+ // src/server/repositories/roles.repository.ts
6378
+ import { BaseRepository as BaseRepository5 } from "@spfn/core/db";
6379
+ import { eq as eq5, asc } from "drizzle-orm";
6220
6380
  var RolesRepository, rolesRepository;
6221
6381
  var init_roles_repository = __esm({
6222
6382
  "src/server/repositories/roles.repository.ts"() {
6223
6383
  "use strict";
6224
6384
  init_roles();
6225
- RolesRepository = class extends BaseRepository4 {
6385
+ RolesRepository = class extends BaseRepository5 {
6226
6386
  /**
6227
6387
  * ID로 역할 조회
6228
6388
  */
6229
- async findById(id13) {
6230
- const result = await this.readDb.select().from(roles).where(eq4(roles.id, id13)).limit(1);
6389
+ async findById(id14) {
6390
+ const result = await this.readDb.select().from(roles).where(eq5(roles.id, id14)).limit(1);
6231
6391
  return result[0] ?? null;
6232
6392
  }
6233
6393
  /**
6234
6394
  * Name으로 역할 조회
6235
6395
  */
6236
6396
  async findByName(name) {
6237
- const result = await this.readDb.select().from(roles).where(eq4(roles.name, name)).limit(1);
6397
+ const result = await this.readDb.select().from(roles).where(eq5(roles.name, name)).limit(1);
6238
6398
  return result[0] ?? null;
6239
6399
  }
6240
6400
  /**
@@ -6247,7 +6407,7 @@ var init_roles_repository = __esm({
6247
6407
  * 활성 역할만 조회
6248
6408
  */
6249
6409
  async findActive() {
6250
- return this.readDb.select().from(roles).where(eq4(roles.isActive, true)).orderBy(asc(roles.priority));
6410
+ return this.readDb.select().from(roles).where(eq5(roles.isActive, true)).orderBy(asc(roles.priority));
6251
6411
  }
6252
6412
  /**
6253
6413
  * 역할 생성
@@ -6258,15 +6418,15 @@ var init_roles_repository = __esm({
6258
6418
  /**
6259
6419
  * 역할 업데이트
6260
6420
  */
6261
- async updateById(id13, data) {
6262
- const result = await this.db.update(roles).set(data).where(eq4(roles.id, id13)).returning();
6421
+ async updateById(id14, data) {
6422
+ const result = await this.db.update(roles).set(data).where(eq5(roles.id, id14)).returning();
6263
6423
  return result[0] ?? null;
6264
6424
  }
6265
6425
  /**
6266
6426
  * 역할 삭제
6267
6427
  */
6268
- async deleteById(id13) {
6269
- const result = await this.db.delete(roles).where(eq4(roles.id, id13)).returning();
6428
+ async deleteById(id14) {
6429
+ const result = await this.db.delete(roles).where(eq5(roles.id, id14)).returning();
6270
6430
  return result[0] ?? null;
6271
6431
  }
6272
6432
  };
@@ -6275,26 +6435,26 @@ var init_roles_repository = __esm({
6275
6435
  });
6276
6436
 
6277
6437
  // src/server/repositories/permissions.repository.ts
6278
- import { BaseRepository as BaseRepository5 } from "@spfn/core/db";
6279
- import { asc as asc2, eq as eq5, inArray } from "drizzle-orm";
6438
+ import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
6439
+ import { asc as asc2, eq as eq6, inArray } from "drizzle-orm";
6280
6440
  var PermissionsRepository, permissionsRepository;
6281
6441
  var init_permissions_repository = __esm({
6282
6442
  "src/server/repositories/permissions.repository.ts"() {
6283
6443
  "use strict";
6284
6444
  init_permissions();
6285
- PermissionsRepository = class extends BaseRepository5 {
6445
+ PermissionsRepository = class extends BaseRepository6 {
6286
6446
  /**
6287
6447
  * ID로 권한 조회
6288
6448
  */
6289
- async findById(id13) {
6290
- const result = await this.readDb.select().from(permissions).where(eq5(permissions.id, id13)).limit(1);
6449
+ async findById(id14) {
6450
+ const result = await this.readDb.select().from(permissions).where(eq6(permissions.id, id14)).limit(1);
6291
6451
  return result[0] ?? null;
6292
6452
  }
6293
6453
  /**
6294
6454
  * Name으로 권한 조회
6295
6455
  */
6296
6456
  async findByName(name) {
6297
- const result = await this.readDb.select().from(permissions).where(eq5(permissions.name, name)).limit(1);
6457
+ const result = await this.readDb.select().from(permissions).where(eq6(permissions.name, name)).limit(1);
6298
6458
  return result[0] ?? null;
6299
6459
  }
6300
6460
  /**
@@ -6314,13 +6474,13 @@ var init_permissions_repository = __esm({
6314
6474
  * 활성 권한만 조회
6315
6475
  */
6316
6476
  async findActive() {
6317
- return this.readDb.select().from(permissions).where(eq5(permissions.isActive, true)).orderBy(asc2(permissions.name));
6477
+ return this.readDb.select().from(permissions).where(eq6(permissions.isActive, true)).orderBy(asc2(permissions.name));
6318
6478
  }
6319
6479
  /**
6320
6480
  * 카테고리별 권한 조회
6321
6481
  */
6322
6482
  async findByCategory(category) {
6323
- return this.readDb.select().from(permissions).where(eq5(permissions.category, category)).orderBy(asc2(permissions.name));
6483
+ return this.readDb.select().from(permissions).where(eq6(permissions.category, category)).orderBy(asc2(permissions.name));
6324
6484
  }
6325
6485
  /**
6326
6486
  * 권한 생성
@@ -6338,15 +6498,15 @@ var init_permissions_repository = __esm({
6338
6498
  /**
6339
6499
  * 권한 업데이트
6340
6500
  */
6341
- async updateById(id13, data) {
6342
- const result = await this.db.update(permissions).set(data).where(eq5(permissions.id, id13)).returning();
6501
+ async updateById(id14, data) {
6502
+ const result = await this.db.update(permissions).set(data).where(eq6(permissions.id, id14)).returning();
6343
6503
  return result[0] ?? null;
6344
6504
  }
6345
6505
  /**
6346
6506
  * 권한 삭제
6347
6507
  */
6348
- async deleteById(id13) {
6349
- const result = await this.db.delete(permissions).where(eq5(permissions.id, id13)).returning();
6508
+ async deleteById(id14) {
6509
+ const result = await this.db.delete(permissions).where(eq6(permissions.id, id14)).returning();
6350
6510
  return result[0] ?? null;
6351
6511
  }
6352
6512
  };
@@ -6355,25 +6515,25 @@ var init_permissions_repository = __esm({
6355
6515
  });
6356
6516
 
6357
6517
  // src/server/repositories/role-permissions.repository.ts
6358
- import { BaseRepository as BaseRepository6 } from "@spfn/core/db";
6359
- import { and as and4, eq as eq6 } from "drizzle-orm";
6518
+ import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
6519
+ import { and as and5, eq as eq7 } from "drizzle-orm";
6360
6520
  var RolePermissionsRepository, rolePermissionsRepository;
6361
6521
  var init_role_permissions_repository = __esm({
6362
6522
  "src/server/repositories/role-permissions.repository.ts"() {
6363
6523
  "use strict";
6364
6524
  init_role_permissions();
6365
- RolePermissionsRepository = class extends BaseRepository6 {
6525
+ RolePermissionsRepository = class extends BaseRepository7 {
6366
6526
  /**
6367
6527
  * 역할 ID로 모든 권한 조회
6368
6528
  */
6369
6529
  async findByRoleId(roleId) {
6370
- return this.readDb.select().from(rolePermissions).where(eq6(rolePermissions.roleId, roleId));
6530
+ return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.roleId, roleId));
6371
6531
  }
6372
6532
  /**
6373
6533
  * 권한 ID로 모든 역할 조회
6374
6534
  */
6375
6535
  async findByPermissionId(permissionId) {
6376
- return this.readDb.select().from(rolePermissions).where(eq6(rolePermissions.permissionId, permissionId));
6536
+ return this.readDb.select().from(rolePermissions).where(eq7(rolePermissions.permissionId, permissionId));
6377
6537
  }
6378
6538
  /**
6379
6539
  * 역할-권한 매핑 생성
@@ -6393,9 +6553,9 @@ var init_role_permissions_repository = __esm({
6393
6553
  */
6394
6554
  async deleteByRoleIdAndPermissionId(roleId, permissionId) {
6395
6555
  const result = await this.db.delete(rolePermissions).where(
6396
- and4(
6397
- eq6(rolePermissions.roleId, roleId),
6398
- eq6(rolePermissions.permissionId, permissionId)
6556
+ and5(
6557
+ eq7(rolePermissions.roleId, roleId),
6558
+ eq7(rolePermissions.permissionId, permissionId)
6399
6559
  )
6400
6560
  ).returning();
6401
6561
  return result[0] ?? null;
@@ -6404,7 +6564,7 @@ var init_role_permissions_repository = __esm({
6404
6564
  * 역할의 모든 권한 매핑 삭제
6405
6565
  */
6406
6566
  async deleteByRoleId(roleId) {
6407
- const result = await this.db.delete(rolePermissions).where(eq6(rolePermissions.roleId, roleId)).returning();
6567
+ const result = await this.db.delete(rolePermissions).where(eq7(rolePermissions.roleId, roleId)).returning();
6408
6568
  return result.length;
6409
6569
  }
6410
6570
  /**
@@ -6425,19 +6585,19 @@ var init_role_permissions_repository = __esm({
6425
6585
  });
6426
6586
 
6427
6587
  // src/server/repositories/user-permissions.repository.ts
6428
- import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
6429
- import { eq as eq7, and as and5, or as or2, isNull as isNull3, isNotNull, lt as lt3, gt as gt2 } from "drizzle-orm";
6588
+ import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
6589
+ import { eq as eq8, and as and6, or as or2, isNull as isNull4, isNotNull, lt as lt3, gt as gt2 } from "drizzle-orm";
6430
6590
  var UserPermissionsRepository, userPermissionsRepository;
6431
6591
  var init_user_permissions_repository = __esm({
6432
6592
  "src/server/repositories/user-permissions.repository.ts"() {
6433
6593
  "use strict";
6434
6594
  init_user_permissions();
6435
- UserPermissionsRepository = class extends BaseRepository7 {
6595
+ UserPermissionsRepository = class extends BaseRepository8 {
6436
6596
  /**
6437
6597
  * 사용자 ID로 모든 권한 오버라이드 조회
6438
6598
  */
6439
6599
  async findByUserId(userId) {
6440
- return this.readDb.select().from(userPermissions).where(eq7(userPermissions.userId, userId));
6600
+ return this.readDb.select().from(userPermissions).where(eq8(userPermissions.userId, userId));
6441
6601
  }
6442
6602
  /**
6443
6603
  * 사용자 ID로 유효한 권한 오버라이드만 조회
@@ -6446,10 +6606,10 @@ var init_user_permissions_repository = __esm({
6446
6606
  async findValidByUserId(userId) {
6447
6607
  const now = /* @__PURE__ */ new Date();
6448
6608
  return this.readDb.select().from(userPermissions).where(
6449
- and5(
6450
- eq7(userPermissions.userId, userId),
6609
+ and6(
6610
+ eq8(userPermissions.userId, userId),
6451
6611
  or2(
6452
- isNull3(userPermissions.expiresAt),
6612
+ isNull4(userPermissions.expiresAt),
6453
6613
  gt2(userPermissions.expiresAt, now)
6454
6614
  )
6455
6615
  )
@@ -6460,9 +6620,9 @@ var init_user_permissions_repository = __esm({
6460
6620
  */
6461
6621
  async findByUserIdAndPermissionId(userId, permissionId) {
6462
6622
  const result = await this.readDb.select().from(userPermissions).where(
6463
- and5(
6464
- eq7(userPermissions.userId, userId),
6465
- eq7(userPermissions.permissionId, permissionId)
6623
+ and6(
6624
+ eq8(userPermissions.userId, userId),
6625
+ eq8(userPermissions.permissionId, permissionId)
6466
6626
  )
6467
6627
  ).limit(1);
6468
6628
  return result[0] ?? null;
@@ -6476,8 +6636,8 @@ var init_user_permissions_repository = __esm({
6476
6636
  /**
6477
6637
  * 사용자 권한 오버라이드 업데이트
6478
6638
  */
6479
- async updateById(id13, data) {
6480
- const result = await this.db.update(userPermissions).set(data).where(eq7(userPermissions.id, id13)).returning();
6639
+ async updateById(id14, data) {
6640
+ const result = await this.db.update(userPermissions).set(data).where(eq8(userPermissions.id, id14)).returning();
6481
6641
  return result[0] ?? null;
6482
6642
  }
6483
6643
  /**
@@ -6485,9 +6645,9 @@ var init_user_permissions_repository = __esm({
6485
6645
  */
6486
6646
  async deleteByUserIdAndPermissionId(userId, permissionId) {
6487
6647
  const result = await this.db.delete(userPermissions).where(
6488
- and5(
6489
- eq7(userPermissions.userId, userId),
6490
- eq7(userPermissions.permissionId, permissionId)
6648
+ and6(
6649
+ eq8(userPermissions.userId, userId),
6650
+ eq8(userPermissions.permissionId, permissionId)
6491
6651
  )
6492
6652
  ).returning();
6493
6653
  return result[0] ?? null;
@@ -6496,7 +6656,7 @@ var init_user_permissions_repository = __esm({
6496
6656
  * 사용자의 모든 권한 오버라이드 삭제
6497
6657
  */
6498
6658
  async deleteByUserId(userId) {
6499
- const result = await this.db.delete(userPermissions).where(eq7(userPermissions.userId, userId)).returning();
6659
+ const result = await this.db.delete(userPermissions).where(eq8(userPermissions.userId, userId)).returning();
6500
6660
  return result.length;
6501
6661
  }
6502
6662
  /**
@@ -6505,7 +6665,7 @@ var init_user_permissions_repository = __esm({
6505
6665
  async deleteExpired() {
6506
6666
  const now = /* @__PURE__ */ new Date();
6507
6667
  const result = await this.db.delete(userPermissions).where(
6508
- and5(
6668
+ and6(
6509
6669
  isNotNull(userPermissions.expiresAt),
6510
6670
  lt3(userPermissions.expiresAt, now)
6511
6671
  )
@@ -6518,33 +6678,33 @@ var init_user_permissions_repository = __esm({
6518
6678
  });
6519
6679
 
6520
6680
  // src/server/repositories/user-profiles.repository.ts
6521
- import { BaseRepository as BaseRepository8 } from "@spfn/core/db";
6522
- import { eq as eq8 } from "drizzle-orm";
6681
+ import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
6682
+ import { eq as eq9 } from "drizzle-orm";
6523
6683
  var UserProfilesRepository, userProfilesRepository;
6524
6684
  var init_user_profiles_repository = __esm({
6525
6685
  "src/server/repositories/user-profiles.repository.ts"() {
6526
6686
  "use strict";
6527
6687
  init_user_profiles();
6528
- UserProfilesRepository = class extends BaseRepository8 {
6688
+ UserProfilesRepository = class extends BaseRepository9 {
6529
6689
  /**
6530
6690
  * ID로 프로필 조회
6531
6691
  */
6532
- async findById(id13) {
6533
- const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id13)).limit(1);
6692
+ async findById(id14) {
6693
+ const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.id, id14)).limit(1);
6534
6694
  return result[0] ?? null;
6535
6695
  }
6536
6696
  /**
6537
6697
  * User ID로 locale만 조회 (경량)
6538
6698
  */
6539
6699
  async findLocaleByUserId(userId) {
6540
- const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
6700
+ const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
6541
6701
  return result[0]?.locale || "en";
6542
6702
  }
6543
6703
  /**
6544
6704
  * User ID로 프로필 조회
6545
6705
  */
6546
6706
  async findByUserId(userId) {
6547
- const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1);
6707
+ const result = await this.readDb.select().from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1);
6548
6708
  return result[0] ?? null;
6549
6709
  }
6550
6710
  /**
@@ -6556,29 +6716,29 @@ var init_user_profiles_repository = __esm({
6556
6716
  /**
6557
6717
  * 프로필 업데이트 (by ID)
6558
6718
  */
6559
- async updateById(id13, data) {
6560
- const result = await this.db.update(userProfiles).set(data).where(eq8(userProfiles.id, id13)).returning();
6719
+ async updateById(id14, data) {
6720
+ const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.id, id14)).returning();
6561
6721
  return result[0] ?? null;
6562
6722
  }
6563
6723
  /**
6564
6724
  * 프로필 업데이트 (by User ID)
6565
6725
  */
6566
6726
  async updateByUserId(userId, data) {
6567
- const result = await this.db.update(userProfiles).set(data).where(eq8(userProfiles.userId, userId)).returning();
6727
+ const result = await this.db.update(userProfiles).set(data).where(eq9(userProfiles.userId, userId)).returning();
6568
6728
  return result[0] ?? null;
6569
6729
  }
6570
6730
  /**
6571
6731
  * 프로필 삭제 (by ID)
6572
6732
  */
6573
- async deleteById(id13) {
6574
- const result = await this.db.delete(userProfiles).where(eq8(userProfiles.id, id13)).returning();
6733
+ async deleteById(id14) {
6734
+ const result = await this.db.delete(userProfiles).where(eq9(userProfiles.id, id14)).returning();
6575
6735
  return result[0] ?? null;
6576
6736
  }
6577
6737
  /**
6578
6738
  * 프로필 삭제 (by User ID)
6579
6739
  */
6580
6740
  async deleteByUserId(userId) {
6581
- const result = await this.db.delete(userProfiles).where(eq8(userProfiles.userId, userId)).returning();
6741
+ const result = await this.db.delete(userProfiles).where(eq9(userProfiles.userId, userId)).returning();
6582
6742
  return result[0] ?? null;
6583
6743
  }
6584
6744
  /**
@@ -6620,7 +6780,7 @@ var init_user_profiles_repository = __esm({
6620
6780
  metadata: userProfiles.metadata,
6621
6781
  createdAt: userProfiles.createdAt,
6622
6782
  updatedAt: userProfiles.updatedAt
6623
- }).from(userProfiles).where(eq8(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
6783
+ }).from(userProfiles).where(eq9(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
6624
6784
  if (!profile) {
6625
6785
  return null;
6626
6786
  }
@@ -6648,8 +6808,8 @@ var init_user_profiles_repository = __esm({
6648
6808
  });
6649
6809
 
6650
6810
  // src/server/repositories/invitations.repository.ts
6651
- import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as sql6 } from "drizzle-orm";
6652
- import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
6811
+ import { eq as eq10, and as and7, lt as lt4, desc as desc2, sql as sql6 } from "drizzle-orm";
6812
+ import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
6653
6813
  var InvitationsRepository, invitationsRepository;
6654
6814
  var init_invitations_repository = __esm({
6655
6815
  "src/server/repositories/invitations.repository.ts"() {
@@ -6658,19 +6818,19 @@ var init_invitations_repository = __esm({
6658
6818
  init_roles();
6659
6819
  init_user_invitations();
6660
6820
  init_email();
6661
- InvitationsRepository = class extends BaseRepository9 {
6821
+ InvitationsRepository = class extends BaseRepository10 {
6662
6822
  /**
6663
6823
  * ID로 초대 조회
6664
6824
  */
6665
- async findById(id13) {
6666
- const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.id, id13)).limit(1);
6825
+ async findById(id14) {
6826
+ const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.id, id14)).limit(1);
6667
6827
  return result[0] ?? null;
6668
6828
  }
6669
6829
  /**
6670
6830
  * Token으로 초대 조회
6671
6831
  */
6672
6832
  async findByToken(token) {
6673
- const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.token, token)).limit(1);
6833
+ const result = await this.readDb.select().from(userInvitations).where(eq10(userInvitations.token, token)).limit(1);
6674
6834
  return result[0] ?? null;
6675
6835
  }
6676
6836
  /**
@@ -6678,9 +6838,9 @@ var init_invitations_repository = __esm({
6678
6838
  */
6679
6839
  async findPendingByEmail(email) {
6680
6840
  const result = await this.readDb.select().from(userInvitations).where(
6681
- and6(
6682
- eq9(userInvitations.email, normalizeEmail(email)),
6683
- eq9(userInvitations.status, "pending")
6841
+ and7(
6842
+ eq10(userInvitations.email, normalizeEmail(email)),
6843
+ eq10(userInvitations.status, "pending")
6684
6844
  )
6685
6845
  ).limit(1);
6686
6846
  return result[0] ?? null;
@@ -6689,13 +6849,13 @@ var init_invitations_repository = __esm({
6689
6849
  * 초대자 ID로 모든 초대 조회
6690
6850
  */
6691
6851
  async findByInvitedBy(invitedBy) {
6692
- return this.readDb.select().from(userInvitations).where(eq9(userInvitations.invitedBy, invitedBy));
6852
+ return this.readDb.select().from(userInvitations).where(eq10(userInvitations.invitedBy, invitedBy));
6693
6853
  }
6694
6854
  /**
6695
6855
  * 상태별 초대 조회
6696
6856
  */
6697
6857
  async findByStatus(status) {
6698
- return this.readDb.select().from(userInvitations).where(eq9(userInvitations.status, status));
6858
+ return this.readDb.select().from(userInvitations).where(eq10(userInvitations.status, status));
6699
6859
  }
6700
6860
  /**
6701
6861
  * 초대 생성
@@ -6706,7 +6866,7 @@ var init_invitations_repository = __esm({
6706
6866
  /**
6707
6867
  * 초대 상태 업데이트
6708
6868
  */
6709
- async updateStatus(id13, status, timestamp2) {
6869
+ async updateStatus(id14, status, timestamp2) {
6710
6870
  const updates = {
6711
6871
  status
6712
6872
  };
@@ -6717,14 +6877,14 @@ var init_invitations_repository = __esm({
6717
6877
  updates.cancelledAt = timestamp2;
6718
6878
  }
6719
6879
  }
6720
- const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id13)).returning();
6880
+ const result = await this.db.update(userInvitations).set(updates).where(eq10(userInvitations.id, id14)).returning();
6721
6881
  return result[0] ?? null;
6722
6882
  }
6723
6883
  /**
6724
6884
  * 초대 삭제
6725
6885
  */
6726
- async deleteById(id13) {
6727
- const result = await this.db.delete(userInvitations).where(eq9(userInvitations.id, id13)).returning();
6886
+ async deleteById(id14) {
6887
+ const result = await this.db.delete(userInvitations).where(eq10(userInvitations.id, id14)).returning();
6728
6888
  return result[0] ?? null;
6729
6889
  }
6730
6890
  /**
@@ -6733,8 +6893,8 @@ var init_invitations_repository = __esm({
6733
6893
  async updateExpiredInvitations() {
6734
6894
  const now = /* @__PURE__ */ new Date();
6735
6895
  const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
6736
- and6(
6737
- eq9(userInvitations.status, "pending"),
6896
+ and7(
6897
+ eq10(userInvitations.status, "pending"),
6738
6898
  lt4(userInvitations.expiresAt, now)
6739
6899
  )
6740
6900
  ).returning();
@@ -6766,7 +6926,7 @@ var init_invitations_repository = __esm({
6766
6926
  id: users.id,
6767
6927
  email: users.email
6768
6928
  }
6769
- }).from(userInvitations).innerJoin(roles, eq9(userInvitations.roleId, roles.id)).innerJoin(users, eq9(userInvitations.invitedBy, users.id)).where(eq9(userInvitations.token, token)).limit(1);
6929
+ }).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(eq10(userInvitations.token, token)).limit(1);
6770
6930
  return result[0] ?? null;
6771
6931
  }
6772
6932
  /**
@@ -6777,12 +6937,12 @@ var init_invitations_repository = __esm({
6777
6937
  const offset = (page - 1) * limit;
6778
6938
  const conditions = [];
6779
6939
  if (status) {
6780
- conditions.push(eq9(userInvitations.status, status));
6940
+ conditions.push(eq10(userInvitations.status, status));
6781
6941
  }
6782
6942
  if (invitedBy) {
6783
- conditions.push(eq9(userInvitations.invitedBy, invitedBy));
6943
+ conditions.push(eq10(userInvitations.invitedBy, invitedBy));
6784
6944
  }
6785
- const whereClause = conditions.length > 0 ? and6(...conditions) : void 0;
6945
+ const whereClause = conditions.length > 0 ? and7(...conditions) : void 0;
6786
6946
  const countResult = await this.readDb.select({ count: sql6`count(*)` }).from(userInvitations).where(whereClause);
6787
6947
  const total = Number(countResult[0]?.count || 0);
6788
6948
  const results = await this.readDb.select({
@@ -6807,7 +6967,7 @@ var init_invitations_repository = __esm({
6807
6967
  id: users.id,
6808
6968
  email: users.email
6809
6969
  }
6810
- }).from(userInvitations).innerJoin(roles, eq9(userInvitations.roleId, roles.id)).innerJoin(users, eq9(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
6970
+ }).from(userInvitations).innerJoin(roles, eq10(userInvitations.roleId, roles.id)).innerJoin(users, eq10(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
6811
6971
  return {
6812
6972
  invitations: results,
6813
6973
  total,
@@ -6819,31 +6979,31 @@ var init_invitations_repository = __esm({
6819
6979
  /**
6820
6980
  * 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
6821
6981
  */
6822
- async updateById(id13, data) {
6982
+ async updateById(id14, data) {
6823
6983
  const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
6824
- const result = await this.db.update(userInvitations).set(patch).where(eq9(userInvitations.id, id13)).returning();
6984
+ const result = await this.db.update(userInvitations).set(patch).where(eq10(userInvitations.id, id14)).returning();
6825
6985
  return result[0] ?? null;
6826
6986
  }
6827
6987
  /**
6828
6988
  * 초대 재전송 (status와 expiresAt 동시 업데이트)
6829
6989
  */
6830
- async resend(id13, newExpiresAt) {
6990
+ async resend(id14, newExpiresAt) {
6831
6991
  const result = await this.db.update(userInvitations).set({
6832
6992
  status: "pending",
6833
6993
  expiresAt: newExpiresAt
6834
- }).where(eq9(userInvitations.id, id13)).returning();
6994
+ }).where(eq10(userInvitations.id, id14)).returning();
6835
6995
  return result[0] ?? null;
6836
6996
  }
6837
6997
  /**
6838
6998
  * 초대 취소 (status, metadata 동시 업데이트)
6839
6999
  */
6840
- async cancel(id13, cancelledBy, reason, currentMetadata) {
7000
+ async cancel(id14, cancelledBy, reason, currentMetadata) {
6841
7001
  const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
6842
7002
  const result = await this.db.update(userInvitations).set({
6843
7003
  status: "cancelled",
6844
7004
  cancelledAt: /* @__PURE__ */ new Date(),
6845
7005
  metadata: newMetadata
6846
- }).where(eq9(userInvitations.id, id13)).returning();
7006
+ }).where(eq10(userInvitations.id, id14)).returning();
6847
7007
  return result[0] ?? null;
6848
7008
  }
6849
7009
  };
@@ -7055,6 +7215,33 @@ var init_schema5 = __esm({
7055
7215
  })
7056
7216
  },
7057
7217
  // ============================================================================
7218
+ // Verified-email signup
7219
+ // ============================================================================
7220
+ SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {
7221
+ ...envNumber({
7222
+ description: "How long an emailed signup confirmation link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.",
7223
+ default: 30,
7224
+ required: false,
7225
+ examples: [15, 30, 60]
7226
+ })
7227
+ },
7228
+ SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {
7229
+ ...envNumber({
7230
+ description: "How long the password-setup session opened by a confirmation link stays valid. Covers one sitting at the password form, not an abandoned tab.",
7231
+ default: 15,
7232
+ required: false,
7233
+ examples: [10, 15, 30]
7234
+ })
7235
+ },
7236
+ SPFN_AUTH_SIGNUP_CONFIRM_PATH: {
7237
+ ...envString({
7238
+ description: "App page the emailed confirmation link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/signup/email/confirm; it is a page in your app, not an API route.",
7239
+ default: "/signup/confirm",
7240
+ required: false,
7241
+ examples: ["/signup/confirm", "/auth/confirm", "/join/verify"]
7242
+ })
7243
+ },
7244
+ // ============================================================================
7058
7245
  // API Configuration
7059
7246
  // ============================================================================
7060
7247
  SPFN_API_URL: {
@@ -7463,15 +7650,15 @@ var init_token_cipher = __esm({
7463
7650
  });
7464
7651
 
7465
7652
  // src/server/repositories/social-accounts.repository.ts
7466
- import { eq as eq10, and as and7 } from "drizzle-orm";
7467
- import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
7653
+ import { eq as eq11, and as and8 } from "drizzle-orm";
7654
+ import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
7468
7655
  var SocialAccountsRepository, socialAccountsRepository;
7469
7656
  var init_social_accounts_repository = __esm({
7470
7657
  "src/server/repositories/social-accounts.repository.ts"() {
7471
7658
  "use strict";
7472
7659
  init_entities();
7473
7660
  init_token_cipher();
7474
- SocialAccountsRepository = class extends BaseRepository10 {
7661
+ SocialAccountsRepository = class extends BaseRepository11 {
7475
7662
  /**
7476
7663
  * 저장 row 의 토큰을 평문으로 복호화해 반환한다.
7477
7664
  *
@@ -7499,10 +7686,10 @@ var init_social_accounts_repository = __esm({
7499
7686
  if (refresh?.needsRotation) {
7500
7687
  heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
7501
7688
  }
7502
- await this.db.update(userSocialAccounts).set(heal).where(and7(
7503
- eq10(userSocialAccounts.id, account.id),
7504
- access?.needsRotation && account.accessToken !== null ? eq10(userSocialAccounts.accessToken, account.accessToken) : void 0,
7505
- refresh?.needsRotation && account.refreshToken !== null ? eq10(userSocialAccounts.refreshToken, account.refreshToken) : void 0
7689
+ await this.db.update(userSocialAccounts).set(heal).where(and8(
7690
+ eq11(userSocialAccounts.id, account.id),
7691
+ access?.needsRotation && account.accessToken !== null ? eq11(userSocialAccounts.accessToken, account.accessToken) : void 0,
7692
+ refresh?.needsRotation && account.refreshToken !== null ? eq11(userSocialAccounts.refreshToken, account.refreshToken) : void 0
7506
7693
  ));
7507
7694
  } catch {
7508
7695
  }
@@ -7519,9 +7706,9 @@ var init_social_accounts_repository = __esm({
7519
7706
  */
7520
7707
  async findByProviderAndProviderId(provider, providerUserId) {
7521
7708
  const result = await this.readDb.select().from(userSocialAccounts).where(
7522
- and7(
7523
- eq10(userSocialAccounts.provider, provider),
7524
- eq10(userSocialAccounts.providerUserId, providerUserId)
7709
+ and8(
7710
+ eq11(userSocialAccounts.provider, provider),
7711
+ eq11(userSocialAccounts.providerUserId, providerUserId)
7525
7712
  )
7526
7713
  ).limit(1);
7527
7714
  return this.decryptAccount(result[0] ?? null);
@@ -7531,7 +7718,7 @@ var init_social_accounts_repository = __esm({
7531
7718
  * Read replica 사용
7532
7719
  */
7533
7720
  async findByUserId(userId) {
7534
- const result = await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
7721
+ const result = await this.readDb.select().from(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId));
7535
7722
  return Promise.all(result.map((account) => this.decryptAccount(account)));
7536
7723
  }
7537
7724
  /**
@@ -7540,9 +7727,9 @@ var init_social_accounts_repository = __esm({
7540
7727
  */
7541
7728
  async findByUserIdAndProvider(userId, provider) {
7542
7729
  const result = await this.readDb.select().from(userSocialAccounts).where(
7543
- and7(
7544
- eq10(userSocialAccounts.userId, userId),
7545
- eq10(userSocialAccounts.provider, provider)
7730
+ and8(
7731
+ eq11(userSocialAccounts.userId, userId),
7732
+ eq11(userSocialAccounts.provider, provider)
7546
7733
  )
7547
7734
  ).limit(1);
7548
7735
  return this.decryptAccount(result[0] ?? null);
@@ -7568,11 +7755,11 @@ var init_social_accounts_repository = __esm({
7568
7755
  * 토큰 정보 업데이트
7569
7756
  * Write primary 사용
7570
7757
  */
7571
- async updateTokens(id13, data) {
7758
+ async updateTokens(id14, data) {
7572
7759
  const accounts = await this.db.select({
7573
7760
  provider: userSocialAccounts.provider,
7574
7761
  providerUserId: userSocialAccounts.providerUserId
7575
- }).from(userSocialAccounts).where(eq10(userSocialAccounts.id, id13)).limit(1);
7762
+ }).from(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).limit(1);
7576
7763
  const account = accounts[0];
7577
7764
  if (!account) {
7578
7765
  return null;
@@ -7586,15 +7773,15 @@ var init_social_accounts_repository = __esm({
7586
7773
  ...data,
7587
7774
  accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
7588
7775
  refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
7589
- }).where(eq10(userSocialAccounts.id, id13)).returning();
7776
+ }).where(eq11(userSocialAccounts.id, id14)).returning();
7590
7777
  return this.decryptAccount(result[0] ?? null);
7591
7778
  }
7592
7779
  /**
7593
7780
  * 소셜 계정 삭제
7594
7781
  * Write primary 사용
7595
7782
  */
7596
- async deleteById(id13) {
7597
- const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id13)).returning();
7783
+ async deleteById(id14) {
7784
+ const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.id, id14)).returning();
7598
7785
  return result[0] ?? null;
7599
7786
  }
7600
7787
  /**
@@ -7603,9 +7790,9 @@ var init_social_accounts_repository = __esm({
7603
7790
  */
7604
7791
  async deleteByUserIdAndProvider(userId, provider) {
7605
7792
  const result = await this.db.delete(userSocialAccounts).where(
7606
- and7(
7607
- eq10(userSocialAccounts.userId, userId),
7608
- eq10(userSocialAccounts.provider, provider)
7793
+ and8(
7794
+ eq11(userSocialAccounts.userId, userId),
7795
+ eq11(userSocialAccounts.provider, provider)
7609
7796
  )
7610
7797
  ).returning();
7611
7798
  return result[0] ?? null;
@@ -7618,7 +7805,7 @@ var init_social_accounts_repository = __esm({
7618
7805
  * Write primary 사용
7619
7806
  */
7620
7807
  async deleteAllByUserId(userId) {
7621
- const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId)).returning();
7808
+ const result = await this.db.delete(userSocialAccounts).where(eq11(userSocialAccounts.userId, userId)).returning();
7622
7809
  return result.length;
7623
7810
  }
7624
7811
  };
@@ -7627,19 +7814,19 @@ var init_social_accounts_repository = __esm({
7627
7814
  });
7628
7815
 
7629
7816
  // src/server/repositories/auth-metadata.repository.ts
7630
- import { BaseRepository as BaseRepository11 } from "@spfn/core/db";
7631
- import { eq as eq11 } from "drizzle-orm";
7817
+ import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
7818
+ import { eq as eq12 } from "drizzle-orm";
7632
7819
  var AuthMetadataRepository, authMetadataRepository;
7633
7820
  var init_auth_metadata_repository = __esm({
7634
7821
  "src/server/repositories/auth-metadata.repository.ts"() {
7635
7822
  "use strict";
7636
7823
  init_auth_metadata();
7637
- AuthMetadataRepository = class extends BaseRepository11 {
7824
+ AuthMetadataRepository = class extends BaseRepository12 {
7638
7825
  /**
7639
7826
  * 키로 값 조회
7640
7827
  */
7641
7828
  async get(key) {
7642
- const result = await this.readDb.select().from(authMetadata).where(eq11(authMetadata.key, key)).limit(1);
7829
+ const result = await this.readDb.select().from(authMetadata).where(eq12(authMetadata.key, key)).limit(1);
7643
7830
  return result[0]?.value ?? null;
7644
7831
  }
7645
7832
  /**
@@ -7662,20 +7849,20 @@ var init_auth_metadata_repository = __esm({
7662
7849
  });
7663
7850
 
7664
7851
  // src/server/repositories/account-deletion-requests.repository.ts
7665
- import { eq as eq12, and as and8, lte } from "drizzle-orm";
7666
- import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
7852
+ import { eq as eq13, and as and9, lte } from "drizzle-orm";
7853
+ import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
7667
7854
  var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
7668
7855
  var init_account_deletion_requests_repository = __esm({
7669
7856
  "src/server/repositories/account-deletion-requests.repository.ts"() {
7670
7857
  "use strict";
7671
7858
  init_account_deletion_requests();
7672
- AccountDeletionRequestsRepository = class extends BaseRepository12 {
7859
+ AccountDeletionRequestsRepository = class extends BaseRepository13 {
7673
7860
  /**
7674
7861
  * ID로 요청 조회
7675
7862
  * Read replica 사용
7676
7863
  */
7677
- async findById(id13) {
7678
- const result = await this.readDb.select().from(accountDeletionRequests).where(eq12(accountDeletionRequests.id, id13)).limit(1);
7864
+ async findById(id14) {
7865
+ const result = await this.readDb.select().from(accountDeletionRequests).where(eq13(accountDeletionRequests.id, id14)).limit(1);
7679
7866
  return result[0] ?? null;
7680
7867
  }
7681
7868
  /**
@@ -7684,9 +7871,9 @@ var init_account_deletion_requests_repository = __esm({
7684
7871
  */
7685
7872
  async findPendingByUserId(userId) {
7686
7873
  const result = await this.readDb.select().from(accountDeletionRequests).where(
7687
- and8(
7688
- eq12(accountDeletionRequests.userId, userId),
7689
- eq12(accountDeletionRequests.status, "pending")
7874
+ and9(
7875
+ eq13(accountDeletionRequests.userId, userId),
7876
+ eq13(accountDeletionRequests.status, "pending")
7690
7877
  )
7691
7878
  ).limit(1);
7692
7879
  return result[0] ?? null;
@@ -7700,9 +7887,9 @@ var init_account_deletion_requests_repository = __esm({
7700
7887
  */
7701
7888
  async findPendingByUserIdOnPrimary(userId) {
7702
7889
  const result = await this.db.select().from(accountDeletionRequests).where(
7703
- and8(
7704
- eq12(accountDeletionRequests.userId, userId),
7705
- eq12(accountDeletionRequests.status, "pending")
7890
+ and9(
7891
+ eq13(accountDeletionRequests.userId, userId),
7892
+ eq13(accountDeletionRequests.status, "pending")
7706
7893
  )
7707
7894
  ).limit(1);
7708
7895
  return result[0] ?? null;
@@ -7713,8 +7900,8 @@ var init_account_deletion_requests_repository = __esm({
7713
7900
  */
7714
7901
  async findDueForPurge(now) {
7715
7902
  return this.readDb.select().from(accountDeletionRequests).where(
7716
- and8(
7717
- eq12(accountDeletionRequests.status, "pending"),
7903
+ and9(
7904
+ eq13(accountDeletionRequests.status, "pending"),
7718
7905
  lte(accountDeletionRequests.purgeScheduledAt, now)
7719
7906
  )
7720
7907
  );
@@ -7734,14 +7921,14 @@ var init_account_deletion_requests_repository = __esm({
7734
7921
  * cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
7735
7922
  * Write primary 사용
7736
7923
  */
7737
- async markCancelled(id13) {
7924
+ async markCancelled(id14) {
7738
7925
  const result = await this.db.update(accountDeletionRequests).set({
7739
7926
  status: "cancelled",
7740
7927
  cancelledAt: /* @__PURE__ */ new Date()
7741
7928
  }).where(
7742
- and8(
7743
- eq12(accountDeletionRequests.id, id13),
7744
- eq12(accountDeletionRequests.status, "pending")
7929
+ and9(
7930
+ eq13(accountDeletionRequests.id, id14),
7931
+ eq13(accountDeletionRequests.status, "pending")
7745
7932
  )
7746
7933
  ).returning();
7747
7934
  return result[0] ?? null;
@@ -7756,15 +7943,15 @@ var init_account_deletion_requests_repository = __esm({
7756
7943
  * destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
7757
7944
  * Write primary 사용
7758
7945
  */
7759
- async markCompleted(id13, purgeStrategy) {
7946
+ async markCompleted(id14, purgeStrategy) {
7760
7947
  const result = await this.db.update(accountDeletionRequests).set({
7761
7948
  status: "completed",
7762
7949
  completedAt: /* @__PURE__ */ new Date(),
7763
7950
  purgeStrategy
7764
7951
  }).where(
7765
- and8(
7766
- eq12(accountDeletionRequests.id, id13),
7767
- eq12(accountDeletionRequests.status, "pending")
7952
+ and9(
7953
+ eq13(accountDeletionRequests.id, id14),
7954
+ eq13(accountDeletionRequests.status, "pending")
7768
7955
  )
7769
7956
  ).returning();
7770
7957
  return result[0] ?? null;
@@ -7775,14 +7962,14 @@ var init_account_deletion_requests_repository = __esm({
7775
7962
  });
7776
7963
 
7777
7964
  // src/server/repositories/ops-tokens.repository.ts
7778
- import { and as and9, desc as desc3, eq as eq13, isNull as isNull4 } from "drizzle-orm";
7779
- import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
7965
+ import { and as and10, desc as desc3, eq as eq14, isNull as isNull5 } from "drizzle-orm";
7966
+ import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
7780
7967
  var OpsTokensRepository, opsTokensRepository;
7781
7968
  var init_ops_tokens_repository = __esm({
7782
7969
  "src/server/repositories/ops-tokens.repository.ts"() {
7783
7970
  "use strict";
7784
7971
  init_ops_tokens();
7785
- OpsTokensRepository = class extends BaseRepository13 {
7972
+ OpsTokensRepository = class extends BaseRepository14 {
7786
7973
  /**
7787
7974
  * Lookup by the secret's hash — the verification path.
7788
7975
  *
@@ -7792,7 +7979,7 @@ var init_ops_tokens_repository = __esm({
7792
7979
  * and revocation is documented as taking effect immediately.
7793
7980
  */
7794
7981
  async findByTokenHash(tokenHash) {
7795
- const result = await this.db.select().from(opsTokens).where(eq13(opsTokens.tokenHash, tokenHash)).limit(1);
7982
+ const result = await this.db.select().from(opsTokens).where(eq14(opsTokens.tokenHash, tokenHash)).limit(1);
7796
7983
  return result[0] ?? null;
7797
7984
  }
7798
7985
  async create(data) {
@@ -7807,13 +7994,13 @@ var init_ops_tokens_repository = __esm({
7807
7994
  * token is already revoked — the first revocation's timestamp is never
7808
7995
  * overwritten.
7809
7996
  */
7810
- async revokeById(id13) {
7811
- const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and9(eq13(opsTokens.id, id13), isNull4(opsTokens.revokedAt))).returning();
7997
+ async revokeById(id14) {
7998
+ const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and10(eq14(opsTokens.id, id14), isNull5(opsTokens.revokedAt))).returning();
7812
7999
  return result[0] ?? null;
7813
8000
  }
7814
8001
  /** Fire-and-forget from the verification path. */
7815
- async updateLastUsedById(id13) {
7816
- await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq13(opsTokens.id, id13));
8002
+ async updateLastUsedById(id14) {
8003
+ await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq14(opsTokens.id, id14));
7817
8004
  }
7818
8005
  };
7819
8006
  opsTokensRepository = new OpsTokensRepository();
@@ -7827,6 +8014,7 @@ var init_repositories = __esm({
7827
8014
  init_users_repository();
7828
8015
  init_keys_repository();
7829
8016
  init_verification_codes_repository();
8017
+ init_signup_link_tokens_repository();
7830
8018
  init_roles_repository();
7831
8019
  init_permissions_repository();
7832
8020
  init_role_permissions_repository();
@@ -7928,7 +8116,7 @@ async function removePermissionFromRole(roleId, permissionId) {
7928
8116
  }
7929
8117
  async function setRolePermissions(roleId, permissionIds) {
7930
8118
  const roleIdNum = Number(roleId);
7931
- const permissionIdNums = permissionIds.map((id13) => Number(id13));
8119
+ const permissionIdNums = permissionIds.map((id14) => Number(id14));
7932
8120
  await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
7933
8121
  }
7934
8122
  async function getAllRoles(includeInactive = false) {
@@ -7948,7 +8136,7 @@ async function getRolePermissions(roleId) {
7948
8136
  }
7949
8137
  const permissionIds = mappings.map((m) => m.permissionId);
7950
8138
  const perms = await Promise.all(
7951
- permissionIds.map((id13) => permissionsRepository.findById(id13))
8139
+ permissionIds.map((id14) => permissionsRepository.findById(id14))
7952
8140
  );
7953
8141
  return perms.filter((p) => p !== null).map((p) => p.name);
7954
8142
  }
@@ -8147,6 +8335,7 @@ function getKeyId(c) {
8147
8335
  // src/server/routes/auth/index.ts
8148
8336
  init_types();
8149
8337
  import { KeyNotFoundError } from "@spfn/auth/errors";
8338
+ import { ValidationError as ValidationError9 } from "@spfn/core/errors";
8150
8339
 
8151
8340
  // src/server/services/auth.service.ts
8152
8341
  init_repositories();
@@ -8184,6 +8373,10 @@ var COOKIE_NAMES = {
8184
8373
  /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
8185
8374
  get OAUTH_CSRF() {
8186
8375
  return `spfn_oauth_csrf${getCookieSuffix()}`;
8376
+ },
8377
+ /** Password-setup session for verified-email signup — temporary, single-purpose */
8378
+ get SIGNUP_SETUP() {
8379
+ return `spfn_signup_setup${getCookieSuffix()}`;
8187
8380
  }
8188
8381
  };
8189
8382
  function matchOAuthCsrfCookies(cookies) {
@@ -8387,24 +8580,28 @@ async function sendAccountExistsNotice(target, targetType) {
8387
8580
  log.error("Failed to send account-exists notice", { target, error: result.error });
8388
8581
  }
8389
8582
  }
8583
+ async function noticeAccountExistsOnce(target, targetType) {
8584
+ const recentNotice = await verificationCodesRepository.findValidByTargetAndPurpose(target, "registration");
8585
+ if (recentNotice) {
8586
+ return;
8587
+ }
8588
+ const dedupeExpiresAt = new Date(Date.now() + ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES * 6e4);
8589
+ await verificationCodesRepository.invalidatePreviousCodes(target, "registration");
8590
+ await verificationCodesRepository.create({
8591
+ target,
8592
+ targetType,
8593
+ code: generateVerificationCode(),
8594
+ purpose: "registration",
8595
+ expiresAt: dedupeExpiresAt,
8596
+ attempts: 0
8597
+ });
8598
+ await sendAccountExistsNotice(target, targetType);
8599
+ }
8390
8600
  async function sendVerificationCodeService(params) {
8391
8601
  const { targetType, purpose } = params;
8392
8602
  const target = normalizeVerificationTarget(params.target, targetType);
8393
8603
  if (purpose === "registration" && await accountExistsForTarget(target, targetType)) {
8394
- const recentNotice = await verificationCodesRepository.findValidByTargetAndPurpose(target, purpose);
8395
- if (!recentNotice) {
8396
- const dedupeExpiresAt = new Date(Date.now() + ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES * 6e4);
8397
- await verificationCodesRepository.invalidatePreviousCodes(target, purpose);
8398
- await verificationCodesRepository.create({
8399
- target,
8400
- targetType,
8401
- code: generateVerificationCode(),
8402
- purpose,
8403
- expiresAt: dedupeExpiresAt,
8404
- attempts: 0
8405
- });
8406
- await sendAccountExistsNotice(target, targetType);
8407
- }
8604
+ await noticeAccountExistsOnce(target, targetType);
8408
8605
  return {
8409
8606
  success: true,
8410
8607
  expiresAt: new Date(Date.now() + VERIFICATION_CODE_EXPIRY_MINUTES * 6e4).toISOString()
@@ -8774,8 +8971,8 @@ async function verifyReauthCredential(user, params) {
8774
8971
  throw new VerificationTokenTargetMismatchError();
8775
8972
  }
8776
8973
  }
8777
- async function sendDeletionEmail(to, subject, text13) {
8778
- const result = await sendEmail2({ to, subject, text: text13 });
8974
+ async function sendDeletionEmail(to, subject, text14) {
8975
+ const result = await sendEmail2({ to, subject, text: text14 });
8779
8976
  if (!result.success) {
8780
8977
  authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
8781
8978
  }
@@ -9022,7 +9219,7 @@ async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
9022
9219
 
9023
9220
  // src/server/services/auth.service.ts
9024
9221
  async function registerService(params) {
9025
- const { email, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
9222
+ const { email, verificationToken } = params;
9026
9223
  const phone = params.phone?.trim();
9027
9224
  const tokenPayload = validateVerificationToken(verificationToken);
9028
9225
  if (!tokenPayload) {
@@ -9039,6 +9236,13 @@ async function registerService(params) {
9039
9236
  if (tokenPayload.targetType !== providedTargetType) {
9040
9237
  throw new VerificationTokenTargetMismatchError2();
9041
9238
  }
9239
+ return await createVerifiedAccount({ ...params, phone });
9240
+ }
9241
+ async function createVerifiedAccount(params) {
9242
+ const { email, phone, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
9243
+ if (!publicKey || !keyId || !fingerprint) {
9244
+ throw new ValidationError3({ message: "Device key material is required to register" });
9245
+ }
9042
9246
  const existingUser = await usersRepository.findByEmailOrPhone(email, phone);
9043
9247
  if (existingUser) {
9044
9248
  const identifierType = email ? "email" : "phone";
@@ -9170,6 +9374,158 @@ async function changePasswordService(params) {
9170
9374
  await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
9171
9375
  }
9172
9376
 
9377
+ // src/server/services/signup-link.service.ts
9378
+ import crypto5 from "crypto";
9379
+ import { env as env7 } from "@spfn/auth/config";
9380
+ import { InvalidSignupLinkError, InvalidSignupSetupSessionError } from "@spfn/auth/errors";
9381
+ import { sendEmail as sendEmail3 } from "@spfn/notification/server";
9382
+ init_repositories();
9383
+ var CREDENTIAL_BYTES = 32;
9384
+ function mintCredential() {
9385
+ const secret = crypto5.randomBytes(CREDENTIAL_BYTES).toString("base64url");
9386
+ return { secret, hash: hashCredential(secret) };
9387
+ }
9388
+ function hashCredential(secret) {
9389
+ return crypto5.createHash("sha256").update(secret).digest("base64url");
9390
+ }
9391
+ function isSafeReturnPath(returnPath) {
9392
+ if (!returnPath.startsWith("/")) {
9393
+ return false;
9394
+ }
9395
+ if (returnPath.startsWith("//") || returnPath.includes("\\")) {
9396
+ return false;
9397
+ }
9398
+ if (returnPath.includes("..")) {
9399
+ return false;
9400
+ }
9401
+ return !/^\/[^/?#]*:/.test(returnPath);
9402
+ }
9403
+ function buildConfirmUrl(token) {
9404
+ const appUrl = (env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL || "").replace(/\/$/, "");
9405
+ const path = env7.SPFN_AUTH_SIGNUP_CONFIRM_PATH || "/signup/confirm";
9406
+ return `${appUrl}${path}?token=${encodeURIComponent(token)}`;
9407
+ }
9408
+ async function sendSignupLinkEmail(email, confirmUrl, expiresInMinutes) {
9409
+ const result = await sendEmail3({
9410
+ to: email,
9411
+ template: "signup-link",
9412
+ data: { confirmUrl, expiresInMinutes }
9413
+ });
9414
+ if (!result.success) {
9415
+ authLogger.email.error("Failed to send signup link email", {
9416
+ email,
9417
+ error: result.error
9418
+ });
9419
+ }
9420
+ }
9421
+ async function requestSignupLinkService(params) {
9422
+ const email = params.email.trim();
9423
+ const returnPath = params.returnPath;
9424
+ const ttlMinutes = env7.SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES ?? 30;
9425
+ const expiresAt = new Date(Date.now() + ttlMinutes * 6e4);
9426
+ const existingUser = await usersRepository.findByEmail(email);
9427
+ if (existingUser) {
9428
+ await noticeAccountExistsOnce(email, "email");
9429
+ return { success: true, expiresAt: expiresAt.toISOString() };
9430
+ }
9431
+ await signupLinkTokensRepository.supersedeLiveForEmail(email);
9432
+ const { secret, hash: hash2 } = mintCredential();
9433
+ await signupLinkTokensRepository.create({
9434
+ email,
9435
+ tokenHash: hash2,
9436
+ returnPath: returnPath ?? null,
9437
+ expiresAt
9438
+ });
9439
+ await sendSignupLinkEmail(email, buildConfirmUrl(secret), ttlMinutes);
9440
+ return { success: true, expiresAt: expiresAt.toISOString() };
9441
+ }
9442
+ function linkRefusalReason(row) {
9443
+ if (!row) {
9444
+ return "unknown token";
9445
+ }
9446
+ if (row.completedAt) {
9447
+ return "signup already completed";
9448
+ }
9449
+ if (row.supersededAt) {
9450
+ return "superseded by a newer request";
9451
+ }
9452
+ if (row.consumedAt) {
9453
+ return "link already used";
9454
+ }
9455
+ if (/* @__PURE__ */ new Date() > new Date(row.expiresAt)) {
9456
+ return "link expired";
9457
+ }
9458
+ return null;
9459
+ }
9460
+ async function confirmSignupLinkService(params) {
9461
+ const row = await signupLinkTokensRepository.findByTokenHash(hashCredential(params.token));
9462
+ const refusal = linkRefusalReason(row);
9463
+ if (refusal || !row) {
9464
+ authLogger.service.warn("Signup link refused", { reason: refusal });
9465
+ throw new InvalidSignupLinkError();
9466
+ }
9467
+ if (await usersRepository.findByEmail(row.email)) {
9468
+ authLogger.service.warn("Signup link refused", { reason: "account created meanwhile" });
9469
+ throw new InvalidSignupLinkError();
9470
+ }
9471
+ const setupTtlMinutes = env7.SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES ?? 15;
9472
+ const setupExpiresAt = new Date(Date.now() + setupTtlMinutes * 6e4);
9473
+ const setup = mintCredential();
9474
+ const claimed = await signupLinkTokensRepository.claimLink(row.id, setup.hash, setupExpiresAt);
9475
+ if (!claimed) {
9476
+ authLogger.service.warn("Signup link refused", { reason: "lost the claim race" });
9477
+ throw new InvalidSignupLinkError();
9478
+ }
9479
+ return {
9480
+ email: claimed.email,
9481
+ returnPath: claimed.returnPath,
9482
+ setupSecret: setup.secret,
9483
+ setupExpiresAt: setupExpiresAt.toISOString()
9484
+ };
9485
+ }
9486
+ function setupRefusalReason(row) {
9487
+ if (!row) {
9488
+ return "unknown setup session";
9489
+ }
9490
+ if (row.completedAt) {
9491
+ return "setup session already used";
9492
+ }
9493
+ if (row.supersededAt) {
9494
+ return "superseded by a newer request";
9495
+ }
9496
+ if (!row.setupExpiresAt || /* @__PURE__ */ new Date() > new Date(row.setupExpiresAt)) {
9497
+ return "setup session expired";
9498
+ }
9499
+ return null;
9500
+ }
9501
+ async function completeSignupService(params) {
9502
+ if (!params.setupSecret) {
9503
+ throw new InvalidSignupSetupSessionError();
9504
+ }
9505
+ const row = await signupLinkTokensRepository.findBySetupSecretHash(hashCredential(params.setupSecret));
9506
+ const refusal = setupRefusalReason(row);
9507
+ if (refusal || !row) {
9508
+ authLogger.service.warn("Signup setup session refused", { reason: refusal });
9509
+ throw new InvalidSignupSetupSessionError();
9510
+ }
9511
+ const claimed = await signupLinkTokensRepository.claimSetupSession(row.id);
9512
+ if (!claimed) {
9513
+ authLogger.service.warn("Signup setup session refused", { reason: "lost the claim race" });
9514
+ throw new InvalidSignupSetupSessionError();
9515
+ }
9516
+ return await createVerifiedAccount({
9517
+ email: claimed.email,
9518
+ password: params.password,
9519
+ publicKey: params.publicKey,
9520
+ keyId: params.keyId,
9521
+ fingerprint: params.fingerprint,
9522
+ algorithm: params.algorithm,
9523
+ deviceName: params.deviceName,
9524
+ platform: params.platform,
9525
+ metadata: params.metadata
9526
+ });
9527
+ }
9528
+
9173
9529
  // src/server/services/rbac.service.ts
9174
9530
  init_repositories();
9175
9531
  init_rbac();
@@ -9347,7 +9703,7 @@ async function getUserPermissions(userId) {
9347
9703
  const permIds = rolePermMappings.map((rp) => rp.permissionId);
9348
9704
  if (permIds.length > 0) {
9349
9705
  const rolePerms = await Promise.all(
9350
- permIds.map((id13) => permissionsRepository.findById(id13))
9706
+ permIds.map((id14) => permissionsRepository.findById(id14))
9351
9707
  );
9352
9708
  for (const perm of rolePerms) {
9353
9709
  if (perm && perm.isActive) {
@@ -9422,10 +9778,10 @@ init_role_service();
9422
9778
 
9423
9779
  // src/server/services/invitation.service.ts
9424
9780
  init_repositories();
9425
- import crypto5 from "crypto";
9781
+ import crypto6 from "crypto";
9426
9782
  import { BadRequestError, NotFoundError as NotFoundError3, ConflictError } from "@spfn/core/errors";
9427
9783
  function generateInvitationToken() {
9428
- return crypto5.randomUUID();
9784
+ return crypto6.randomUUID();
9429
9785
  }
9430
9786
  function calculateExpiresAt(days = 7) {
9431
9787
  const expiresAt = /* @__PURE__ */ new Date();
@@ -9560,20 +9916,20 @@ async function acceptInvitation(params) {
9560
9916
  async function listInvitations(params) {
9561
9917
  return await invitationsRepository.list(params);
9562
9918
  }
9563
- async function cancelInvitation(id13, cancelledBy, reason) {
9564
- const invitation = await invitationsRepository.findById(id13);
9919
+ async function cancelInvitation(id14, cancelledBy, reason) {
9920
+ const invitation = await invitationsRepository.findById(id14);
9565
9921
  if (!invitation) {
9566
9922
  throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
9567
9923
  }
9568
9924
  if (invitation.status !== "pending") {
9569
9925
  throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
9570
9926
  }
9571
- await invitationsRepository.cancel(id13, cancelledBy, reason, invitation.metadata);
9927
+ await invitationsRepository.cancel(id14, cancelledBy, reason, invitation.metadata);
9572
9928
  console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
9573
9929
  }
9574
- async function deleteInvitation(id13) {
9575
- await invitationsRepository.deleteById(id13);
9576
- console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id13}`);
9930
+ async function deleteInvitation(id14) {
9931
+ await invitationsRepository.deleteById(id14);
9932
+ console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id14}`);
9577
9933
  }
9578
9934
  async function expireOldInvitations() {
9579
9935
  const count = await invitationsRepository.updateExpiredInvitations();
@@ -9582,8 +9938,8 @@ async function expireOldInvitations() {
9582
9938
  }
9583
9939
  return count;
9584
9940
  }
9585
- async function resendInvitation(id13, expiresInDays = 7) {
9586
- const invitation = await invitationsRepository.findById(id13);
9941
+ async function resendInvitation(id14, expiresInDays = 7) {
9942
+ const invitation = await invitationsRepository.findById(id14);
9587
9943
  if (!invitation) {
9588
9944
  throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
9589
9945
  }
@@ -9591,7 +9947,7 @@ async function resendInvitation(id13, expiresInDays = 7) {
9591
9947
  throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
9592
9948
  }
9593
9949
  const newExpiresAt = calculateExpiresAt(expiresInDays);
9594
- const updated = await invitationsRepository.resend(id13, newExpiresAt);
9950
+ const updated = await invitationsRepository.resend(id14, newExpiresAt);
9595
9951
  if (!updated) {
9596
9952
  throw new Error("Failed to update invitation");
9597
9953
  }
@@ -9745,7 +10101,7 @@ async function updateUserProfileService(userId, params) {
9745
10101
 
9746
10102
  // src/server/services/oauth.service.ts
9747
10103
  init_repositories();
9748
- import { env as env11 } from "@spfn/auth/config";
10104
+ import { env as env12 } from "@spfn/auth/config";
9749
10105
  import { ValidationError as ValidationError8 } from "@spfn/core/errors";
9750
10106
  import {
9751
10107
  AccountDisabledError as AccountDisabledError2,
@@ -9754,21 +10110,21 @@ import {
9754
10110
  } from "@spfn/auth/errors";
9755
10111
 
9756
10112
  // src/server/lib/oauth/google.ts
9757
- import { env as env7 } from "@spfn/auth/config";
10113
+ import { env as env8 } from "@spfn/auth/config";
9758
10114
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
9759
10115
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
9760
10116
  var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
9761
10117
  function isGoogleOAuthEnabled() {
9762
- return !!(env7.SPFN_AUTH_GOOGLE_CLIENT_ID && env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
10118
+ return !!(env8.SPFN_AUTH_GOOGLE_CLIENT_ID && env8.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
9763
10119
  }
9764
10120
  function getGoogleOAuthConfig() {
9765
- const clientId = env7.SPFN_AUTH_GOOGLE_CLIENT_ID;
9766
- const clientSecret = env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
10121
+ const clientId = env8.SPFN_AUTH_GOOGLE_CLIENT_ID;
10122
+ const clientSecret = env8.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
9767
10123
  if (!clientId || !clientSecret) {
9768
10124
  throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
9769
10125
  }
9770
- const baseUrl = env7.NEXT_PUBLIC_SPFN_APP_URL || env7.SPFN_APP_URL;
9771
- const redirectUri = env7.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
10126
+ const baseUrl = env8.NEXT_PUBLIC_SPFN_APP_URL || env8.SPFN_APP_URL;
10127
+ const redirectUri = env8.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
9772
10128
  return {
9773
10129
  clientId,
9774
10130
  clientSecret,
@@ -9776,7 +10132,7 @@ function getGoogleOAuthConfig() {
9776
10132
  };
9777
10133
  }
9778
10134
  function getDefaultScopes() {
9779
- const envScopes = env7.SPFN_AUTH_GOOGLE_SCOPES;
10135
+ const envScopes = env8.SPFN_AUTH_GOOGLE_SCOPES;
9780
10136
  if (envScopes) {
9781
10137
  return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
9782
10138
  }
@@ -9854,9 +10210,9 @@ async function refreshAccessToken(refreshToken) {
9854
10210
 
9855
10211
  // src/server/lib/oauth/state.ts
9856
10212
  import * as jose from "jose";
9857
- import { env as env8 } from "@spfn/auth/config";
10213
+ import { env as env9 } from "@spfn/auth/config";
9858
10214
  async function getStateKey() {
9859
- const secret = env8.SPFN_AUTH_SESSION_SECRET;
10215
+ const secret = env9.SPFN_AUTH_SESSION_SECRET;
9860
10216
  const encoder = new TextEncoder();
9861
10217
  const data = encoder.encode(`oauth-state:${secret}`);
9862
10218
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -9905,8 +10261,8 @@ var registry2 = /* @__PURE__ */ new Map();
9905
10261
  function registerOAuthProvider(provider) {
9906
10262
  registry2.set(provider.id, provider);
9907
10263
  }
9908
- function getOAuthProvider(id13) {
9909
- return registry2.get(id13);
10264
+ function getOAuthProvider(id14) {
10265
+ return registry2.get(id14);
9910
10266
  }
9911
10267
  function getRegisteredProviders() {
9912
10268
  return [...registry2.values()];
@@ -9959,14 +10315,14 @@ async function verifySignature(params) {
9959
10315
  init_token_cipher();
9960
10316
 
9961
10317
  // src/server/lib/oauth/google-provider.ts
9962
- import { env as env9 } from "@spfn/auth/config";
10318
+ import { env as env10 } from "@spfn/auth/config";
9963
10319
  import { NativeSignInUnsupportedError } from "@spfn/auth/errors";
9964
10320
  var GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
9965
10321
  var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
9966
10322
  function getGoogleNativeAudiences() {
9967
- const ids = (env9.SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
9968
- if (env9.SPFN_AUTH_GOOGLE_CLIENT_ID) {
9969
- ids.push(env9.SPFN_AUTH_GOOGLE_CLIENT_ID);
10323
+ const ids = (env10.SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
10324
+ if (env10.SPFN_AUTH_GOOGLE_CLIENT_ID) {
10325
+ ids.push(env10.SPFN_AUTH_GOOGLE_CLIENT_ID);
9970
10326
  }
9971
10327
  return ids;
9972
10328
  }
@@ -10028,13 +10384,13 @@ registerOAuthProvider(googleProvider);
10028
10384
 
10029
10385
  // src/server/lib/oauth/apple-provider.ts
10030
10386
  import { createHash as createHash2 } from "crypto";
10031
- import { env as env10 } from "@spfn/auth/config";
10387
+ import { env as env11 } from "@spfn/auth/config";
10032
10388
  import { ValidationError as ValidationError4 } from "@spfn/core/errors";
10033
10389
  import { NativeSignInUnsupportedError as NativeSignInUnsupportedError2 } from "@spfn/auth/errors";
10034
10390
  var APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys";
10035
10391
  var APPLE_ISSUER = "https://appleid.apple.com";
10036
10392
  function getAppleClientIds() {
10037
- return (env10.SPFN_AUTH_APPLE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
10393
+ return (env11.SPFN_AUTH_APPLE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
10038
10394
  }
10039
10395
  function hashNonce(rawNonce) {
10040
10396
  return createHash2("sha256").update(rawNonce).digest("hex");
@@ -10747,8 +11103,8 @@ async function oauthCallbackService(params) {
10747
11103
  algorithm: stateData.algorithm
10748
11104
  });
10749
11105
  await updateLastLoginService(userId);
10750
- const appUrl = env11.NEXT_PUBLIC_SPFN_APP_URL || env11.SPFN_APP_URL;
10751
- const callbackPath = env11.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
11106
+ const appUrl = env12.NEXT_PUBLIC_SPFN_APP_URL || env12.SPFN_APP_URL;
11107
+ const callbackPath = env12.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
10752
11108
  const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
10753
11109
  const redirectUrl = buildRedirectUrl(callbackUrl, {
10754
11110
  userId: String(userId),
@@ -10876,7 +11232,7 @@ function buildRedirectUrl(baseUrl, params) {
10876
11232
  return `${url.pathname}${url.search}`;
10877
11233
  }
10878
11234
  function buildOAuthErrorUrl(error) {
10879
- const errorUrl = env11.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
11235
+ const errorUrl = env12.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
10880
11236
  return errorUrl.replace("{error}", encodeURIComponent(error));
10881
11237
  }
10882
11238
  function isOAuthProviderEnabled(provider) {
@@ -11042,8 +11398,8 @@ async function verifyOpsTokenService(token) {
11042
11398
  scopes: record.scopes
11043
11399
  };
11044
11400
  }
11045
- async function revokeOpsTokenService(id13) {
11046
- return await opsTokensRepository.revokeById(id13);
11401
+ async function revokeOpsTokenService(id14) {
11402
+ return await opsTokensRepository.revokeById(id14);
11047
11403
  }
11048
11404
  async function listOpsTokensService() {
11049
11405
  return await opsTokensRepository.list();
@@ -11179,6 +11535,54 @@ var register = route.post("/_auth/register").input({
11179
11535
  const { body } = await c.data();
11180
11536
  return await registerService(body);
11181
11537
  });
11538
+ var requestSignupLink = route.post("/_auth/signup/email").input({
11539
+ body: Type.Object({
11540
+ email: EmailSchema,
11541
+ returnPath: Type.Optional(Type.String({
11542
+ maxLength: 512,
11543
+ description: "Relative path within the app to return to after signup. Absolute URLs are rejected."
11544
+ }))
11545
+ })
11546
+ }).use([rateLimitPolicy("auth-signup-link", { limit: 5, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 20 }) })]).skip(["auth"]).handler(async (c) => {
11547
+ const { body } = await c.data();
11548
+ if (body.returnPath !== void 0 && !isSafeReturnPath(body.returnPath)) {
11549
+ throw new ValidationError9({ message: "returnPath must be a relative path within the app" });
11550
+ }
11551
+ return await requestSignupLinkService(body);
11552
+ });
11553
+ var confirmSignupLink = route.post("/_auth/signup/email/confirm").input({
11554
+ body: Type.Object({
11555
+ token: Type.String({
11556
+ minLength: 16,
11557
+ maxLength: 256,
11558
+ description: "Token from the confirmation link"
11559
+ })
11560
+ })
11561
+ }).use([rateLimitPolicy("auth-signup-confirm", { limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
11562
+ const { body } = await c.data();
11563
+ return await confirmSignupLinkService(body);
11564
+ });
11565
+ var completeSignup = route.post("/_auth/signup/password").input({
11566
+ body: Type.Object({
11567
+ password: PasswordSchema,
11568
+ metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
11569
+ description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
11570
+ }))
11571
+ })
11572
+ }).interceptor({
11573
+ body: Type.Object({
11574
+ setupSecret: Type.String({ description: "Password-setup session secret, from the HttpOnly cookie" }),
11575
+ publicKey: Type.String({ description: "Client public key" }),
11576
+ keyId: Type.String({ description: "Key identifier" }),
11577
+ fingerprint: Type.String({ description: "Key fingerprint" }),
11578
+ algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
11579
+ deviceName: Type.Optional(DeviceNameSchema),
11580
+ platform: Type.Optional(PlatformSchema)
11581
+ })
11582
+ }).use([rateLimitPolicy("auth-signup-password", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
11583
+ const { body } = await c.data();
11584
+ return await completeSignupService(body);
11585
+ });
11182
11586
  var login = route.post("/_auth/login").input({
11183
11587
  body: Type.Object({
11184
11588
  email: Type.Optional(EmailSchema),
@@ -11351,13 +11755,13 @@ var CanonicalJsonError = class extends Error {
11351
11755
  var INT64_MIN = -(2n ** 63n);
11352
11756
  var INT64_MAX = 2n ** 63n - 1n;
11353
11757
  function parseCanonicalJson(bytes) {
11354
- let text13;
11758
+ let text14;
11355
11759
  try {
11356
- text13 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
11760
+ text14 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
11357
11761
  } catch {
11358
11762
  throw new CanonicalJsonError("INVALID_UTF8");
11359
11763
  }
11360
- const parser = new Parser(text13);
11764
+ const parser = new Parser(text14);
11361
11765
  const value = parser.parseValue();
11362
11766
  parser.skipWhitespace();
11363
11767
  if (!parser.atEnd()) {
@@ -11378,8 +11782,8 @@ function isCanonicalBytes(bytes, value) {
11378
11782
  return true;
11379
11783
  }
11380
11784
  var Parser = class {
11381
- constructor(text13) {
11382
- this.text = text13;
11785
+ constructor(text14) {
11786
+ this.text = text14;
11383
11787
  }
11384
11788
  pos = 0;
11385
11789
  atEnd() {
@@ -13292,7 +13696,7 @@ var deleteCookie = (c, name, opt) => {
13292
13696
  init_types();
13293
13697
  init_schema3();
13294
13698
  import { Transactional as Transactional3 } from "@spfn/core/db";
13295
- import { ValidationError as ValidationError9 } from "@spfn/core/errors";
13699
+ import { ValidationError as ValidationError10 } from "@spfn/core/errors";
13296
13700
  import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
13297
13701
  import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
13298
13702
  var providerParams = Type.Object({
@@ -13412,10 +13816,10 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
13412
13816
  }).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
13413
13817
  const { body } = await c.data();
13414
13818
  if (!isGoogleOAuthEnabled()) {
13415
- throw new ValidationError9({ message: "Google OAuth is not configured" });
13819
+ throw new ValidationError10({ message: "Google OAuth is not configured" });
13416
13820
  }
13417
13821
  if (!body.state) {
13418
- throw new ValidationError9({
13822
+ throw new ValidationError10({
13419
13823
  message: "OAuth state is required. Ensure the OAuth interceptor is configured."
13420
13824
  });
13421
13825
  }
@@ -13510,7 +13914,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
13510
13914
  const { params, body } = await c.data();
13511
13915
  const provider = requireEnabledProvider(params.provider);
13512
13916
  if (!body.state) {
13513
- throw new ValidationError9({
13917
+ throw new ValidationError10({
13514
13918
  message: "OAuth state is required. Ensure the OAuth interceptor is configured."
13515
13919
  });
13516
13920
  }
@@ -13811,6 +14215,10 @@ var mainAuthRouter = defineRouter6({
13811
14215
  sendVerificationCode,
13812
14216
  verifyCode,
13813
14217
  register,
14218
+ // Verified-email signup routes
14219
+ requestSignupLink,
14220
+ confirmSignupLink,
14221
+ completeSignup,
13814
14222
  login,
13815
14223
  logout,
13816
14224
  rotateKey,
@@ -13870,11 +14278,11 @@ init_types();
13870
14278
  init_schema3();
13871
14279
 
13872
14280
  // src/server/lib/crypto.ts
13873
- import crypto6 from "crypto";
14281
+ import crypto7 from "crypto";
13874
14282
  import jwt3 from "jsonwebtoken";
13875
14283
  function generateKeyPairES256() {
13876
- const keyId = crypto6.randomUUID();
13877
- const { privateKey, publicKey } = crypto6.generateKeyPairSync("ec", {
14284
+ const keyId = crypto7.randomUUID();
14285
+ const { privateKey, publicKey } = crypto7.generateKeyPairSync("ec", {
13878
14286
  namedCurve: "P-256",
13879
14287
  // ES256
13880
14288
  publicKeyEncoding: {
@@ -13888,7 +14296,7 @@ function generateKeyPairES256() {
13888
14296
  });
13889
14297
  const privateKeyB64 = privateKey.toString("base64");
13890
14298
  const publicKeyB64 = publicKey.toString("base64");
13891
- const fingerprint = crypto6.createHash("sha256").update(publicKey).digest("hex");
14299
+ const fingerprint = crypto7.createHash("sha256").update(publicKey).digest("hex");
13892
14300
  return {
13893
14301
  privateKey: privateKeyB64,
13894
14302
  publicKey: publicKeyB64,
@@ -13898,8 +14306,8 @@ function generateKeyPairES256() {
13898
14306
  };
13899
14307
  }
13900
14308
  function generateKeyPairRS256() {
13901
- const keyId = crypto6.randomUUID();
13902
- const { privateKey, publicKey } = crypto6.generateKeyPairSync("rsa", {
14309
+ const keyId = crypto7.randomUUID();
14310
+ const { privateKey, publicKey } = crypto7.generateKeyPairSync("rsa", {
13903
14311
  modulusLength: 2048,
13904
14312
  publicKeyEncoding: {
13905
14313
  type: "spki",
@@ -13912,7 +14320,7 @@ function generateKeyPairRS256() {
13912
14320
  });
13913
14321
  const privateKeyB64 = privateKey.toString("base64");
13914
14322
  const publicKeyB64 = publicKey.toString("base64");
13915
- const fingerprint = crypto6.createHash("sha256").update(publicKey).digest("hex");
14323
+ const fingerprint = crypto7.createHash("sha256").update(publicKey).digest("hex");
13916
14324
  return {
13917
14325
  privateKey: privateKeyB64,
13918
14326
  publicKey: publicKeyB64,
@@ -13927,7 +14335,7 @@ function generateKeyPair(algorithm = "ES256") {
13927
14335
  function generateClientToken(payload, privateKeyB64, algorithm, options) {
13928
14336
  try {
13929
14337
  const privateKeyDER = Buffer.from(privateKeyB64, "base64");
13930
- const privateKeyObject = crypto6.createPrivateKey({
14338
+ const privateKeyObject = crypto7.createPrivateKey({
13931
14339
  key: privateKeyDER,
13932
14340
  format: "der",
13933
14341
  type: "pkcs8"
@@ -13971,10 +14379,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
13971
14379
 
13972
14380
  // src/server/lib/session.ts
13973
14381
  import * as jose2 from "jose";
13974
- import { env as env12 } from "@spfn/auth/config";
14382
+ import { env as env13 } from "@spfn/auth/config";
13975
14383
  import { env as coreEnv } from "@spfn/core/config";
13976
14384
  async function getSessionSecretKey() {
13977
- const secret = env12.SPFN_AUTH_SESSION_SECRET;
14385
+ const secret = env13.SPFN_AUTH_SESSION_SECRET;
13978
14386
  const encoder = new TextEncoder();
13979
14387
  const data = encoder.encode(secret);
13980
14388
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
@@ -14056,14 +14464,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
14056
14464
  }
14057
14465
 
14058
14466
  // src/server/setup.ts
14059
- import { env as env13 } from "@spfn/auth/config";
14467
+ import { env as env14 } from "@spfn/auth/config";
14060
14468
  import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
14061
14469
  init_repositories();
14062
14470
  function parseAdminAccounts() {
14063
14471
  const accounts = [];
14064
- if (env13.SPFN_AUTH_ADMIN_ACCOUNTS) {
14472
+ if (env14.SPFN_AUTH_ADMIN_ACCOUNTS) {
14065
14473
  try {
14066
- const accountsJson = env13.SPFN_AUTH_ADMIN_ACCOUNTS;
14474
+ const accountsJson = env14.SPFN_AUTH_ADMIN_ACCOUNTS;
14067
14475
  const parsed = JSON.parse(accountsJson);
14068
14476
  if (!Array.isArray(parsed)) {
14069
14477
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
@@ -14090,11 +14498,11 @@ function parseAdminAccounts() {
14090
14498
  return accounts;
14091
14499
  }
14092
14500
  }
14093
- const adminEmails = env13.SPFN_AUTH_ADMIN_EMAILS;
14501
+ const adminEmails = env14.SPFN_AUTH_ADMIN_EMAILS;
14094
14502
  if (adminEmails) {
14095
14503
  const emails = adminEmails.split(",").map((s) => s.trim());
14096
- const passwords = (env13.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
14097
- const roles2 = (env13.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
14504
+ const passwords = (env14.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
14505
+ const roles2 = (env14.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
14098
14506
  if (passwords.length !== emails.length) {
14099
14507
  authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
14100
14508
  return accounts;
@@ -14116,8 +14524,8 @@ function parseAdminAccounts() {
14116
14524
  }
14117
14525
  return accounts;
14118
14526
  }
14119
- const adminEmail = env13.SPFN_AUTH_ADMIN_EMAIL;
14120
- const adminPassword = env13.SPFN_AUTH_ADMIN_PASSWORD;
14527
+ const adminEmail = env14.SPFN_AUTH_ADMIN_EMAIL;
14528
+ const adminPassword = env14.SPFN_AUTH_ADMIN_PASSWORD;
14121
14529
  if (adminEmail && adminPassword) {
14122
14530
  accounts.push({
14123
14531
  email: adminEmail,
@@ -14253,6 +14661,7 @@ export {
14253
14661
  RolePermissionsRepository,
14254
14662
  RolesRepository,
14255
14663
  SOCIAL_PROVIDERS,
14664
+ SignupLinkTokensRepository,
14256
14665
  SocialAccountsRepository,
14257
14666
  TargetTypeSchema,
14258
14667
  USER_STATUSES,
@@ -14287,9 +14696,11 @@ export {
14287
14696
  cancelInvitation,
14288
14697
  changePasswordService,
14289
14698
  checkUsernameAvailableService,
14699
+ completeSignupService,
14290
14700
  configureAuth,
14291
14701
  configureDeletion,
14292
14702
  configureOAuthTokenCipher,
14703
+ confirmSignupLinkService,
14293
14704
  createAuthDeletionJobRouter,
14294
14705
  createAuthDeletionPurgeJob,
14295
14706
  createAuthLifecycle,
@@ -14360,6 +14771,7 @@ export {
14360
14771
  isEncrypted,
14361
14772
  isGoogleOAuthEnabled,
14362
14773
  isOAuthProviderEnabled,
14774
+ isSafeReturnPath,
14363
14775
  issueOneTimeTokenService,
14364
14776
  issueOpsTokenService,
14365
14777
  kakaoProvider,
@@ -14394,6 +14806,7 @@ export {
14394
14806
  registerService,
14395
14807
  removePermissionFromRole,
14396
14808
  requestAccountDeletionService,
14809
+ requestSignupLinkService,
14397
14810
  requireAnyPermission,
14398
14811
  requireEnabledProvider,
14399
14812
  requireOpsScope,
@@ -14418,6 +14831,8 @@ export {
14418
14831
  setRolePermissions,
14419
14832
  shouldRefreshSession,
14420
14833
  shouldRotateKey,
14834
+ signupLinkTokens,
14835
+ signupLinkTokensRepository,
14421
14836
  socialAccountsRepository,
14422
14837
  sweepDuePurges,
14423
14838
  unsealSession,