@spfn/auth 0.3.0-beta.4 → 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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AuthInitOptions, l as OAuthProvider, j as VerificationPurpose, h as PermissionCategory, n as AuthContext } from './authenticate-Ctul07Sc.js';
2
- export { o as AuthProfileOutcome, p as AuthProfileVerifier, C as ChangePasswordParams, D as DeviceNameSchema, E as EmailSchema, I as IssueOneTimeTokenResult, q as KEY_FINGERPRINT_PREFIX_LENGTH, K as KeySummary, r as LoginParams, L as LoginResult, s as LogoutParams, N as NativeVerifyOptions, t as NormalizedIdentity, u as OAuthCallbackParams, v as OAuthCallbackResult, w as OAuthCodeExchangeOptions, x as OAuthNativeParams, d as OAuthNativeResult, y as OAuthStartParams, O as OAuthStartResult, z as OAuthTokens, B as PasswordSchema, F as PhoneSchema, G as PlatformSchema, H as RegisterParams, J as RegisterPublicKeyParams, a as RegisterResult, M as RevokeAllKeysParams, c as RevokeAllKeysResult, Q as RevokeKeyParams, T as RotateKeyParams, b as RotateKeyResult, W as SendVerificationCodeParams, S as SendVerificationCodeResult, X as TargetTypeSchema, Y as UnlinkNotification, Z as UnlinkNotifyRejection, _ as UnlinkNotifyRequest, $ as UnlinkNotifyResult, V as VERIFICATION_PURPOSES, i as VERIFICATION_TARGET_TYPES, a0 as VerificationPurposeSchema, k as VerificationTargetType, a1 as VerifyCodeParams, a2 as VerifyCodeResult, m as authRouter, a3 as authenticate, a4 as buildOAuthErrorUrl, a5 as changePasswordService, a6 as getEnabledOAuthProviders, a7 as getGoogleAccessToken, a8 as getOAuthProvider, a9 as getRegisteredProviders, aa as isOAuthProviderEnabled, ab as issueOneTimeTokenService, ac as listKeysService, ad as loginService, ae as logoutService, af as oauthCallbackService, ag as oauthNativeService, ah as oauthStartService, ai as oauthUnlinkNotifyService, aj as optionalAuth, ak as registerOAuthProvider, al as registerPublicKeyService, am as registerService, an as requireEnabledProvider, ao as resolveAuthenticatedUser, ap as revokeAllKeysService, aq as revokeKeyService, ar as rotateKeyService, as as runAuthProfile, at as selectAuthProfile, au as sendVerificationCodeService, av as verifyCodeService, aw as verifyOneTimeTokenService } from './authenticate-Ctul07Sc.js';
1
+ import { A as AuthInitOptions, n as OAuthProvider, k as VerificationPurpose, i as PermissionCategory, o as AuthContext } from './authenticate-98lBIMxP.js';
2
+ export { p as AuthProfileOutcome, q as AuthProfileVerifier, r as ChangePasswordParams, s as CompleteSignupParams, t as ConfirmSignupLinkParams, C as ConfirmSignupLinkResult, D as DeviceNameSchema, E as EmailSchema, I as IssueOneTimeTokenResult, u as KEY_FINGERPRINT_PREFIX_LENGTH, K as KeySummary, v as LoginParams, L as LoginResult, w as LogoutParams, N as NativeVerifyOptions, x as NormalizedIdentity, y as OAuthCallbackParams, z as OAuthCallbackResult, B as OAuthCodeExchangeOptions, F as OAuthNativeParams, e as OAuthNativeResult, G as OAuthStartParams, O as OAuthStartResult, H as OAuthTokens, J as PasswordSchema, M as PhoneSchema, Q as PlatformSchema, T as RegisterParams, W as RegisterPublicKeyParams, a as RegisterResult, X as RequestSignupLinkParams, b as RequestSignupLinkResult, Y as RevokeAllKeysParams, d as RevokeAllKeysResult, Z as RevokeKeyParams, _ as RotateKeyParams, c as RotateKeyResult, $ as SendVerificationCodeParams, S as SendVerificationCodeResult, a0 as TargetTypeSchema, a1 as UnlinkNotification, a2 as UnlinkNotifyRejection, a3 as UnlinkNotifyRequest, a4 as UnlinkNotifyResult, V as VERIFICATION_PURPOSES, j as VERIFICATION_TARGET_TYPES, a5 as VerificationPurposeSchema, l as VerificationTargetType, a6 as VerifyCodeParams, a7 as VerifyCodeResult, m as authRouter, a8 as authenticate, a9 as buildOAuthErrorUrl, aa as changePasswordService, ab as completeSignupService, ac as confirmSignupLinkService, ad as getEnabledOAuthProviders, ae as getGoogleAccessToken, af as getOAuthProvider, ag as getRegisteredProviders, ah as isOAuthProviderEnabled, ai as isSafeReturnPath, aj as issueOneTimeTokenService, ak as listKeysService, al as loginService, am as logoutService, an as oauthCallbackService, ao as oauthNativeService, ap as oauthStartService, aq as oauthUnlinkNotifyService, ar as optionalAuth, as as registerOAuthProvider, at as registerPublicKeyService, au as registerService, av as requestSignupLinkService, aw as requireEnabledProvider, ax as resolveAuthenticatedUser, ay as revokeAllKeysService, az as revokeKeyService, aA as rotateKeyService, aB as runAuthProfile, aC as selectAuthProfile, aD as sendVerificationCodeService, aE as verifyCodeService, aF as verifyOneTimeTokenService } from './authenticate-98lBIMxP.js';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
  import { K as KeyAlgorithmType, d as InvitationStatus, j as SocialProvider, c as AccountDeletionRequestedBy, i as PurgeStrategy } from './types-DYyhze28.js';
5
5
  export { A as ACCOUNT_DELETION_REQUESTED_BY, a as ACCOUNT_DELETION_REQUEST_STATUSES, b as AccountDeletionRequestStatus, I as INVITATION_STATUSES, e as KEY_ALGORITHM, f as KEY_DEVICE_NAME_MAX_LENGTH, g as KEY_PLATFORM, h as KeyPlatformType, P as PURGE_STRATEGIES, S as SOCIAL_PROVIDERS, U as USER_STATUSES, k as UserStatus } from './types-DYyhze28.js';
@@ -431,6 +431,42 @@ declare function updateUsernameService(userId: string | number | bigint, usernam
431
431
  */
432
432
  declare function initializeAuth(options?: AuthInitOptions): Promise<void>;
433
433
 
434
+ /**
435
+ * @spfn/auth - Stored email normalization
436
+ *
437
+ * Brings existing rows into the canonical form the repository now writes and
438
+ * reads. Without it, normalizing the code alone would lock out every account
439
+ * whose address was stored with any capitalization: the lookup folds the typed
440
+ * address to lower case, the stored one stays mixed, and they stop matching.
441
+ *
442
+ * Runs once at startup and records that it ran, so a healthy install pays one
443
+ * small metadata read per boot rather than a scan of the users table.
444
+ *
445
+ * The work itself stays in the database — one query to find the addresses two
446
+ * rows would share, one statement to fold the rest. Nothing proportional to the
447
+ * table is carried through the application, which is what keeps a legacy
448
+ * install's first boot from turning into a row-at-a-time migration.
449
+ */
450
+ interface EmailNormalizationResult {
451
+ /** Rows rewritten to canonical form. */
452
+ normalized: number;
453
+ /** User id groups that share an address once folded, left untouched. */
454
+ conflicts: number[][];
455
+ }
456
+ /**
457
+ * Rewrite stored addresses to canonical form, skipping anything that would
458
+ * collide.
459
+ *
460
+ * A collision means two accounts differ only by capitalization. Which of them is
461
+ * the real one, and what happens to the other's data, is not a question this can
462
+ * answer — so every row in the group is left exactly as it is and all of their
463
+ * ids are reported. The alternative, merging or deleting one, destroys an
464
+ * account on a guess.
465
+ *
466
+ * @returns what was rewritten and what was left for an operator
467
+ */
468
+ declare function normalizeStoredEmails(): Promise<EmailNormalizationResult>;
469
+
434
470
  /**
435
471
  * @spfn/auth - Permission Service
436
472
  *
@@ -3057,6 +3093,209 @@ declare const verificationCodes: drizzle_orm_pg_core.PgTableWithColumns<{
3057
3093
  type VerificationCode = typeof verificationCodes.$inferSelect;
3058
3094
  type NewVerificationCode = typeof verificationCodes.$inferInsert;
3059
3095
 
3096
+ /**
3097
+ * @spfn/auth - Signup Link Tokens Entity
3098
+ *
3099
+ * Backs the verified-email signup flow: a one-time confirmation link is emailed,
3100
+ * opening it mints a short-lived setup session, and setting a password on that
3101
+ * session creates the account.
3102
+ *
3103
+ * A separate table from `verification_codes` on purpose. That table stores its
3104
+ * six-digit code in plaintext, which is acceptable for a short-lived numeric code
3105
+ * bounded by an attempt counter, but a link token is a bearer credential sitting
3106
+ * in someone's mailbox and must only ever be stored as a hash. Sharing one table
3107
+ * would make "plaintext or hash" a per-row property that nothing enforces.
3108
+ */
3109
+ declare const signupLinkTokens: drizzle_orm_pg_core.PgTableWithColumns<{
3110
+ name: "signup_link_tokens";
3111
+ schema: string;
3112
+ columns: {
3113
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
3114
+ name: string;
3115
+ tableName: "signup_link_tokens";
3116
+ dataType: "object date";
3117
+ data: Date;
3118
+ driverParam: string;
3119
+ notNull: true;
3120
+ hasDefault: true;
3121
+ isPrimaryKey: false;
3122
+ isAutoincrement: false;
3123
+ hasRuntimeDefault: false;
3124
+ enumValues: undefined;
3125
+ identity: undefined;
3126
+ generated: undefined;
3127
+ }>;
3128
+ updatedAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
3129
+ name: string;
3130
+ tableName: "signup_link_tokens";
3131
+ dataType: "object date";
3132
+ data: Date;
3133
+ driverParam: string;
3134
+ notNull: true;
3135
+ hasDefault: true;
3136
+ isPrimaryKey: false;
3137
+ isAutoincrement: false;
3138
+ hasRuntimeDefault: false;
3139
+ enumValues: undefined;
3140
+ identity: undefined;
3141
+ generated: undefined;
3142
+ }>;
3143
+ id: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
3144
+ name: string;
3145
+ tableName: "signup_link_tokens";
3146
+ dataType: "number int53";
3147
+ data: number;
3148
+ driverParam: number;
3149
+ notNull: true;
3150
+ hasDefault: true;
3151
+ isPrimaryKey: false;
3152
+ isAutoincrement: false;
3153
+ hasRuntimeDefault: false;
3154
+ enumValues: undefined;
3155
+ identity: undefined;
3156
+ generated: undefined;
3157
+ }>;
3158
+ email: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3159
+ name: string;
3160
+ tableName: "signup_link_tokens";
3161
+ dataType: "string";
3162
+ data: string;
3163
+ driverParam: string;
3164
+ notNull: true;
3165
+ hasDefault: false;
3166
+ isPrimaryKey: false;
3167
+ isAutoincrement: false;
3168
+ hasRuntimeDefault: false;
3169
+ enumValues: undefined;
3170
+ identity: undefined;
3171
+ generated: undefined;
3172
+ }>;
3173
+ tokenHash: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3174
+ name: string;
3175
+ tableName: "signup_link_tokens";
3176
+ dataType: "string";
3177
+ data: string;
3178
+ driverParam: string;
3179
+ notNull: true;
3180
+ hasDefault: false;
3181
+ isPrimaryKey: false;
3182
+ isAutoincrement: false;
3183
+ hasRuntimeDefault: false;
3184
+ enumValues: undefined;
3185
+ identity: undefined;
3186
+ generated: undefined;
3187
+ }>;
3188
+ returnPath: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3189
+ name: string;
3190
+ tableName: "signup_link_tokens";
3191
+ dataType: "string";
3192
+ data: string;
3193
+ driverParam: string;
3194
+ notNull: false;
3195
+ hasDefault: false;
3196
+ isPrimaryKey: false;
3197
+ isAutoincrement: false;
3198
+ hasRuntimeDefault: false;
3199
+ enumValues: undefined;
3200
+ identity: undefined;
3201
+ generated: undefined;
3202
+ }>;
3203
+ expiresAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>, {
3204
+ name: string;
3205
+ tableName: "signup_link_tokens";
3206
+ dataType: "object date";
3207
+ data: Date;
3208
+ driverParam: string;
3209
+ notNull: true;
3210
+ hasDefault: false;
3211
+ isPrimaryKey: false;
3212
+ isAutoincrement: false;
3213
+ hasRuntimeDefault: false;
3214
+ enumValues: undefined;
3215
+ identity: undefined;
3216
+ generated: undefined;
3217
+ }>;
3218
+ consumedAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTimestampBuilder, {
3219
+ name: string;
3220
+ tableName: "signup_link_tokens";
3221
+ dataType: "object date";
3222
+ data: Date;
3223
+ driverParam: string;
3224
+ notNull: false;
3225
+ hasDefault: false;
3226
+ isPrimaryKey: false;
3227
+ isAutoincrement: false;
3228
+ hasRuntimeDefault: false;
3229
+ enumValues: undefined;
3230
+ identity: undefined;
3231
+ generated: undefined;
3232
+ }>;
3233
+ supersededAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTimestampBuilder, {
3234
+ name: string;
3235
+ tableName: "signup_link_tokens";
3236
+ dataType: "object date";
3237
+ data: Date;
3238
+ driverParam: string;
3239
+ notNull: false;
3240
+ hasDefault: false;
3241
+ isPrimaryKey: false;
3242
+ isAutoincrement: false;
3243
+ hasRuntimeDefault: false;
3244
+ enumValues: undefined;
3245
+ identity: undefined;
3246
+ generated: undefined;
3247
+ }>;
3248
+ setupSecretHash: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3249
+ name: string;
3250
+ tableName: "signup_link_tokens";
3251
+ dataType: "string";
3252
+ data: string;
3253
+ driverParam: string;
3254
+ notNull: false;
3255
+ hasDefault: false;
3256
+ isPrimaryKey: false;
3257
+ isAutoincrement: false;
3258
+ hasRuntimeDefault: false;
3259
+ enumValues: undefined;
3260
+ identity: undefined;
3261
+ generated: undefined;
3262
+ }>;
3263
+ setupExpiresAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTimestampBuilder, {
3264
+ name: string;
3265
+ tableName: "signup_link_tokens";
3266
+ dataType: "object date";
3267
+ data: Date;
3268
+ driverParam: string;
3269
+ notNull: false;
3270
+ hasDefault: false;
3271
+ isPrimaryKey: false;
3272
+ isAutoincrement: false;
3273
+ hasRuntimeDefault: false;
3274
+ enumValues: undefined;
3275
+ identity: undefined;
3276
+ generated: undefined;
3277
+ }>;
3278
+ completedAt: drizzle_orm_pg_core.PgBuildColumn<"signup_link_tokens", drizzle_orm_pg_core.PgTimestampBuilder, {
3279
+ name: string;
3280
+ tableName: "signup_link_tokens";
3281
+ dataType: "object date";
3282
+ data: Date;
3283
+ driverParam: string;
3284
+ notNull: false;
3285
+ hasDefault: false;
3286
+ isPrimaryKey: false;
3287
+ isAutoincrement: false;
3288
+ hasRuntimeDefault: false;
3289
+ enumValues: undefined;
3290
+ identity: undefined;
3291
+ generated: undefined;
3292
+ }>;
3293
+ };
3294
+ dialect: "pg";
3295
+ }>;
3296
+ type SignupLinkToken = typeof signupLinkTokens.$inferSelect;
3297
+ type NewSignupLinkToken = typeof signupLinkTokens.$inferInsert;
3298
+
3060
3299
  /**
3061
3300
  * @spfn/auth - Permissions Entity
3062
3301
  *
@@ -3629,6 +3868,38 @@ declare class UsersRepository extends BaseRepository {
3629
3868
  phoneVerifiedAt: Date | null;
3630
3869
  lastLoginAt: Date | null;
3631
3870
  }>;
3871
+ /**
3872
+ * 이메일로 사용자 조회 — 저장된 형태와 무관하게 찾는다.
3873
+ *
3874
+ * `findByEmail` asks whether a row holds this exact address; this asks
3875
+ * whether any row *is* this address, whatever form it was written in. The
3876
+ * difference matters to a caller that answers "no" by creating an account:
3877
+ * a lookup that misses a row stored in another form would make a second
3878
+ * account for a person who already has one, and the unique constraint
3879
+ * cannot object because the two stored strings differ.
3880
+ *
3881
+ * Folding in the predicate means no index on `email` applies, so this is for
3882
+ * the few addresses a caller decides about — admin seeding — not for a
3883
+ * request path. Write primary: the answer decides whether to insert.
3884
+ */
3885
+ findByEmailInAnyStoredForm(email: string): Promise<{
3886
+ deletedAt: Date | null;
3887
+ deletedBy: string | null;
3888
+ createdAt: Date;
3889
+ updatedAt: Date;
3890
+ id: number;
3891
+ publicId: string;
3892
+ email: string | null;
3893
+ phone: string | null;
3894
+ username: string | null;
3895
+ passwordHash: string | null;
3896
+ passwordChangeRequired: boolean;
3897
+ roleId: number;
3898
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3899
+ emailVerifiedAt: Date | null;
3900
+ phoneVerifiedAt: Date | null;
3901
+ lastLoginAt: Date | null;
3902
+ }>;
3632
3903
  /**
3633
3904
  * 전화번호로 사용자 조회
3634
3905
  * Read replica 사용
@@ -3770,6 +4041,51 @@ declare class UsersRepository extends BaseRepository {
3770
4041
  deletedAt: Date | null;
3771
4042
  deletedBy: string | null;
3772
4043
  }>;
4044
+ /**
4045
+ * User ids grouped by an address two or more rows share once folded.
4046
+ *
4047
+ * The whole comparison happens in the database and only the colliding groups
4048
+ * come back, so the size of the answer is the size of the problem rather
4049
+ * than the size of the table. `users.email` is unique, so a group of more
4050
+ * than one can only be rows that differ by capitalization or padding —
4051
+ * exactly the ones a rewrite cannot decide between.
4052
+ *
4053
+ * Every member id is returned, including a row already holding the
4054
+ * canonical form, because the operator has to compare the accounts against
4055
+ * each other to settle which is real.
4056
+ *
4057
+ * Write primary: the caller is about to rewrite rows and a replica could
4058
+ * still be showing the pre-fix state.
4059
+ */
4060
+ findEmailConflictGroups(): Promise<number[][]>;
4061
+ /**
4062
+ * Fold every stored address to canonical form, leaving the given ids alone.
4063
+ *
4064
+ * One statement rather than a row at a time: the rewrite is the same
4065
+ * expression the detection uses, so the database can do it in place. A
4066
+ * legacy install with a large users table therefore pays one update instead
4067
+ * of a round trip per row on the boot path, and no list of addresses is ever
4068
+ * carried through the application.
4069
+ *
4070
+ * The excluded ids travel as a single array parameter, so the count of
4071
+ * conflicts cannot run into the protocol's limit on bind parameters.
4072
+ *
4073
+ * One statement also means all or nothing. `users.email` is unique, so if an
4074
+ * instance still running the old code registers a canonical address in the
4075
+ * moment between the conflict query and this update, the update aborts and
4076
+ * nothing is folded on this boot. The next boot sees that pair as a conflict
4077
+ * and folds everything else, so the repair is deferred rather than lost.
4078
+ *
4079
+ * `lower(btrim(...))` is the SQL spelling of `normalizeEmail`. The two agree
4080
+ * on every address this package's validation accepts (ASCII, no interior
4081
+ * whitespace); an address outside that set — reachable only by an app
4082
+ * writing to the repository directly — may fold differently in a database
4083
+ * whose collation lower-cases non-ASCII letters.
4084
+ *
4085
+ * @param excludedIds - Rows to leave untouched, normally the conflict groups
4086
+ * @returns How many rows were rewritten
4087
+ */
4088
+ normalizeEmailsExcept(excludedIds: number[]): Promise<number>;
3773
4089
  /**
3774
4090
  * 사용자 정보 업데이트
3775
4091
  * Write primary 사용
@@ -4428,6 +4744,75 @@ declare class VerificationCodesRepository extends BaseRepository {
4428
4744
  }
4429
4745
  declare const verificationCodesRepository: VerificationCodesRepository;
4430
4746
 
4747
+ /**
4748
+ * Signup Link Tokens Repository
4749
+ *
4750
+ * Data access for the verified-email signup flow. Extends BaseRepository for
4751
+ * transaction-context detection and read/write splitting.
4752
+ *
4753
+ * The two claim methods (`claimLink`, `claimSetupSession`) update conditionally
4754
+ * and return the row only if THIS call was the one that moved it. Two concurrent
4755
+ * confirms — or two concurrent password submits — therefore produce one winner
4756
+ * and one refusal, rather than both proceeding on a row they each read as
4757
+ * unclaimed. Read-then-write would lose that race.
4758
+ */
4759
+
4760
+ declare class SignupLinkTokensRepository extends BaseRepository {
4761
+ /**
4762
+ * Create a signup link row.
4763
+ * Write primary.
4764
+ */
4765
+ create(data: NewSignupLinkToken): Promise<SignupLinkToken>;
4766
+ /**
4767
+ * Find a row by the hash of an emailed token, in any state.
4768
+ *
4769
+ * Deliberately unfiltered: the caller decides why a link is refused, and a
4770
+ * row filtered out here would be indistinguishable from an unknown token in
4771
+ * the logs.
4772
+ *
4773
+ * Read replica.
4774
+ */
4775
+ findByTokenHash(tokenHash: string): Promise<SignupLinkToken | null>;
4776
+ /**
4777
+ * Find a row by the hash of a setup session secret, in any state.
4778
+ * Read replica.
4779
+ */
4780
+ findBySetupSecretHash(setupSecretHash: string): Promise<SignupLinkToken | null>;
4781
+ /**
4782
+ * Consume a link and open a setup session on it, but only if it is still
4783
+ * unconsumed and not superseded.
4784
+ *
4785
+ * @returns the updated row, or null if another request claimed it first
4786
+ */
4787
+ claimLink(id: number, setupSecretHash: string, setupExpiresAt: Date): Promise<SignupLinkToken | null>;
4788
+ /**
4789
+ * Mark a setup session as completed, but only if it has not completed
4790
+ * already.
4791
+ *
4792
+ * @returns the updated row, or null if another request completed it first
4793
+ */
4794
+ claimSetupSession(id: number): Promise<SignupLinkToken | null>;
4795
+ /**
4796
+ * Supersede every live link for an address, so requesting a new one
4797
+ * invalidates the previous one and any setup session opened from it.
4798
+ *
4799
+ * Write primary.
4800
+ *
4801
+ * @returns number of rows superseded
4802
+ */
4803
+ supersedeLiveForEmail(email: string): Promise<number>;
4804
+ /**
4805
+ * Delete every signup link row for an address (account destruction).
4806
+ *
4807
+ * Rows key on the email text and carry no user FK, so destruction has to
4808
+ * clean them up by address the way verification codes are cleaned up.
4809
+ *
4810
+ * Write primary.
4811
+ */
4812
+ deleteByEmail(email: string): Promise<number>;
4813
+ }
4814
+ declare const signupLinkTokensRepository: SignupLinkTokensRepository;
4815
+
4431
4816
  /**
4432
4817
  * Roles Repository
4433
4818
  *
@@ -5145,12 +5530,12 @@ declare class InvitationsRepository extends BaseRepository {
5145
5530
  create(data: NewInvitation): Promise<{
5146
5531
  email: string;
5147
5532
  metadata: Record<string, any> | null;
5533
+ token: string;
5148
5534
  id: number;
5149
5535
  roleId: number;
5150
5536
  createdAt: Date;
5151
5537
  updatedAt: Date;
5152
5538
  status: "pending" | "accepted" | "expired" | "cancelled";
5153
- token: string;
5154
5539
  invitedBy: number;
5155
5540
  expiresAt: Date;
5156
5541
  acceptedAt: Date | null;
@@ -5179,12 +5564,12 @@ declare class InvitationsRepository extends BaseRepository {
5179
5564
  deleteById(id: number): Promise<{
5180
5565
  email: string;
5181
5566
  metadata: Record<string, any> | null;
5567
+ token: string;
5182
5568
  id: number;
5183
5569
  roleId: number;
5184
5570
  createdAt: Date;
5185
5571
  updatedAt: Date;
5186
5572
  status: "pending" | "accepted" | "expired" | "cancelled";
5187
- token: string;
5188
5573
  invitedBy: number;
5189
5574
  expiresAt: Date;
5190
5575
  acceptedAt: Date | null;
@@ -5581,10 +5966,10 @@ declare class AccountDeletionRequestsRepository extends BaseRepository {
5581
5966
  reason: string | null;
5582
5967
  purgeScheduledAt: Date;
5583
5968
  cancelledAt: Date | null;
5969
+ completedAt: Date | null;
5584
5970
  userPublicId: string;
5585
5971
  requestedAt: Date;
5586
5972
  requestedBy: "admin" | "self";
5587
- completedAt: Date | null;
5588
5973
  purgeStrategy: "anonymize" | "hard-delete" | null;
5589
5974
  }>;
5590
5975
  /**
@@ -5669,6 +6054,43 @@ declare class OpsTokensRepository extends BaseRepository {
5669
6054
  }
5670
6055
  declare const opsTokensRepository: OpsTokensRepository;
5671
6056
 
6057
+ /**
6058
+ * @spfn/auth - Email address normalization
6059
+ *
6060
+ * One canonical form for an address, applied on the way in AND on the way out,
6061
+ * so `Foo@Example.com` and `foo@example.com` are the same account everywhere.
6062
+ *
6063
+ * Both sides have to agree or normalization makes things worse rather than
6064
+ * better: normalizing only the lookup means an address stored in mixed case can
6065
+ * no longer be found, and normalizing only the write means an old row stays
6066
+ * unreachable. `UsersRepository` therefore applies this to every read and every
6067
+ * write of `users.email`, which is what keeps a call site from forgetting.
6068
+ */
6069
+ /**
6070
+ * Trim and lower-case an address.
6071
+ *
6072
+ * Nothing else. Two normalizations that look tempting are deliberately absent:
6073
+ * stripping dots and cutting at `+` are Gmail delivery rules, not internet ones,
6074
+ * and applying them would merge addresses that other providers treat as
6075
+ * genuinely different people.
6076
+ *
6077
+ * The local part is technically case-sensitive per RFC 5321, but no mail
6078
+ * provider in practice treats it that way, while users routinely retype their
6079
+ * address with different capitalization. Matching the practice avoids duplicate
6080
+ * accounts; honouring the RFC creates them.
6081
+ *
6082
+ * @param email - Address as supplied
6083
+ * @returns The canonical form used for storage and lookup
6084
+ */
6085
+ declare function normalizeEmail(email: string): string;
6086
+ /**
6087
+ * Normalize when there is an address, pass null/undefined through untouched.
6088
+ *
6089
+ * Accounts registered by phone have no email, and `null` is meaningfully
6090
+ * different from `''` in that column.
6091
+ */
6092
+ declare function normalizeOptionalEmail<T extends string | null | undefined>(email: T): T;
6093
+
5672
6094
  /**
5673
6095
  * @spfn/auth - Password Helpers
5674
6096
  *
@@ -6227,6 +6649,8 @@ declare const COOKIE_NAMES: {
6227
6649
  readonly OAUTH_PENDING: string;
6228
6650
  /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
6229
6651
  readonly OAUTH_CSRF: string;
6652
+ /** Password-setup session for verified-email signup — temporary, single-purpose */
6653
+ readonly SIGNUP_SETUP: string;
6230
6654
  };
6231
6655
  /**
6232
6656
  * OAuth CSRF 쿠키를 PORT 접미사와 무관하게 전부 수집한다.
@@ -6270,6 +6694,12 @@ interface BeforeRegisterContext {
6270
6694
  channel: RegisterChannel;
6271
6695
  /** Social provider — only set when channel is 'oauth' */
6272
6696
  provider?: SocialProvider;
6697
+ /**
6698
+ * Canonical form of the address — trimmed and lower-cased, the same form
6699
+ * the account is stored under. A policy keyed on the address (a denylist, a
6700
+ * domain allowlist) therefore matches whatever capitalization the person
6701
+ * typed, instead of being walked past by `Blocked@Example.com`.
6702
+ */
6273
6703
  email?: string;
6274
6704
  /**
6275
6705
  * Whether the email is verified — only set when channel is 'oauth'.
@@ -6349,6 +6779,11 @@ declare function getAuthConfig(): AuthConfig;
6349
6779
  * Single entry point for every registration channel so a new channel cannot
6350
6780
  * forget the configured-check. Callers invoke this right before creating the
6351
6781
  * user row.
6782
+ *
6783
+ * The address is folded here rather than at each call site, for the same reason
6784
+ * the check itself lives here: three channels supply it, and a policy that sees
6785
+ * a different spelling depending on which one the person came through is a
6786
+ * policy that can be walked past.
6352
6787
  */
6353
6788
  declare function runBeforeRegister(context: BeforeRegisterContext): Promise<void>;
6354
6789
  /**
@@ -6808,8 +7243,8 @@ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
6808
7243
  [x: string]: unknown;
6809
7244
  } | undefined;
6810
7245
  email: string;
6811
- roleId: number;
6812
7246
  token: string;
7247
+ roleId: number;
6813
7248
  invitedBy: string;
6814
7249
  expiresAt: string;
6815
7250
  invitationId: string;
@@ -6915,4 +7350,4 @@ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
6915
7350
  type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
6916
7351
  type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
6917
7352
 
6918
- export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewOpsToken, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type OAuthUnlinkedPayload, type OpsToken, OpsTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOpsToken, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, issueOpsTokenService, kakaoProvider, keysRepository, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, naverProvider, oauthUnlinkedEvent, oneTimeTokenAuth, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken };
7353
+ export { type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthDeletionCancelledPayload, type AuthDeletionCompletedPayload, type AuthDeletionConfig, type AuthDeletionRequestedPayload, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthLoginPayload, type AuthMetadataEntity, AuthMetadataRepository, AuthProviderSchema, type AuthRegisterPayload, type BeforeRegisterContext, COOKIE_NAMES, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, type DecryptedOAuthToken, type EmailNormalizationResult, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, type InvitationAcceptedPayload, type InvitationCreatedPayload, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, KeysRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewInvitation, type NewOpsToken, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewSignupLinkToken, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, type OAuthUnlinkedPayload, type OpsToken, OpsTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, type SessionPayload, type SignupLinkToken, SignupLinkTokensRepository, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenPayload, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOpsToken, type VerifyIdTokenParams, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertCanAssignRole, authDeletionCancelledEvent, authDeletionCompletedEvent, authDeletionRequestedEvent, authJobRouter, authLogger, authLoginEvent, authMetadata, authMetadataRepository, authRegisterEvent, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureDeletion, configureOAuthTokenCipher, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthLifecycle, createInvitation, createOAuthState, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, encryptToken, exchangeCodeForTokens, expireOldInvitations, generateOAuthNonce, generateToken, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getDeletionConfig, getDummyPasswordHash, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashPassword, initOneTimeTokenManager, initializeAuth, invitationAcceptedEvent, invitationCreatedEvent, invitationsRepository, isEncrypted, isGoogleOAuthEnabled, issueOpsTokenService, kakaoProvider, keysRepository, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, naverProvider, normalizeEmail, normalizeOptionalEmail, normalizeStoredEmails, oauthUnlinkedEvent, oneTimeTokenAuth, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, permissions, permissionsRepository, purgeUserService, refreshAccessToken, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, setRolePermissions, signupLinkTokens, signupLinkTokensRepository, socialAccountsRepository, sweepDuePurges, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken };