@spfn/auth 0.3.0-beta.3 → 0.3.0-beta.5
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/README.md +52 -2
- package/dist/client-proof.d.ts +23 -4
- package/dist/client-proof.js +57 -2
- package/dist/client-proof.js.map +1 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +162 -1
- package/dist/server.js +213 -23
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -4521,6 +4521,19 @@ var init_schema3 = __esm({
|
|
|
4521
4521
|
}
|
|
4522
4522
|
});
|
|
4523
4523
|
|
|
4524
|
+
// src/server/helpers/email.ts
|
|
4525
|
+
function normalizeEmail(email) {
|
|
4526
|
+
return email.trim().toLowerCase();
|
|
4527
|
+
}
|
|
4528
|
+
function normalizeOptionalEmail(email) {
|
|
4529
|
+
return typeof email === "string" ? normalizeEmail(email) : email;
|
|
4530
|
+
}
|
|
4531
|
+
var init_email = __esm({
|
|
4532
|
+
"src/server/helpers/email.ts"() {
|
|
4533
|
+
"use strict";
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4524
4537
|
// src/server/entities/schema.ts
|
|
4525
4538
|
import { createSchema } from "@spfn/core/db";
|
|
4526
4539
|
var authSchema;
|
|
@@ -5486,7 +5499,7 @@ var init_entities = __esm({
|
|
|
5486
5499
|
});
|
|
5487
5500
|
|
|
5488
5501
|
// src/server/repositories/users.repository.ts
|
|
5489
|
-
import { eq, and } from "drizzle-orm";
|
|
5502
|
+
import { eq, and, sql as sql4 } from "drizzle-orm";
|
|
5490
5503
|
import { BaseRepository } from "@spfn/core/db";
|
|
5491
5504
|
import { EntityNotFoundError, NotFoundError } from "@spfn/core/errors";
|
|
5492
5505
|
var UsersRepository, usersRepository;
|
|
@@ -5494,6 +5507,7 @@ var init_users_repository = __esm({
|
|
|
5494
5507
|
"src/server/repositories/users.repository.ts"() {
|
|
5495
5508
|
"use strict";
|
|
5496
5509
|
init_entities();
|
|
5510
|
+
init_email();
|
|
5497
5511
|
UsersRepository = class extends BaseRepository {
|
|
5498
5512
|
/**
|
|
5499
5513
|
* ID로 사용자 조회
|
|
@@ -5519,7 +5533,25 @@ var init_users_repository = __esm({
|
|
|
5519
5533
|
* Read replica 사용
|
|
5520
5534
|
*/
|
|
5521
5535
|
async findByEmail(email) {
|
|
5522
|
-
const result = await this.readDb.select().from(users).where(eq(users.email, email)).limit(1);
|
|
5536
|
+
const result = await this.readDb.select().from(users).where(eq(users.email, normalizeEmail(email))).limit(1);
|
|
5537
|
+
return result[0] ?? null;
|
|
5538
|
+
}
|
|
5539
|
+
/**
|
|
5540
|
+
* 이메일로 사용자 조회 — 저장된 형태와 무관하게 찾는다.
|
|
5541
|
+
*
|
|
5542
|
+
* `findByEmail` asks whether a row holds this exact address; this asks
|
|
5543
|
+
* whether any row *is* this address, whatever form it was written in. The
|
|
5544
|
+
* difference matters to a caller that answers "no" by creating an account:
|
|
5545
|
+
* a lookup that misses a row stored in another form would make a second
|
|
5546
|
+
* account for a person who already has one, and the unique constraint
|
|
5547
|
+
* cannot object because the two stored strings differ.
|
|
5548
|
+
*
|
|
5549
|
+
* Folding in the predicate means no index on `email` applies, so this is for
|
|
5550
|
+
* the few addresses a caller decides about — admin seeding — not for a
|
|
5551
|
+
* request path. Write primary: the answer decides whether to insert.
|
|
5552
|
+
*/
|
|
5553
|
+
async findByEmailInAnyStoredForm(email) {
|
|
5554
|
+
const result = await this.db.select().from(users).where(sql4`lower(btrim(${users.email})) = ${normalizeEmail(email)}`).limit(1);
|
|
5523
5555
|
return result[0] ?? null;
|
|
5524
5556
|
}
|
|
5525
5557
|
/**
|
|
@@ -5585,14 +5617,69 @@ var init_users_repository = __esm({
|
|
|
5585
5617
|
* Write primary 사용
|
|
5586
5618
|
*/
|
|
5587
5619
|
async create(data) {
|
|
5588
|
-
return await this._create(users, data);
|
|
5620
|
+
return await this._create(users, { ...data, email: normalizeOptionalEmail(data.email) });
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* User ids grouped by an address two or more rows share once folded.
|
|
5624
|
+
*
|
|
5625
|
+
* The whole comparison happens in the database and only the colliding groups
|
|
5626
|
+
* come back, so the size of the answer is the size of the problem rather
|
|
5627
|
+
* than the size of the table. `users.email` is unique, so a group of more
|
|
5628
|
+
* than one can only be rows that differ by capitalization or padding —
|
|
5629
|
+
* exactly the ones a rewrite cannot decide between.
|
|
5630
|
+
*
|
|
5631
|
+
* Every member id is returned, including a row already holding the
|
|
5632
|
+
* canonical form, because the operator has to compare the accounts against
|
|
5633
|
+
* each other to settle which is real.
|
|
5634
|
+
*
|
|
5635
|
+
* Write primary: the caller is about to rewrite rows and a replica could
|
|
5636
|
+
* still be showing the pre-fix state.
|
|
5637
|
+
*/
|
|
5638
|
+
async findEmailConflictGroups() {
|
|
5639
|
+
const rows = await this.db.select({ ids: sql4`array_agg(${users.id} ORDER BY ${users.id})` }).from(users).where(sql4`${users.email} IS NOT NULL`).groupBy(sql4`lower(btrim(${users.email}))`).having(sql4`count(*) > 1`);
|
|
5640
|
+
return rows.map((row) => row.ids.map(Number));
|
|
5641
|
+
}
|
|
5642
|
+
/**
|
|
5643
|
+
* Fold every stored address to canonical form, leaving the given ids alone.
|
|
5644
|
+
*
|
|
5645
|
+
* One statement rather than a row at a time: the rewrite is the same
|
|
5646
|
+
* expression the detection uses, so the database can do it in place. A
|
|
5647
|
+
* legacy install with a large users table therefore pays one update instead
|
|
5648
|
+
* of a round trip per row on the boot path, and no list of addresses is ever
|
|
5649
|
+
* carried through the application.
|
|
5650
|
+
*
|
|
5651
|
+
* The excluded ids travel as a single array parameter, so the count of
|
|
5652
|
+
* conflicts cannot run into the protocol's limit on bind parameters.
|
|
5653
|
+
*
|
|
5654
|
+
* One statement also means all or nothing. `users.email` is unique, so if an
|
|
5655
|
+
* instance still running the old code registers a canonical address in the
|
|
5656
|
+
* moment between the conflict query and this update, the update aborts and
|
|
5657
|
+
* nothing is folded on this boot. The next boot sees that pair as a conflict
|
|
5658
|
+
* and folds everything else, so the repair is deferred rather than lost.
|
|
5659
|
+
*
|
|
5660
|
+
* `lower(btrim(...))` is the SQL spelling of `normalizeEmail`. The two agree
|
|
5661
|
+
* on every address this package's validation accepts (ASCII, no interior
|
|
5662
|
+
* whitespace); an address outside that set — reachable only by an app
|
|
5663
|
+
* writing to the repository directly — may fold differently in a database
|
|
5664
|
+
* whose collation lower-cases non-ASCII letters.
|
|
5665
|
+
*
|
|
5666
|
+
* @param excludedIds - Rows to leave untouched, normally the conflict groups
|
|
5667
|
+
* @returns How many rows were rewritten
|
|
5668
|
+
*/
|
|
5669
|
+
async normalizeEmailsExcept(excludedIds) {
|
|
5670
|
+
const keepConflicts = excludedIds.length > 0 ? sql4` AND NOT (${users.id} = ANY(string_to_array(${excludedIds.join(",")}, ',')::bigint[]))` : sql4``;
|
|
5671
|
+
const pending = sql4`${users.email} IS NOT NULL AND ${users.email} <> lower(btrim(${users.email}))${keepConflicts}`;
|
|
5672
|
+
const [counted] = await this.db.select({ rows: sql4`count(*)` }).from(users).where(pending);
|
|
5673
|
+
await this.db.update(users).set({ email: sql4`lower(btrim(${users.email}))` }).where(pending);
|
|
5674
|
+
return Number(counted?.rows ?? 0);
|
|
5589
5675
|
}
|
|
5590
5676
|
/**
|
|
5591
5677
|
* 사용자 정보 업데이트
|
|
5592
5678
|
* Write primary 사용
|
|
5593
5679
|
*/
|
|
5594
5680
|
async updateById(id13, data) {
|
|
5595
|
-
const
|
|
5681
|
+
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();
|
|
5596
5683
|
return result[0] ?? null;
|
|
5597
5684
|
}
|
|
5598
5685
|
/**
|
|
@@ -5759,7 +5846,7 @@ var init_users_repository = __esm({
|
|
|
5759
5846
|
|
|
5760
5847
|
// src/server/repositories/keys.repository.ts
|
|
5761
5848
|
import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
|
|
5762
|
-
import { eq as eq2, and as and2, or, isNull, lt, ne, desc, sql as
|
|
5849
|
+
import { eq as eq2, and as and2, or, isNull, lt, ne, desc, sql as sql5 } from "drizzle-orm";
|
|
5763
5850
|
var LAST_USED_THROTTLE_MS, KeysRepository, keysRepository;
|
|
5764
5851
|
var init_keys_repository = __esm({
|
|
5765
5852
|
"src/server/repositories/keys.repository.ts"() {
|
|
@@ -6000,19 +6087,19 @@ var init_keys_repository = __esm({
|
|
|
6000
6087
|
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id13), lastUsedIsStale));
|
|
6001
6088
|
return;
|
|
6002
6089
|
}
|
|
6003
|
-
const identityChanged =
|
|
6090
|
+
const identityChanged = sql5`(
|
|
6004
6091
|
${userPublicKeys.clientKind} IS DISTINCT FROM ${identity.kind}
|
|
6005
6092
|
OR ${userPublicKeys.clientVersion} IS DISTINCT FROM ${identity.version}
|
|
6006
6093
|
OR ${userPublicKeys.clientContractVersion} IS DISTINCT FROM ${identity.contractVersion}
|
|
6007
6094
|
)`;
|
|
6008
6095
|
const now = /* @__PURE__ */ new Date();
|
|
6009
|
-
const nowParam =
|
|
6096
|
+
const nowParam = sql5`${now.toISOString()}::timestamptz`;
|
|
6010
6097
|
await this.db.update(userPublicKeys).set({
|
|
6011
6098
|
lastUsedAt: now,
|
|
6012
6099
|
clientKind: identity.kind,
|
|
6013
6100
|
clientVersion: identity.version,
|
|
6014
6101
|
clientContractVersion: identity.contractVersion,
|
|
6015
|
-
clientSeenAt:
|
|
6102
|
+
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
6016
6103
|
}).where(and2(
|
|
6017
6104
|
eq2(userPublicKeys.id, id13),
|
|
6018
6105
|
or(lastUsedIsStale, identityChanged)
|
|
@@ -6561,7 +6648,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6561
6648
|
});
|
|
6562
6649
|
|
|
6563
6650
|
// src/server/repositories/invitations.repository.ts
|
|
6564
|
-
import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as
|
|
6651
|
+
import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as sql6 } from "drizzle-orm";
|
|
6565
6652
|
import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
|
|
6566
6653
|
var InvitationsRepository, invitationsRepository;
|
|
6567
6654
|
var init_invitations_repository = __esm({
|
|
@@ -6570,6 +6657,7 @@ var init_invitations_repository = __esm({
|
|
|
6570
6657
|
init_users();
|
|
6571
6658
|
init_roles();
|
|
6572
6659
|
init_user_invitations();
|
|
6660
|
+
init_email();
|
|
6573
6661
|
InvitationsRepository = class extends BaseRepository9 {
|
|
6574
6662
|
/**
|
|
6575
6663
|
* ID로 초대 조회
|
|
@@ -6591,7 +6679,7 @@ var init_invitations_repository = __esm({
|
|
|
6591
6679
|
async findPendingByEmail(email) {
|
|
6592
6680
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
6593
6681
|
and6(
|
|
6594
|
-
eq9(userInvitations.email, email),
|
|
6682
|
+
eq9(userInvitations.email, normalizeEmail(email)),
|
|
6595
6683
|
eq9(userInvitations.status, "pending")
|
|
6596
6684
|
)
|
|
6597
6685
|
).limit(1);
|
|
@@ -6613,7 +6701,7 @@ var init_invitations_repository = __esm({
|
|
|
6613
6701
|
* 초대 생성
|
|
6614
6702
|
*/
|
|
6615
6703
|
async create(data) {
|
|
6616
|
-
return await this._create(userInvitations, data);
|
|
6704
|
+
return await this._create(userInvitations, { ...data, email: normalizeEmail(data.email) });
|
|
6617
6705
|
}
|
|
6618
6706
|
/**
|
|
6619
6707
|
* 초대 상태 업데이트
|
|
@@ -6695,7 +6783,7 @@ var init_invitations_repository = __esm({
|
|
|
6695
6783
|
conditions.push(eq9(userInvitations.invitedBy, invitedBy));
|
|
6696
6784
|
}
|
|
6697
6785
|
const whereClause = conditions.length > 0 ? and6(...conditions) : void 0;
|
|
6698
|
-
const countResult = await this.readDb.select({ count:
|
|
6786
|
+
const countResult = await this.readDb.select({ count: sql6`count(*)` }).from(userInvitations).where(whereClause);
|
|
6699
6787
|
const total = Number(countResult[0]?.count || 0);
|
|
6700
6788
|
const results = await this.readDb.select({
|
|
6701
6789
|
id: userInvitations.id,
|
|
@@ -6732,7 +6820,8 @@ var init_invitations_repository = __esm({
|
|
|
6732
6820
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
6733
6821
|
*/
|
|
6734
6822
|
async updateById(id13, data) {
|
|
6735
|
-
const
|
|
6823
|
+
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();
|
|
6736
6825
|
return result[0] ?? null;
|
|
6737
6826
|
}
|
|
6738
6827
|
/**
|
|
@@ -7879,6 +7968,9 @@ import { defineRouter as defineRouter6 } from "@spfn/core/route";
|
|
|
7879
7968
|
// src/server/routes/auth/index.ts
|
|
7880
7969
|
init_schema3();
|
|
7881
7970
|
|
|
7971
|
+
// src/server/helpers/index.ts
|
|
7972
|
+
init_email();
|
|
7973
|
+
|
|
7882
7974
|
// src/server/helpers/password.ts
|
|
7883
7975
|
import * as bcrypt from "@node-rs/bcrypt";
|
|
7884
7976
|
import { env } from "@spfn/auth/config";
|
|
@@ -8070,6 +8162,7 @@ import {
|
|
|
8070
8162
|
} from "@spfn/auth/errors";
|
|
8071
8163
|
|
|
8072
8164
|
// src/server/lib/config.ts
|
|
8165
|
+
init_email();
|
|
8073
8166
|
import { env as env4 } from "@spfn/auth/config";
|
|
8074
8167
|
function getCookieSuffix() {
|
|
8075
8168
|
const port = process.env.SPFN_PORT;
|
|
@@ -8135,7 +8228,7 @@ function getAuthConfig() {
|
|
|
8135
8228
|
async function runBeforeRegister(context) {
|
|
8136
8229
|
const { beforeRegister } = globalConfig;
|
|
8137
8230
|
if (beforeRegister) {
|
|
8138
|
-
await beforeRegister(context);
|
|
8231
|
+
await beforeRegister({ ...context, email: normalizeOptionalEmail(context.email) });
|
|
8139
8232
|
}
|
|
8140
8233
|
}
|
|
8141
8234
|
function getSessionTtl(override) {
|
|
@@ -8178,11 +8271,15 @@ var authLogger = {
|
|
|
8178
8271
|
};
|
|
8179
8272
|
|
|
8180
8273
|
// src/server/services/verification.service.ts
|
|
8274
|
+
init_email();
|
|
8181
8275
|
init_repositories();
|
|
8182
8276
|
var ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES = 60;
|
|
8183
8277
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
8184
8278
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
8185
8279
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
8280
|
+
function normalizeVerificationTarget(target, targetType) {
|
|
8281
|
+
return targetType === "email" ? normalizeEmail(target) : target.trim();
|
|
8282
|
+
}
|
|
8186
8283
|
function generateVerificationCode() {
|
|
8187
8284
|
return crypto4.randomInt(0, 1e6).toString().padStart(6, "0");
|
|
8188
8285
|
}
|
|
@@ -8291,7 +8388,8 @@ async function sendAccountExistsNotice(target, targetType) {
|
|
|
8291
8388
|
}
|
|
8292
8389
|
}
|
|
8293
8390
|
async function sendVerificationCodeService(params) {
|
|
8294
|
-
const {
|
|
8391
|
+
const { targetType, purpose } = params;
|
|
8392
|
+
const target = normalizeVerificationTarget(params.target, targetType);
|
|
8295
8393
|
if (purpose === "registration" && await accountExistsForTarget(target, targetType)) {
|
|
8296
8394
|
const recentNotice = await verificationCodesRepository.findValidByTargetAndPurpose(target, purpose);
|
|
8297
8395
|
if (!recentNotice) {
|
|
@@ -8325,7 +8423,8 @@ async function sendVerificationCodeService(params) {
|
|
|
8325
8423
|
};
|
|
8326
8424
|
}
|
|
8327
8425
|
async function verifyCodeService(params) {
|
|
8328
|
-
const {
|
|
8426
|
+
const { targetType, code, purpose } = params;
|
|
8427
|
+
const target = normalizeVerificationTarget(params.target, targetType);
|
|
8329
8428
|
const validation = await validateVerificationCode(target, code, purpose);
|
|
8330
8429
|
if (!validation.valid) {
|
|
8331
8430
|
throw new InvalidVerificationCodeError({ message: validation.error || "Invalid verification code" });
|
|
@@ -8923,7 +9022,8 @@ async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
|
|
|
8923
9022
|
|
|
8924
9023
|
// src/server/services/auth.service.ts
|
|
8925
9024
|
async function registerService(params) {
|
|
8926
|
-
const { email,
|
|
9025
|
+
const { email, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
9026
|
+
const phone = params.phone?.trim();
|
|
8927
9027
|
const tokenPayload = validateVerificationToken(verificationToken);
|
|
8928
9028
|
if (!tokenPayload) {
|
|
8929
9029
|
throw new InvalidVerificationTokenError2();
|
|
@@ -8931,7 +9031,7 @@ async function registerService(params) {
|
|
|
8931
9031
|
if (tokenPayload.purpose !== "registration") {
|
|
8932
9032
|
throw new VerificationTokenPurposeMismatchError2({ expected: "registration", actual: tokenPayload.purpose });
|
|
8933
9033
|
}
|
|
8934
|
-
const providedTarget = email
|
|
9034
|
+
const providedTarget = email ? normalizeEmail(email) : phone;
|
|
8935
9035
|
if (tokenPayload.target !== providedTarget) {
|
|
8936
9036
|
throw new VerificationTokenTargetMismatchError2();
|
|
8937
9037
|
}
|
|
@@ -9210,6 +9310,29 @@ async function syncMappings(allMappings, rolesByName, permsByName) {
|
|
|
9210
9310
|
}
|
|
9211
9311
|
}
|
|
9212
9312
|
|
|
9313
|
+
// src/server/services/email-normalization.service.ts
|
|
9314
|
+
init_repositories();
|
|
9315
|
+
var BACKFILL_KEY = "auth:email_normalization";
|
|
9316
|
+
async function normalizeStoredEmails() {
|
|
9317
|
+
if (await authMetadataRepository.get(BACKFILL_KEY)) {
|
|
9318
|
+
return { normalized: 0, conflicts: [] };
|
|
9319
|
+
}
|
|
9320
|
+
const conflicts = await usersRepository.findEmailConflictGroups();
|
|
9321
|
+
const normalized = await usersRepository.normalizeEmailsExcept(conflicts.flat());
|
|
9322
|
+
if (normalized > 0) {
|
|
9323
|
+
authLogger.service.info(`\u2709\uFE0F Normalized ${normalized} stored email address(es)`);
|
|
9324
|
+
}
|
|
9325
|
+
if (conflicts.length > 0) {
|
|
9326
|
+
authLogger.service.error(
|
|
9327
|
+
`${conflicts.length} email group(s) differ only by capitalization and cannot be normalized automatically. The accounts are untouched and the ones stored in mixed case cannot sign in until this is resolved.`,
|
|
9328
|
+
{ conflictingUserIds: conflicts }
|
|
9329
|
+
);
|
|
9330
|
+
return { normalized, conflicts };
|
|
9331
|
+
}
|
|
9332
|
+
await authMetadataRepository.set(BACKFILL_KEY, "done");
|
|
9333
|
+
return { normalized, conflicts };
|
|
9334
|
+
}
|
|
9335
|
+
|
|
9213
9336
|
// src/server/services/permission.service.ts
|
|
9214
9337
|
init_repositories();
|
|
9215
9338
|
import { ForbiddenError } from "@spfn/core/errors";
|
|
@@ -10932,6 +11055,7 @@ import { Transactional } from "@spfn/core/db";
|
|
|
10932
11055
|
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
10933
11056
|
|
|
10934
11057
|
// src/server/lib/rate-limit-keys.ts
|
|
11058
|
+
init_email();
|
|
10935
11059
|
import { createHash as createHash5 } from "crypto";
|
|
10936
11060
|
import { getClientIp } from "@spfn/core/middleware";
|
|
10937
11061
|
async function readJsonBody(c) {
|
|
@@ -10943,7 +11067,7 @@ async function readJsonBody(c) {
|
|
|
10943
11067
|
}
|
|
10944
11068
|
function accountKey(body) {
|
|
10945
11069
|
if (typeof body.email === "string" && body.email.trim()) {
|
|
10946
|
-
return `email:${body.email
|
|
11070
|
+
return `email:${normalizeEmail(body.email)}`;
|
|
10947
11071
|
}
|
|
10948
11072
|
if (typeof body.phone === "string" && body.phone.trim()) {
|
|
10949
11073
|
return `phone:${body.phone.trim()}`;
|
|
@@ -10955,7 +11079,7 @@ function targetKey(body) {
|
|
|
10955
11079
|
return void 0;
|
|
10956
11080
|
}
|
|
10957
11081
|
const type = typeof body.targetType === "string" ? body.targetType : "target";
|
|
10958
|
-
const value = type === "email" ? body.target
|
|
11082
|
+
const value = type === "email" ? normalizeEmail(body.target) : body.target.trim();
|
|
10959
11083
|
return `${type}:${value}`;
|
|
10960
11084
|
}
|
|
10961
11085
|
function byIpAndAccount(options = {}) {
|
|
@@ -11675,6 +11799,10 @@ function contractViolation(message) {
|
|
|
11675
11799
|
|
|
11676
11800
|
// src/server/client-proof/contract-bundle.ts
|
|
11677
11801
|
import { createHash as createHash7 } from "crypto";
|
|
11802
|
+
import {
|
|
11803
|
+
CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
|
|
11804
|
+
ServerTimeResponseSchema
|
|
11805
|
+
} from "@spfn/core/server";
|
|
11678
11806
|
init_types();
|
|
11679
11807
|
|
|
11680
11808
|
// src/server/client-proof/proof.ts
|
|
@@ -11805,17 +11933,68 @@ function isRequestContentType(value) {
|
|
|
11805
11933
|
return value.split(";")[0].trim().toLowerCase() === CLIENT_PROOF_CONTENT_TYPE;
|
|
11806
11934
|
}
|
|
11807
11935
|
|
|
11936
|
+
// src/server/client-proof/contract-types.ts
|
|
11937
|
+
import {
|
|
11938
|
+
CORE_TIME_OPERATION_ID,
|
|
11939
|
+
CORE_TIME_ROUTE
|
|
11940
|
+
} from "@spfn/core/server";
|
|
11941
|
+
function importCoreTimeContract() {
|
|
11942
|
+
const { method, path, contract } = CORE_TIME_ROUTE;
|
|
11943
|
+
if (method !== "GET" || typeof path !== "string" || contract?.auth !== "none" || contract.requiresSession !== false || typeof contract.since !== "string") {
|
|
11944
|
+
throw new Error("core.time does not match the clientProofV1 synchronization prerequisite");
|
|
11945
|
+
}
|
|
11946
|
+
return {
|
|
11947
|
+
id: CORE_TIME_OPERATION_ID,
|
|
11948
|
+
method,
|
|
11949
|
+
path,
|
|
11950
|
+
authProfile: contract.auth,
|
|
11951
|
+
requiresSession: contract.requiresSession,
|
|
11952
|
+
sourceSince: contract.since
|
|
11953
|
+
};
|
|
11954
|
+
}
|
|
11955
|
+
var IMPORTED_CORE_TIME_CONTRACT = importCoreTimeContract();
|
|
11956
|
+
var CORE_PREREQUISITE_OPERATIONS = [
|
|
11957
|
+
{
|
|
11958
|
+
id: IMPORTED_CORE_TIME_CONTRACT.id,
|
|
11959
|
+
method: IMPORTED_CORE_TIME_CONTRACT.method,
|
|
11960
|
+
path: IMPORTED_CORE_TIME_CONTRACT.path,
|
|
11961
|
+
authProfile: IMPORTED_CORE_TIME_CONTRACT.authProfile,
|
|
11962
|
+
requiresSession: IMPORTED_CORE_TIME_CONTRACT.requiresSession,
|
|
11963
|
+
responseType: "ServerTimeResponse",
|
|
11964
|
+
summary: "Returns the server epoch used to timestamp clientProofV1 proofs.",
|
|
11965
|
+
since: "0.9.0"
|
|
11966
|
+
}
|
|
11967
|
+
];
|
|
11968
|
+
|
|
11808
11969
|
// src/server/client-proof/contract-bundle.ts
|
|
11809
11970
|
init_wire_headers();
|
|
11810
|
-
var CONTRACT_VERSION = "0.
|
|
11811
|
-
var CONTRACT_SUPPORTED_RANGE = ">=0.
|
|
11971
|
+
var CONTRACT_VERSION = "0.9.0";
|
|
11972
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.9.0 <0.10.0";
|
|
11812
11973
|
function required(name, type) {
|
|
11813
11974
|
return { name, type, optional: false };
|
|
11814
11975
|
}
|
|
11815
11976
|
function optional(name, type) {
|
|
11816
11977
|
return { name, type, optional: true };
|
|
11817
11978
|
}
|
|
11979
|
+
function coreTimeResponseDeclaration() {
|
|
11980
|
+
if (ServerTimeResponseSchema.type !== "object" || ServerTimeResponseSchema.additionalProperties !== false) {
|
|
11981
|
+
throw new Error("core.time response must remain a closed object");
|
|
11982
|
+
}
|
|
11983
|
+
const requiredFields = new Set(ServerTimeResponseSchema.required);
|
|
11984
|
+
const fields = Object.entries(ServerTimeResponseSchema.properties).map(([name, schema]) => {
|
|
11985
|
+
if (schema.type !== "integer") {
|
|
11986
|
+
throw new Error(`core.time response field ${name} is outside the mobile type grammar`);
|
|
11987
|
+
}
|
|
11988
|
+
return {
|
|
11989
|
+
name,
|
|
11990
|
+
type: "integer",
|
|
11991
|
+
optional: !requiredFields.has(name)
|
|
11992
|
+
};
|
|
11993
|
+
});
|
|
11994
|
+
return { name: "ServerTimeResponse", fields };
|
|
11995
|
+
}
|
|
11818
11996
|
var CONTRACT_TYPES = [
|
|
11997
|
+
coreTimeResponseDeclaration(),
|
|
11819
11998
|
{
|
|
11820
11999
|
name: "HandshakeRequest",
|
|
11821
12000
|
fields: [
|
|
@@ -13961,7 +14140,7 @@ async function ensureAdminExists() {
|
|
|
13961
14140
|
for (const account of accounts) {
|
|
13962
14141
|
authLogger.setup.info(`Creating ${account.email} admin account(s)...`);
|
|
13963
14142
|
try {
|
|
13964
|
-
const existing = await usersRepository.
|
|
14143
|
+
const existing = await usersRepository.findByEmailInAnyStoredForm(account.email);
|
|
13965
14144
|
if (existing) {
|
|
13966
14145
|
authLogger.setup.info(`\u26A0\uFE0F Account already exists: ${account.email} (skipped)`);
|
|
13967
14146
|
skipped++;
|
|
@@ -14013,6 +14192,14 @@ function createAuthLifecycle(options = {}) {
|
|
|
14013
14192
|
*/
|
|
14014
14193
|
afterInfrastructure: async () => {
|
|
14015
14194
|
await initializeAuth(options);
|
|
14195
|
+
try {
|
|
14196
|
+
await normalizeStoredEmails();
|
|
14197
|
+
} catch (error) {
|
|
14198
|
+
authLogger.service.error(
|
|
14199
|
+
"Stored email normalization did not complete. Addresses stay as they are, so an account stored in mixed case cannot sign in until a later boot succeeds.",
|
|
14200
|
+
{ error }
|
|
14201
|
+
);
|
|
14202
|
+
}
|
|
14016
14203
|
await ensureAdminExists();
|
|
14017
14204
|
initOneTimeTokenManager(options.oneTimeToken);
|
|
14018
14205
|
}
|
|
@@ -14184,6 +14371,9 @@ export {
|
|
|
14184
14371
|
logoutService,
|
|
14185
14372
|
matchOAuthCsrfCookies,
|
|
14186
14373
|
naverProvider,
|
|
14374
|
+
normalizeEmail,
|
|
14375
|
+
normalizeOptionalEmail,
|
|
14376
|
+
normalizeStoredEmails,
|
|
14187
14377
|
oauthCallbackService,
|
|
14188
14378
|
oauthNativeService,
|
|
14189
14379
|
oauthStartService,
|