@spfn/auth 0.3.0-beta.24 → 0.3.0-beta.26
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 +163 -5
- package/dist/client-proof.d.ts +10 -1
- package/dist/client-proof.js +126 -9
- package/dist/client-proof.js.map +1 -1
- package/dist/client.d.ts +50 -1
- package/dist/client.js +25 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +48 -2
- package/dist/errors.js +27 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -16
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-ZJd9anVT.d.ts → machine-principals-CdEgxOB1.d.ts} +1397 -886
- package/dist/nextjs/api.js +194 -49
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.d.ts +17 -0
- package/dist/nextjs/server.js +33 -6
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +256 -96
- package/dist/server.js +1328 -676
- package/dist/server.js.map +1 -1
- package/migrations/20260919023107_even_mikhail_rasputin/migration.sql +20 -0
- package/migrations/20260919023107_even_mikhail_rasputin/snapshot.json +6300 -0
- package/package.json +1 -1
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(
|
|
901
|
-
return CreateType({ [Kind]: "Argument", index:
|
|
900
|
+
function Argument(index21) {
|
|
901
|
+
return CreateType({ [Kind]: "Argument", index: index21 });
|
|
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,
|
|
1140
|
-
return pattern[
|
|
1139
|
+
function IsNonEscaped(pattern, index21, char) {
|
|
1140
|
+
return pattern[index21] === char && pattern.charCodeAt(index21 - 1) !== 92;
|
|
1141
1141
|
}
|
|
1142
|
-
function IsOpenParen(pattern,
|
|
1143
|
-
return IsNonEscaped(pattern,
|
|
1142
|
+
function IsOpenParen(pattern, index21) {
|
|
1143
|
+
return IsNonEscaped(pattern, index21, "(");
|
|
1144
1144
|
}
|
|
1145
|
-
function IsCloseParen(pattern,
|
|
1146
|
-
return IsNonEscaped(pattern,
|
|
1145
|
+
function IsCloseParen(pattern, index21) {
|
|
1146
|
+
return IsNonEscaped(pattern, index21, ")");
|
|
1147
1147
|
}
|
|
1148
|
-
function IsSeparator(pattern,
|
|
1149
|
-
return IsNonEscaped(pattern,
|
|
1148
|
+
function IsSeparator(pattern, index21) {
|
|
1149
|
+
return IsNonEscaped(pattern, index21, "|");
|
|
1150
1150
|
}
|
|
1151
1151
|
function IsGroup(pattern) {
|
|
1152
1152
|
if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1)))
|
|
1153
1153
|
return false;
|
|
1154
1154
|
let count2 = 0;
|
|
1155
|
-
for (let
|
|
1156
|
-
if (IsOpenParen(pattern,
|
|
1155
|
+
for (let index21 = 0; index21 < pattern.length; index21++) {
|
|
1156
|
+
if (IsOpenParen(pattern, index21))
|
|
1157
1157
|
count2 += 1;
|
|
1158
|
-
if (IsCloseParen(pattern,
|
|
1158
|
+
if (IsCloseParen(pattern, index21))
|
|
1159
1159
|
count2 -= 1;
|
|
1160
|
-
if (count2 === 0 &&
|
|
1160
|
+
if (count2 === 0 && index21 !== pattern.length - 1)
|
|
1161
1161
|
return false;
|
|
1162
1162
|
}
|
|
1163
1163
|
return true;
|
|
@@ -1167,19 +1167,19 @@ function InGroup(pattern) {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
function IsPrecedenceOr(pattern) {
|
|
1169
1169
|
let count2 = 0;
|
|
1170
|
-
for (let
|
|
1171
|
-
if (IsOpenParen(pattern,
|
|
1170
|
+
for (let index21 = 0; index21 < pattern.length; index21++) {
|
|
1171
|
+
if (IsOpenParen(pattern, index21))
|
|
1172
1172
|
count2 += 1;
|
|
1173
|
-
if (IsCloseParen(pattern,
|
|
1173
|
+
if (IsCloseParen(pattern, index21))
|
|
1174
1174
|
count2 -= 1;
|
|
1175
|
-
if (IsSeparator(pattern,
|
|
1175
|
+
if (IsSeparator(pattern, index21) && count2 === 0)
|
|
1176
1176
|
return true;
|
|
1177
1177
|
}
|
|
1178
1178
|
return false;
|
|
1179
1179
|
}
|
|
1180
1180
|
function IsPrecedenceAnd(pattern) {
|
|
1181
|
-
for (let
|
|
1182
|
-
if (IsOpenParen(pattern,
|
|
1181
|
+
for (let index21 = 0; index21 < pattern.length; index21++) {
|
|
1182
|
+
if (IsOpenParen(pattern, index21))
|
|
1183
1183
|
return true;
|
|
1184
1184
|
}
|
|
1185
1185
|
return false;
|
|
@@ -1187,16 +1187,16 @@ function IsPrecedenceAnd(pattern) {
|
|
|
1187
1187
|
function Or(pattern) {
|
|
1188
1188
|
let [count2, start] = [0, 0];
|
|
1189
1189
|
const expressions = [];
|
|
1190
|
-
for (let
|
|
1191
|
-
if (IsOpenParen(pattern,
|
|
1190
|
+
for (let index21 = 0; index21 < pattern.length; index21++) {
|
|
1191
|
+
if (IsOpenParen(pattern, index21))
|
|
1192
1192
|
count2 += 1;
|
|
1193
|
-
if (IsCloseParen(pattern,
|
|
1193
|
+
if (IsCloseParen(pattern, index21))
|
|
1194
1194
|
count2 -= 1;
|
|
1195
|
-
if (IsSeparator(pattern,
|
|
1196
|
-
const range2 = pattern.slice(start,
|
|
1195
|
+
if (IsSeparator(pattern, index21) && count2 === 0) {
|
|
1196
|
+
const range2 = pattern.slice(start, index21);
|
|
1197
1197
|
if (range2.length > 0)
|
|
1198
1198
|
expressions.push(TemplateLiteralParse(range2));
|
|
1199
|
-
start =
|
|
1199
|
+
start = index21 + 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,
|
|
1213
|
-
if (!IsOpenParen(value,
|
|
1212
|
+
function Group(value, index21) {
|
|
1213
|
+
if (!IsOpenParen(value, index21))
|
|
1214
1214
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
|
|
1215
1215
|
let count2 = 0;
|
|
1216
|
-
for (let scan =
|
|
1216
|
+
for (let scan = index21; scan < value.length; scan++) {
|
|
1217
1217
|
if (IsOpenParen(value, scan))
|
|
1218
1218
|
count2 += 1;
|
|
1219
1219
|
if (IsCloseParen(value, scan))
|
|
1220
1220
|
count2 -= 1;
|
|
1221
1221
|
if (count2 === 0)
|
|
1222
|
-
return [
|
|
1222
|
+
return [index21, scan];
|
|
1223
1223
|
}
|
|
1224
1224
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
|
|
1225
1225
|
}
|
|
1226
|
-
function Range(pattern2,
|
|
1227
|
-
for (let scan =
|
|
1226
|
+
function Range(pattern2, index21) {
|
|
1227
|
+
for (let scan = index21; scan < pattern2.length; scan++) {
|
|
1228
1228
|
if (IsOpenParen(pattern2, scan))
|
|
1229
|
-
return [
|
|
1229
|
+
return [index21, scan];
|
|
1230
1230
|
}
|
|
1231
|
-
return [
|
|
1231
|
+
return [index21, pattern2.length];
|
|
1232
1232
|
}
|
|
1233
1233
|
const expressions = [];
|
|
1234
|
-
for (let
|
|
1235
|
-
if (IsOpenParen(pattern,
|
|
1236
|
-
const [start, end] = Group(pattern,
|
|
1234
|
+
for (let index21 = 0; index21 < pattern.length; index21++) {
|
|
1235
|
+
if (IsOpenParen(pattern, index21)) {
|
|
1236
|
+
const [start, end] = Group(pattern, index21);
|
|
1237
1237
|
const range = pattern.slice(start, end + 1);
|
|
1238
1238
|
expressions.push(TemplateLiteralParse(range));
|
|
1239
|
-
|
|
1239
|
+
index21 = end;
|
|
1240
1240
|
} else {
|
|
1241
|
-
const [start, end] = Range(pattern,
|
|
1241
|
+
const [start, end] = Range(pattern, index21);
|
|
1242
1242
|
const range = pattern.slice(start, end);
|
|
1243
1243
|
if (range.length > 0)
|
|
1244
1244
|
expressions.push(TemplateLiteralParse(range));
|
|
1245
|
-
|
|
1245
|
+
index21 = 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,
|
|
2580
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index21) => IntoBooleanResult(Visit3(right.parameters[index21], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
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,
|
|
2586
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index21) => IntoBooleanResult(Visit3(right.parameters[index21], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
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,
|
|
2746
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index21) => Visit3(schema, right.items[index21]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
|
|
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;
|
|
@@ -4520,6 +4520,12 @@ var init_schema3 = __esm({
|
|
|
4520
4520
|
}),
|
|
4521
4521
|
Type.Object({
|
|
4522
4522
|
status: Type.Literal("approved"),
|
|
4523
|
+
// The `LoginResult` discriminant, carried because the approved branch is
|
|
4524
|
+
// a `LoginResult` spread whole (#95). Always false here: a device-code
|
|
4525
|
+
// approval is the owner saying yes on a device that is already signed
|
|
4526
|
+
// in, which is itself a second factor, so this channel never steps up
|
|
4527
|
+
// and never carries a challenge.
|
|
4528
|
+
mfaRequired: Type.Boolean(),
|
|
4523
4529
|
userId: Type.String(),
|
|
4524
4530
|
publicId: Type.String(),
|
|
4525
4531
|
email: Type.Optional(Type.String()),
|
|
@@ -4815,7 +4821,7 @@ var init_wire_headers = __esm({
|
|
|
4815
4821
|
});
|
|
4816
4822
|
|
|
4817
4823
|
// src/server/entities/user-public-keys.ts
|
|
4818
|
-
import { text as text4, boolean as boolean3, index as index4 } from "drizzle-orm/pg-core";
|
|
4824
|
+
import { text as text4, boolean as boolean3, bigint, index as index4 } from "drizzle-orm/pg-core";
|
|
4819
4825
|
import { id as id4, foreignKey as foreignKey3, enumText as enumText2, utcTimestamp as utcTimestamp2 } from "@spfn/core/db";
|
|
4820
4826
|
var userPublicKeys;
|
|
4821
4827
|
var init_user_public_keys = __esm({
|
|
@@ -4946,6 +4952,19 @@ var init_user_public_keys = __esm({
|
|
|
4946
4952
|
// false: Key is deactivated (cannot be used for verification)
|
|
4947
4953
|
// Used for: soft key rotation, temporary key suspension
|
|
4948
4954
|
isActive: boolean3("is_active").notNull().default(true),
|
|
4955
|
+
// The second-factor challenge gating this key, when it is pending (#95)
|
|
4956
|
+
// null: the ordinary state — the key is whatever `is_active` says it is
|
|
4957
|
+
// set: the key was registered inactive because the account has a second
|
|
4958
|
+
// factor and this device is new, and it stays that way until the
|
|
4959
|
+
// challenge is verified. `is_active` alone cannot say this, because a
|
|
4960
|
+
// revoked key is inactive too, and the two must not read alike:
|
|
4961
|
+
// `listKeys` hides a pending key in both modes and a global revocation
|
|
4962
|
+
// deletes it outright rather than revoking it
|
|
4963
|
+
// NOT a foreign key, deliberately. `mfa_challenges.key_id` points back at
|
|
4964
|
+
// this row, and a pair of constraints in a cycle leaves neither row
|
|
4965
|
+
// writable first. The link that decides anything is that one; this column
|
|
4966
|
+
// is what makes "is this row pending" a predicate on the keys table
|
|
4967
|
+
pendingMfaChallengeId: bigint("pending_mfa_challenge_id", { mode: "number" }),
|
|
4949
4968
|
// Key creation timestamp
|
|
4950
4969
|
// Automatically set on insertion
|
|
4951
4970
|
createdAt: utcTimestamp2("created_at").notNull().defaultNow(),
|
|
@@ -5339,7 +5358,7 @@ var init_passkeys = __esm({
|
|
|
5339
5358
|
});
|
|
5340
5359
|
|
|
5341
5360
|
// src/server/entities/webauthn-challenges.ts
|
|
5342
|
-
import { bigint, index as index11, text as text11, uniqueIndex as uniqueIndex5 } from "drizzle-orm/pg-core";
|
|
5361
|
+
import { bigint as bigint2, index as index11, text as text11, uniqueIndex as uniqueIndex5 } from "drizzle-orm/pg-core";
|
|
5343
5362
|
import { id as id11, enumText as enumText6, utcTimestamp as utcTimestamp9 } from "@spfn/core/db";
|
|
5344
5363
|
var WEBAUTHN_CHALLENGE_KINDS, webauthnChallenges;
|
|
5345
5364
|
var init_webauthn_challenges = __esm({
|
|
@@ -5364,7 +5383,7 @@ var init_webauthn_challenges = __esm({
|
|
|
5364
5383
|
// Nullable, so `foreignKey()` (which is notNull by construction) does
|
|
5365
5384
|
// not apply; the cascade is still declared, so destroying an account
|
|
5366
5385
|
// takes its in-flight enrollment challenges with it.
|
|
5367
|
-
userId:
|
|
5386
|
+
userId: bigint2("user_id", { mode: "number" }).references(() => users.id, { onDelete: "cascade" }),
|
|
5368
5387
|
// SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS from creation
|
|
5369
5388
|
expiresAt: utcTimestamp9("expires_at").notNull(),
|
|
5370
5389
|
// Set by the verify that spent it. Non-null means the challenge is
|
|
@@ -5384,7 +5403,7 @@ var init_webauthn_challenges = __esm({
|
|
|
5384
5403
|
});
|
|
5385
5404
|
|
|
5386
5405
|
// src/server/entities/mfa-totp.ts
|
|
5387
|
-
import { bigint as
|
|
5406
|
+
import { bigint as bigint3, integer as integer6, text as text12, uniqueIndex as uniqueIndex6 } from "drizzle-orm/pg-core";
|
|
5388
5407
|
import { id as id12, foreignKey as foreignKey8, timestamps as timestamps10, utcTimestamp as utcTimestamp10 } from "@spfn/core/db";
|
|
5389
5408
|
var MFA_CONFIRM_ATTEMPT_LIMIT, mfaTotp;
|
|
5390
5409
|
var init_mfa_totp = __esm({
|
|
@@ -5407,7 +5426,7 @@ var init_mfa_totp = __esm({
|
|
|
5407
5426
|
// the screen cannot be replayed inside its own window. Per account
|
|
5408
5427
|
// rather than per device: the cost is that a second device signing in
|
|
5409
5428
|
// within the same step is refused and retries on the next one.
|
|
5410
|
-
lastUsedStep:
|
|
5429
|
+
lastUsedStep: bigint3("last_used_step", { mode: "number" }),
|
|
5411
5430
|
// Wrong codes against the unconfirmed row. Reset by a fresh `enroll`.
|
|
5412
5431
|
failedConfirmAttempts: integer6("failed_confirm_attempts").notNull().default(0),
|
|
5413
5432
|
...timestamps10()
|
|
@@ -5480,9 +5499,62 @@ var init_mfa_verifications = __esm({
|
|
|
5480
5499
|
}
|
|
5481
5500
|
});
|
|
5482
5501
|
|
|
5502
|
+
// src/server/entities/mfa-challenges.ts
|
|
5503
|
+
import { index as index13, integer as integer8, jsonb, text as text15, uniqueIndex as uniqueIndex7 } from "drizzle-orm/pg-core";
|
|
5504
|
+
import { id as id14, enumText as enumText8, foreignKey as foreignKey11, timestamps as timestamps12, utcTimestamp as utcTimestamp13 } from "@spfn/core/db";
|
|
5505
|
+
var MFA_CHALLENGE_CHANNELS, MFA_CHALLENGE_ATTEMPT_LIMIT, mfaChallenges;
|
|
5506
|
+
var init_mfa_challenges = __esm({
|
|
5507
|
+
"src/server/entities/mfa-challenges.ts"() {
|
|
5508
|
+
"use strict";
|
|
5509
|
+
init_users();
|
|
5510
|
+
init_user_public_keys();
|
|
5511
|
+
init_schema4();
|
|
5512
|
+
MFA_CHALLENGE_CHANNELS = ["password", "oauth", "oauth-native", "password-reset"];
|
|
5513
|
+
MFA_CHALLENGE_ATTEMPT_LIMIT = 5;
|
|
5514
|
+
mfaChallenges = authSchema.table(
|
|
5515
|
+
"mfa_challenges",
|
|
5516
|
+
{
|
|
5517
|
+
id: id14(),
|
|
5518
|
+
userId: foreignKey11("user", () => users.id),
|
|
5519
|
+
// SHA-256 of the 32-byte secret, base64url. The secret leaves the server
|
|
5520
|
+
// in the 202 body, the OAuth callback query and the verify request, and
|
|
5521
|
+
// is nowhere else — not in a log line and not in this column.
|
|
5522
|
+
challengeHash: text15("challenge_hash").notNull(),
|
|
5523
|
+
// The inactive key this challenge would activate. Cascade, so deleting a
|
|
5524
|
+
// pending key — the sweep, five wrong proofs, a global revocation —
|
|
5525
|
+
// takes the challenge with it rather than leaving one pointing at
|
|
5526
|
+
// nothing.
|
|
5527
|
+
keyId: text15("key_id").notNull().references(() => userPublicKeys.keyId, { onDelete: "cascade" }),
|
|
5528
|
+
channel: enumText8("channel", MFA_CHALLENGE_CHANNELS).notNull(),
|
|
5529
|
+
// The account's key generation when this was minted. A challenge from an
|
|
5530
|
+
// earlier generation verifies nothing.
|
|
5531
|
+
keyEpoch: integer8("key_epoch").notNull(),
|
|
5532
|
+
// Wrong proofs so far. MFA_CHALLENGE_ATTEMPT_LIMIT of them expire the
|
|
5533
|
+
// challenge and delete the pending key.
|
|
5534
|
+
attempts: integer8("attempts").notNull().default(0),
|
|
5535
|
+
// SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES from creation.
|
|
5536
|
+
expiresAt: utcTimestamp13("expires_at").notNull(),
|
|
5537
|
+
// Set by the verify that spent it, in a conditional UPDATE, so two
|
|
5538
|
+
// concurrent verifies produce one winner.
|
|
5539
|
+
verifiedAt: utcTimestamp13("verified_at"),
|
|
5540
|
+
// The login announcement the 202 held back. See DeferredLoginEvent.
|
|
5541
|
+
loginEvent: jsonb("login_event").$type(),
|
|
5542
|
+
...timestamps12()
|
|
5543
|
+
},
|
|
5544
|
+
(table) => [
|
|
5545
|
+
// The verify path addresses a row by this alone. Unique so one presented
|
|
5546
|
+
// secret can never match two rows.
|
|
5547
|
+
uniqueIndex7("mfa_challenge_hash_idx").on(table.challengeHash),
|
|
5548
|
+
// The sweep and the live-challenge lookups both scan by expiry.
|
|
5549
|
+
index13("mfa_challenges_expires_at_idx").on(table.expiresAt)
|
|
5550
|
+
]
|
|
5551
|
+
);
|
|
5552
|
+
}
|
|
5553
|
+
});
|
|
5554
|
+
|
|
5483
5555
|
// src/server/entities/device-authorizations.ts
|
|
5484
|
-
import { text as
|
|
5485
|
-
import { id as
|
|
5556
|
+
import { text as text16, uniqueIndex as uniqueIndex8 } from "drizzle-orm/pg-core";
|
|
5557
|
+
import { id as id15, timestamps as timestamps13, enumText as enumText9, utcTimestamp as utcTimestamp14, optionalForeignKey } from "@spfn/core/db";
|
|
5486
5558
|
var DEVICE_AUTH_STATUSES, deviceAuthorizations;
|
|
5487
5559
|
var init_device_authorizations = __esm({
|
|
5488
5560
|
"src/server/entities/device-authorizations.ts"() {
|
|
@@ -5494,58 +5566,58 @@ var init_device_authorizations = __esm({
|
|
|
5494
5566
|
deviceAuthorizations = authSchema.table(
|
|
5495
5567
|
"device_authorizations",
|
|
5496
5568
|
{
|
|
5497
|
-
id:
|
|
5569
|
+
id: id15(),
|
|
5498
5570
|
// SHA-256 of the device code, hex
|
|
5499
5571
|
// The code itself (32 random bytes, base64url) is returned to the waiting
|
|
5500
5572
|
// device once and never stored: it is the only thing that device holds,
|
|
5501
5573
|
// so a dump of this table must not let its reader finish someone's login
|
|
5502
|
-
deviceCodeHash:
|
|
5574
|
+
deviceCodeHash: text16("device_code_hash").notNull().unique(),
|
|
5503
5575
|
// The code a person reads off the waiting device's screen
|
|
5504
5576
|
// Stored normalized — uppercase, no dash — because that is the only form
|
|
5505
5577
|
// a lookup can match; the dash is put back for display
|
|
5506
5578
|
// Plaintext on purpose: it authorizes nothing without an approver who is
|
|
5507
5579
|
// already authenticated, and it has to be found by exact match
|
|
5508
|
-
userCode:
|
|
5580
|
+
userCode: text16("user_code").notNull(),
|
|
5509
5581
|
// Key material the waiting device generated, same shapes as user_public_keys
|
|
5510
5582
|
// Parked, not registered: it can sign nothing until a poll moves it over
|
|
5511
|
-
publicKey:
|
|
5512
|
-
keyId:
|
|
5513
|
-
fingerprint:
|
|
5514
|
-
algorithm:
|
|
5583
|
+
publicKey: text16("public_key").notNull(),
|
|
5584
|
+
keyId: text16("key_id").notNull(),
|
|
5585
|
+
fingerprint: text16("fingerprint").notNull(),
|
|
5586
|
+
algorithm: enumText9("algorithm", KEY_ALGORITHM).notNull().default("ES256"),
|
|
5515
5587
|
// Device labels the waiting device supplied
|
|
5516
5588
|
// null: it sent none
|
|
5517
5589
|
// Shown to the approver so they can tell whether the device asking is the
|
|
5518
5590
|
// one in front of them — display only, so a client that lies gains nothing
|
|
5519
5591
|
// but a wrong line on the approval screen
|
|
5520
|
-
deviceName:
|
|
5521
|
-
platform:
|
|
5522
|
-
status:
|
|
5592
|
+
deviceName: text16("device_name"),
|
|
5593
|
+
platform: enumText9("platform", KEY_PLATFORM),
|
|
5594
|
+
status: enumText9("status", DEVICE_AUTH_STATUSES).notNull().default("pending"),
|
|
5523
5595
|
// Who approved, written at approve time from the approver's session
|
|
5524
5596
|
// null while pending, and on every record that was denied or expired
|
|
5525
5597
|
// Never taken from a request body — that would be the whole authorization
|
|
5526
5598
|
userId: optionalForeignKey("user", () => users.id, { onDelete: "cascade" }),
|
|
5527
5599
|
// Expiry — 10 minutes from creation by default
|
|
5528
5600
|
// Judged on read; no job clears the row
|
|
5529
|
-
expiresAt:
|
|
5601
|
+
expiresAt: utcTimestamp14("expires_at").notNull(),
|
|
5530
5602
|
// Set when the approver said yes
|
|
5531
|
-
approvedAt:
|
|
5603
|
+
approvedAt: utcTimestamp14("approved_at"),
|
|
5532
5604
|
// Set when the poll that registered the key won the race
|
|
5533
5605
|
// Non-null means the record is spent, whatever else it says
|
|
5534
|
-
consumedAt:
|
|
5535
|
-
...
|
|
5606
|
+
consumedAt: utcTimestamp14("consumed_at"),
|
|
5607
|
+
...timestamps13()
|
|
5536
5608
|
},
|
|
5537
5609
|
(table) => [
|
|
5538
5610
|
// Lookup path for info/approve/deny
|
|
5539
5611
|
// Unique so a typed code can never address two records
|
|
5540
|
-
|
|
5612
|
+
uniqueIndex8("device_authorization_user_code_idx").on(table.userCode)
|
|
5541
5613
|
]
|
|
5542
5614
|
);
|
|
5543
5615
|
}
|
|
5544
5616
|
});
|
|
5545
5617
|
|
|
5546
5618
|
// src/server/entities/user-invitations.ts
|
|
5547
|
-
import { text as
|
|
5548
|
-
import { id as
|
|
5619
|
+
import { text as text17, index as index14 } from "drizzle-orm/pg-core";
|
|
5620
|
+
import { id as id16, timestamps as timestamps14, enumText as enumText10, utcTimestamp as utcTimestamp15, typedJsonb as typedJsonb2, foreignKey as foreignKey12 } from "@spfn/core/db";
|
|
5549
5621
|
var userInvitations;
|
|
5550
5622
|
var init_user_invitations = __esm({
|
|
5551
5623
|
"src/server/entities/user-invitations.ts"() {
|
|
@@ -5558,39 +5630,39 @@ var init_user_invitations = __esm({
|
|
|
5558
5630
|
"user_invitations",
|
|
5559
5631
|
{
|
|
5560
5632
|
// Primary key
|
|
5561
|
-
id:
|
|
5633
|
+
id: id16(),
|
|
5562
5634
|
// Target email address for the invitation
|
|
5563
5635
|
// Will become the user's email upon acceptance
|
|
5564
|
-
email:
|
|
5636
|
+
email: text17("email").notNull(),
|
|
5565
5637
|
// Unique invitation token (UUID v4)
|
|
5566
5638
|
// Used in invitation URL: /auth/invite/{token}
|
|
5567
5639
|
// Single-use token that expires after acceptance
|
|
5568
|
-
token:
|
|
5640
|
+
token: text17("token").notNull().unique(),
|
|
5569
5641
|
// Role to be assigned when invitation is accepted
|
|
5570
5642
|
// Foreign key to roles table
|
|
5571
|
-
roleId:
|
|
5643
|
+
roleId: foreignKey12("role", () => roles.id),
|
|
5572
5644
|
// User who created this invitation
|
|
5573
5645
|
// Foreign key to users table
|
|
5574
5646
|
// Used for: audit trail, permission checks
|
|
5575
|
-
invitedBy:
|
|
5647
|
+
invitedBy: foreignKey12("invited_by", () => users.id, { onDelete: "cascade" }),
|
|
5576
5648
|
// Invitation status
|
|
5577
5649
|
// - pending: Invitation sent, awaiting acceptance
|
|
5578
5650
|
// - accepted: User accepted and account created
|
|
5579
5651
|
// - expired: Invitation expired (automatic)
|
|
5580
5652
|
// - cancelled: Invitation cancelled by admin
|
|
5581
|
-
status:
|
|
5653
|
+
status: enumText10("status", INVITATION_STATUSES).default("pending").notNull(),
|
|
5582
5654
|
// Expiration timestamp (default: 7 days from creation)
|
|
5583
5655
|
// Invitation cannot be accepted after this time
|
|
5584
5656
|
// Background job should update status to 'expired'
|
|
5585
|
-
expiresAt:
|
|
5657
|
+
expiresAt: utcTimestamp15("expires_at").notNull(),
|
|
5586
5658
|
// Timestamp when invitation was accepted
|
|
5587
5659
|
// null = not yet accepted
|
|
5588
5660
|
// Used for: audit trail, analytics
|
|
5589
|
-
acceptedAt:
|
|
5661
|
+
acceptedAt: utcTimestamp15("accepted_at"),
|
|
5590
5662
|
// Timestamp when invitation was cancelled
|
|
5591
5663
|
// null = not cancelled
|
|
5592
5664
|
// Used for: audit trail
|
|
5593
|
-
cancelledAt:
|
|
5665
|
+
cancelledAt: utcTimestamp15("cancelled_at"),
|
|
5594
5666
|
// Additional metadata (JSONB)
|
|
5595
5667
|
// Use cases:
|
|
5596
5668
|
// - Custom welcome message
|
|
@@ -5599,26 +5671,26 @@ var init_user_invitations = __esm({
|
|
|
5599
5671
|
// - Custom fields for app-specific data
|
|
5600
5672
|
// Example: { message: "Welcome!", department: "Engineering" }
|
|
5601
5673
|
metadata: typedJsonb2("metadata"),
|
|
5602
|
-
...
|
|
5674
|
+
...timestamps14()
|
|
5603
5675
|
},
|
|
5604
5676
|
(table) => [
|
|
5605
5677
|
// Indexes for query optimization
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5678
|
+
index14("invitations_token_idx").on(table.token),
|
|
5679
|
+
index14("invitations_email_idx").on(table.email),
|
|
5680
|
+
index14("invitations_status_idx").on(table.status),
|
|
5681
|
+
index14("invitations_invited_by_idx").on(table.invitedBy),
|
|
5682
|
+
index14("invitations_expires_at_idx").on(table.expiresAt),
|
|
5611
5683
|
// For cleanup jobs
|
|
5612
|
-
|
|
5684
|
+
index14("invitations_role_id_idx").on(table.roleId)
|
|
5613
5685
|
]
|
|
5614
5686
|
);
|
|
5615
5687
|
}
|
|
5616
5688
|
});
|
|
5617
5689
|
|
|
5618
5690
|
// src/server/entities/account-deletion-requests.ts
|
|
5619
|
-
import { text as
|
|
5691
|
+
import { text as text18, index as index15, uniqueIndex as uniqueIndex9 } from "drizzle-orm/pg-core";
|
|
5620
5692
|
import { sql as sql2 } from "drizzle-orm";
|
|
5621
|
-
import { id as
|
|
5693
|
+
import { id as id17, timestamps as timestamps15, enumText as enumText11, utcTimestamp as utcTimestamp16, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
|
|
5622
5694
|
var accountDeletionRequests;
|
|
5623
5695
|
var init_account_deletion_requests = __esm({
|
|
5624
5696
|
"src/server/entities/account-deletion-requests.ts"() {
|
|
@@ -5629,40 +5701,40 @@ var init_account_deletion_requests = __esm({
|
|
|
5629
5701
|
accountDeletionRequests = authSchema.table(
|
|
5630
5702
|
"account_deletion_requests",
|
|
5631
5703
|
{
|
|
5632
|
-
id:
|
|
5704
|
+
id: id17(),
|
|
5633
5705
|
// Foreign key to users table. `set null` (optionalForeignKey default) so this
|
|
5634
5706
|
// row survives a hard-delete purge of the user it refers to.
|
|
5635
5707
|
userId: optionalForeignKey2("user", () => users.id),
|
|
5636
5708
|
// Snapshot of the user's public UUID at request time — stays readable even
|
|
5637
5709
|
// after userId is nulled out or the account is anonymized.
|
|
5638
|
-
userPublicId:
|
|
5710
|
+
userPublicId: text18("user_public_id").notNull(),
|
|
5639
5711
|
// When the deletion was requested
|
|
5640
|
-
requestedAt:
|
|
5712
|
+
requestedAt: utcTimestamp16("requested_at").notNull().defaultNow(),
|
|
5641
5713
|
// When the purge job is allowed to run (requestedAt + grace period; equals
|
|
5642
5714
|
// requestedAt itself for immediate/zero-grace deletions)
|
|
5643
|
-
purgeScheduledAt:
|
|
5715
|
+
purgeScheduledAt: utcTimestamp16("purge_scheduled_at").notNull(),
|
|
5644
5716
|
// Request lifecycle status
|
|
5645
5717
|
// - pending: awaiting purgeScheduledAt (or immediate purge)
|
|
5646
5718
|
// - cancelled: recovered before purge
|
|
5647
5719
|
// - completed: purge ran
|
|
5648
|
-
status:
|
|
5720
|
+
status: enumText11("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
|
|
5649
5721
|
// Who initiated the request
|
|
5650
|
-
requestedBy:
|
|
5722
|
+
requestedBy: enumText11("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
|
|
5651
5723
|
// Optional free-text reason (self-service UI, admin note, DSR reference, ...)
|
|
5652
|
-
reason:
|
|
5653
|
-
cancelledAt:
|
|
5654
|
-
completedAt:
|
|
5724
|
+
reason: text18("reason"),
|
|
5725
|
+
cancelledAt: utcTimestamp16("cancelled_at"),
|
|
5726
|
+
completedAt: utcTimestamp16("completed_at"),
|
|
5655
5727
|
// Purge strategy actually executed (set on completion; null while pending)
|
|
5656
|
-
purgeStrategy:
|
|
5657
|
-
...
|
|
5728
|
+
purgeStrategy: enumText11("purge_strategy", PURGE_STRATEGIES),
|
|
5729
|
+
...timestamps15()
|
|
5658
5730
|
},
|
|
5659
5731
|
(table) => [
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5732
|
+
index15("account_deletion_requests_user_id_idx").on(table.userId),
|
|
5733
|
+
index15("account_deletion_requests_status_idx").on(table.status),
|
|
5734
|
+
index15("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
|
|
5735
|
+
index15("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
|
|
5664
5736
|
// Partial unique index: at most one pending request per user at a time.
|
|
5665
|
-
|
|
5737
|
+
uniqueIndex9("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
|
|
5666
5738
|
]
|
|
5667
5739
|
);
|
|
5668
5740
|
}
|
|
@@ -5814,8 +5886,8 @@ var init_rbac = __esm({
|
|
|
5814
5886
|
});
|
|
5815
5887
|
|
|
5816
5888
|
// src/server/entities/permissions.ts
|
|
5817
|
-
import { text as
|
|
5818
|
-
import { id as
|
|
5889
|
+
import { text as text19, boolean as boolean5, index as index16 } from "drizzle-orm/pg-core";
|
|
5890
|
+
import { id as id18, timestamps as timestamps16, enumText as enumText12, typedJsonb as typedJsonb3 } from "@spfn/core/db";
|
|
5819
5891
|
var permissions;
|
|
5820
5892
|
var init_permissions = __esm({
|
|
5821
5893
|
"src/server/entities/permissions.ts"() {
|
|
@@ -5826,7 +5898,7 @@ var init_permissions = __esm({
|
|
|
5826
5898
|
"permissions",
|
|
5827
5899
|
{
|
|
5828
5900
|
// Primary key
|
|
5829
|
-
id:
|
|
5901
|
+
id: id18(),
|
|
5830
5902
|
// Permission identifier
|
|
5831
5903
|
// Format: resource:action or namespace:resource:action
|
|
5832
5904
|
// Examples:
|
|
@@ -5834,20 +5906,20 @@ var init_permissions = __esm({
|
|
|
5834
5906
|
// - Namespaced: 'auth:user:delete', 'cms:post:publish'
|
|
5835
5907
|
// Must be unique across all permissions
|
|
5836
5908
|
// Used in: permission checks, role assignments, API guards
|
|
5837
|
-
name:
|
|
5909
|
+
name: text19("name").notNull().unique(),
|
|
5838
5910
|
// Display name for UI
|
|
5839
5911
|
// Human-readable name shown in admin panels
|
|
5840
5912
|
// Example: "Delete Users", "Publish Posts"
|
|
5841
|
-
displayName:
|
|
5913
|
+
displayName: text19("display_name").notNull(),
|
|
5842
5914
|
// Permission description
|
|
5843
5915
|
// Detailed explanation of what this permission allows
|
|
5844
5916
|
// Example: "Allows deletion of user accounts from the system"
|
|
5845
|
-
description:
|
|
5917
|
+
description: text19("description"),
|
|
5846
5918
|
// Category for grouping
|
|
5847
5919
|
// Used for: organizing permissions in UI, filtering
|
|
5848
5920
|
// Built-in categories: auth, user, rbac, system
|
|
5849
5921
|
// Custom categories: any app-specific category
|
|
5850
|
-
category:
|
|
5922
|
+
category: enumText12("category", PERMISSION_CATEGORIES),
|
|
5851
5923
|
// Built-in permission flag
|
|
5852
5924
|
// true: Core package permissions (auth:*, user:*, rbac:*)
|
|
5853
5925
|
// - Cannot be deleted or modified
|
|
@@ -5878,22 +5950,22 @@ var init_permissions = __esm({
|
|
|
5878
5950
|
// - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
|
|
5879
5951
|
// Example: { icon: 'trash', color: 'red', requiresMfa: true }
|
|
5880
5952
|
metadata: typedJsonb3("metadata"),
|
|
5881
|
-
...
|
|
5953
|
+
...timestamps16()
|
|
5882
5954
|
},
|
|
5883
5955
|
(table) => [
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5956
|
+
index16("permissions_name_idx").on(table.name),
|
|
5957
|
+
index16("permissions_category_idx").on(table.category),
|
|
5958
|
+
index16("permissions_is_system_idx").on(table.isSystem),
|
|
5959
|
+
index16("permissions_is_active_idx").on(table.isActive),
|
|
5960
|
+
index16("permissions_is_builtin_idx").on(table.isBuiltin)
|
|
5889
5961
|
]
|
|
5890
5962
|
);
|
|
5891
5963
|
}
|
|
5892
5964
|
});
|
|
5893
5965
|
|
|
5894
5966
|
// src/server/entities/role-permissions.ts
|
|
5895
|
-
import { index as
|
|
5896
|
-
import { id as
|
|
5967
|
+
import { index as index17, unique } from "drizzle-orm/pg-core";
|
|
5968
|
+
import { id as id19, timestamps as timestamps17, foreignKey as foreignKey13 } from "@spfn/core/db";
|
|
5897
5969
|
var rolePermissions;
|
|
5898
5970
|
var init_role_permissions = __esm({
|
|
5899
5971
|
"src/server/entities/role-permissions.ts"() {
|
|
@@ -5905,25 +5977,25 @@ var init_role_permissions = __esm({
|
|
|
5905
5977
|
"role_permissions",
|
|
5906
5978
|
{
|
|
5907
5979
|
// Primary key
|
|
5908
|
-
id:
|
|
5980
|
+
id: id19(),
|
|
5909
5981
|
// Role reference
|
|
5910
5982
|
// Foreign key to roles table
|
|
5911
5983
|
// Cascade delete: when role is deleted, all role-permission mappings are removed
|
|
5912
5984
|
// Used for: defining which permissions each role has
|
|
5913
5985
|
// Example: Admin role → [user:create, user:delete, user:update]
|
|
5914
|
-
roleId:
|
|
5986
|
+
roleId: foreignKey13("role", () => roles.id, { onDelete: "cascade" }),
|
|
5915
5987
|
// Permission reference
|
|
5916
5988
|
// Foreign key to permissions table
|
|
5917
5989
|
// Cascade delete: when permission is deleted, all role-permission mappings are removed
|
|
5918
5990
|
// Used for: granting permissions to roles
|
|
5919
5991
|
// Example: user:delete permission → [Admin, Superadmin]
|
|
5920
|
-
permissionId:
|
|
5921
|
-
...
|
|
5992
|
+
permissionId: foreignKey13("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
5993
|
+
...timestamps17()
|
|
5922
5994
|
},
|
|
5923
5995
|
(table) => [
|
|
5924
5996
|
// Indexes for query performance
|
|
5925
|
-
|
|
5926
|
-
|
|
5997
|
+
index17("role_permissions_role_id_idx").on(table.roleId),
|
|
5998
|
+
index17("role_permissions_permission_id_idx").on(table.permissionId),
|
|
5927
5999
|
// Unique constraint: one role-permission pair only
|
|
5928
6000
|
unique("role_permissions_unique").on(table.roleId, table.permissionId)
|
|
5929
6001
|
]
|
|
@@ -5932,8 +6004,8 @@ var init_role_permissions = __esm({
|
|
|
5932
6004
|
});
|
|
5933
6005
|
|
|
5934
6006
|
// src/server/entities/user-permissions.ts
|
|
5935
|
-
import { boolean as boolean6, text as
|
|
5936
|
-
import { id as
|
|
6007
|
+
import { boolean as boolean6, text as text20, index as index18, unique as unique2 } from "drizzle-orm/pg-core";
|
|
6008
|
+
import { id as id20, timestamps as timestamps18, utcTimestamp as utcTimestamp17, foreignKey as foreignKey14 } from "@spfn/core/db";
|
|
5937
6009
|
var userPermissions;
|
|
5938
6010
|
var init_user_permissions = __esm({
|
|
5939
6011
|
"src/server/entities/user-permissions.ts"() {
|
|
@@ -5945,15 +6017,15 @@ var init_user_permissions = __esm({
|
|
|
5945
6017
|
"user_permissions",
|
|
5946
6018
|
{
|
|
5947
6019
|
// Primary key
|
|
5948
|
-
id:
|
|
6020
|
+
id: id20(),
|
|
5949
6021
|
// User reference
|
|
5950
6022
|
// Foreign key to users table
|
|
5951
6023
|
// Cascade delete: when user is deleted, all overrides are removed
|
|
5952
|
-
userId:
|
|
6024
|
+
userId: foreignKey14("user", () => users.id, { onDelete: "cascade" }),
|
|
5953
6025
|
// Permission reference
|
|
5954
6026
|
// Foreign key to permissions table
|
|
5955
6027
|
// Cascade delete: when permission is deleted, all overrides are removed
|
|
5956
|
-
permissionId:
|
|
6028
|
+
permissionId: foreignKey14("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
5957
6029
|
// Grant or revoke flag
|
|
5958
6030
|
// true: GRANT this permission to the user (additive override)
|
|
5959
6031
|
// - Grants permission even if role doesn't have it
|
|
@@ -5968,19 +6040,19 @@ var init_user_permissions = __esm({
|
|
|
5968
6040
|
// Reason for grant/revocation
|
|
5969
6041
|
// Used for: audit trail, compliance documentation
|
|
5970
6042
|
// Example: "Temporary access for project X", "Security incident - restricted"
|
|
5971
|
-
reason:
|
|
6043
|
+
reason: text20("reason"),
|
|
5972
6044
|
// Expiration timestamp (optional)
|
|
5973
6045
|
// null: Permanent override (remains until manually removed)
|
|
5974
6046
|
// timestamp: Permission expires at this time (auto-revoked by background job)
|
|
5975
6047
|
// Use case: Time-limited elevated access, temporary restrictions
|
|
5976
|
-
expiresAt:
|
|
5977
|
-
...
|
|
6048
|
+
expiresAt: utcTimestamp17("expires_at"),
|
|
6049
|
+
...timestamps18()
|
|
5978
6050
|
},
|
|
5979
6051
|
(table) => [
|
|
5980
6052
|
// Indexes for query performance
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6053
|
+
index18("user_permissions_user_id_idx").on(table.userId),
|
|
6054
|
+
index18("user_permissions_permission_id_idx").on(table.permissionId),
|
|
6055
|
+
index18("user_permissions_expires_at_idx").on(table.expiresAt),
|
|
5984
6056
|
// Unique constraint: one user-permission pair only
|
|
5985
6057
|
unique2("user_permissions_unique").on(table.userId, table.permissionId)
|
|
5986
6058
|
]
|
|
@@ -5989,8 +6061,8 @@ var init_user_permissions = __esm({
|
|
|
5989
6061
|
});
|
|
5990
6062
|
|
|
5991
6063
|
// src/server/entities/oauth2-clients.ts
|
|
5992
|
-
import { text as
|
|
5993
|
-
import { id as
|
|
6064
|
+
import { text as text21 } from "drizzle-orm/pg-core";
|
|
6065
|
+
import { id as id21, timestamps as timestamps19, utcTimestamp as utcTimestamp18 } from "@spfn/core/db";
|
|
5994
6066
|
var oauth2Clients;
|
|
5995
6067
|
var init_oauth2_clients = __esm({
|
|
5996
6068
|
"src/server/entities/oauth2-clients.ts"() {
|
|
@@ -5999,35 +6071,35 @@ var init_oauth2_clients = __esm({
|
|
|
5999
6071
|
oauth2Clients = authSchema.table(
|
|
6000
6072
|
"oauth2_clients",
|
|
6001
6073
|
{
|
|
6002
|
-
id:
|
|
6074
|
+
id: id21(),
|
|
6003
6075
|
// The `client_id` the client sends on every later request. Random, opaque,
|
|
6004
6076
|
// and the unique constraint doubles as the lookup index.
|
|
6005
|
-
clientId:
|
|
6077
|
+
clientId: text21("client_id").notNull().unique(),
|
|
6006
6078
|
// Self-declared label, shown on the consent screen and nowhere else.
|
|
6007
6079
|
// A client that lies about it gains one wrong line on that screen.
|
|
6008
|
-
clientName:
|
|
6080
|
+
clientName: text21("client_name").notNull(),
|
|
6009
6081
|
// Registered redirect URIs, stored exactly as the client wrote them.
|
|
6010
6082
|
// The registration answer echoes them back verbatim, so normalising here
|
|
6011
6083
|
// would answer a client with a URI it did not register. Matching
|
|
6012
6084
|
// normalises both sides instead — see lib/oauth2/redirect-uri.ts.
|
|
6013
|
-
redirectUris:
|
|
6085
|
+
redirectUris: text21("redirect_uris").array().notNull(),
|
|
6014
6086
|
// Client IP the registration arrived from, so the per-IP cap on clients
|
|
6015
6087
|
// nobody has approved yet can be counted. Nullable: an IP is not always
|
|
6016
6088
|
// knowable behind a proxy that forwards none, and a registration is not
|
|
6017
6089
|
// worth refusing over that.
|
|
6018
|
-
createdIp:
|
|
6090
|
+
createdIp: text21("created_ip"),
|
|
6019
6091
|
// Last token issuance or refresh under this client, updated
|
|
6020
6092
|
// fire-and-forget. Operator-facing only; nothing is authorized by it.
|
|
6021
|
-
lastUsedAt:
|
|
6022
|
-
...
|
|
6093
|
+
lastUsedAt: utcTimestamp18("last_used_at"),
|
|
6094
|
+
...timestamps19()
|
|
6023
6095
|
}
|
|
6024
6096
|
);
|
|
6025
6097
|
}
|
|
6026
6098
|
});
|
|
6027
6099
|
|
|
6028
6100
|
// src/server/entities/oauth2-grants.ts
|
|
6029
|
-
import { uniqueIndex as
|
|
6030
|
-
import { id as
|
|
6101
|
+
import { uniqueIndex as uniqueIndex10, index as index19, text as text22 } from "drizzle-orm/pg-core";
|
|
6102
|
+
import { id as id22, timestamps as timestamps20, utcTimestamp as utcTimestamp19, foreignKey as foreignKey15 } from "@spfn/core/db";
|
|
6031
6103
|
var oauth2Grants;
|
|
6032
6104
|
var init_oauth2_grants = __esm({
|
|
6033
6105
|
"src/server/entities/oauth2-grants.ts"() {
|
|
@@ -6038,39 +6110,39 @@ var init_oauth2_grants = __esm({
|
|
|
6038
6110
|
oauth2Grants = authSchema.table(
|
|
6039
6111
|
"oauth2_grants",
|
|
6040
6112
|
{
|
|
6041
|
-
id:
|
|
6113
|
+
id: id22(),
|
|
6042
6114
|
// `oauth2_client_id` and not `client_id`: the opaque string a client
|
|
6043
6115
|
// sends is called `client_id` everywhere in the protocol, and a bigint
|
|
6044
6116
|
// foreign key under that name in this schema would read as that value.
|
|
6045
|
-
client:
|
|
6117
|
+
client: foreignKey15("oauth2_client", () => oauth2Clients.id, { onDelete: "cascade" }),
|
|
6046
6118
|
// Whose consent this is. Read from the approving session at authorize
|
|
6047
6119
|
// time, never from a request body — that is the whole authorization.
|
|
6048
|
-
user:
|
|
6120
|
+
user: foreignKey15("user", () => users.id, { onDelete: "cascade" }),
|
|
6049
6121
|
// The RFC 8707 target this consent is for, normalised (see lib/oauth2).
|
|
6050
6122
|
// An access token is only good against the resource its grant names.
|
|
6051
|
-
resource:
|
|
6123
|
+
resource: text22("resource").notNull(),
|
|
6052
6124
|
// Scope names the user approved. A refresh may ask for a subset of these
|
|
6053
6125
|
// and never for more.
|
|
6054
|
-
scopes:
|
|
6126
|
+
scopes: text22("scopes").array().notNull(),
|
|
6055
6127
|
// null = live; a timestamp cuts off every code and token beneath it
|
|
6056
|
-
revokedAt:
|
|
6057
|
-
...
|
|
6128
|
+
revokedAt: utcTimestamp19("revoked_at"),
|
|
6129
|
+
...timestamps20()
|
|
6058
6130
|
},
|
|
6059
6131
|
(table) => [
|
|
6060
6132
|
// One consent per (client, user, resource) — the re-consent path updates
|
|
6061
6133
|
// this row rather than inserting beside it.
|
|
6062
|
-
|
|
6134
|
+
uniqueIndex10("oauth2_grant_client_user_resource_idx").on(table.client, table.user, table.resource),
|
|
6063
6135
|
// The global-revocation path and the user's own grant list both address
|
|
6064
6136
|
// rows by user alone.
|
|
6065
|
-
|
|
6137
|
+
index19("oauth2_grant_user_idx").on(table.user)
|
|
6066
6138
|
]
|
|
6067
6139
|
);
|
|
6068
6140
|
}
|
|
6069
6141
|
});
|
|
6070
6142
|
|
|
6071
6143
|
// src/server/entities/oauth2-authorization-codes.ts
|
|
6072
|
-
import { text as
|
|
6073
|
-
import { id as
|
|
6144
|
+
import { text as text23 } from "drizzle-orm/pg-core";
|
|
6145
|
+
import { id as id23, timestamps as timestamps21, utcTimestamp as utcTimestamp20, foreignKey as foreignKey16 } from "@spfn/core/db";
|
|
6074
6146
|
var oauth2AuthorizationCodes;
|
|
6075
6147
|
var init_oauth2_authorization_codes = __esm({
|
|
6076
6148
|
"src/server/entities/oauth2-authorization-codes.ts"() {
|
|
@@ -6080,33 +6152,33 @@ var init_oauth2_authorization_codes = __esm({
|
|
|
6080
6152
|
oauth2AuthorizationCodes = authSchema.table(
|
|
6081
6153
|
"oauth2_authorization_codes",
|
|
6082
6154
|
{
|
|
6083
|
-
id:
|
|
6155
|
+
id: id23(),
|
|
6084
6156
|
// SHA-256 hex of the code. The code itself (32 random bytes, base64url)
|
|
6085
6157
|
// is in the redirect and nowhere else.
|
|
6086
|
-
codeHash:
|
|
6087
|
-
grant:
|
|
6158
|
+
codeHash: text23("code_hash").notNull().unique(),
|
|
6159
|
+
grant: foreignKey16("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
6088
6160
|
// The exact redirect_uri string the authorize request carried. The token
|
|
6089
6161
|
// request must repeat it character for character (RFC 6749 §4.1.3).
|
|
6090
|
-
redirectUri:
|
|
6162
|
+
redirectUri: text23("redirect_uri").notNull(),
|
|
6091
6163
|
// The PKCE S256 challenge. `code_verifier` at the token endpoint is
|
|
6092
6164
|
// hashed and compared against this; `plain` is not accepted anywhere,
|
|
6093
6165
|
// so no method column is needed.
|
|
6094
|
-
codeChallenge:
|
|
6166
|
+
codeChallenge: text23("code_challenge").notNull(),
|
|
6095
6167
|
// 60 seconds from issuance. Judged by the database in the statement that
|
|
6096
6168
|
// spends the row, not only in a read before it.
|
|
6097
|
-
expiresAt:
|
|
6169
|
+
expiresAt: utcTimestamp20("expires_at").notNull(),
|
|
6098
6170
|
// null = unspent. Non-null means spent, and a second presentation of the
|
|
6099
6171
|
// same code revokes the grant.
|
|
6100
|
-
usedAt:
|
|
6101
|
-
...
|
|
6172
|
+
usedAt: utcTimestamp20("used_at"),
|
|
6173
|
+
...timestamps21()
|
|
6102
6174
|
}
|
|
6103
6175
|
);
|
|
6104
6176
|
}
|
|
6105
6177
|
});
|
|
6106
6178
|
|
|
6107
6179
|
// src/server/entities/oauth2-tokens.ts
|
|
6108
|
-
import { index as
|
|
6109
|
-
import { id as
|
|
6180
|
+
import { index as index20, text as text24 } from "drizzle-orm/pg-core";
|
|
6181
|
+
import { id as id24, timestamps as timestamps22, enumText as enumText13, utcTimestamp as utcTimestamp21, foreignKey as foreignKey17 } from "@spfn/core/db";
|
|
6110
6182
|
var OAUTH2_TOKEN_KINDS, oauth2Tokens;
|
|
6111
6183
|
var init_oauth2_tokens = __esm({
|
|
6112
6184
|
"src/server/entities/oauth2-tokens.ts"() {
|
|
@@ -6117,30 +6189,30 @@ var init_oauth2_tokens = __esm({
|
|
|
6117
6189
|
oauth2Tokens = authSchema.table(
|
|
6118
6190
|
"oauth2_tokens",
|
|
6119
6191
|
{
|
|
6120
|
-
id:
|
|
6192
|
+
id: id24(),
|
|
6121
6193
|
// SHA-256 hex of the token value. Lookup key; the unique constraint
|
|
6122
6194
|
// doubles as the index.
|
|
6123
|
-
tokenHash:
|
|
6124
|
-
kind:
|
|
6125
|
-
grant:
|
|
6195
|
+
tokenHash: text24("token_hash").notNull().unique(),
|
|
6196
|
+
kind: enumText13("kind", OAUTH2_TOKEN_KINDS).notNull(),
|
|
6197
|
+
grant: foreignKey17("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
6126
6198
|
// What this particular token carries, which may be narrower than its
|
|
6127
6199
|
// grant's scopes: a refresh request is allowed to ask for a subset.
|
|
6128
6200
|
// Never wider — that is `invalid_scope`.
|
|
6129
|
-
scopes:
|
|
6130
|
-
expiresAt:
|
|
6201
|
+
scopes: text24("scopes").array().notNull(),
|
|
6202
|
+
expiresAt: utcTimestamp21("expires_at").notNull(),
|
|
6131
6203
|
// null = live. Set by revoke, by a grant revocation, and by the two
|
|
6132
6204
|
// replay detections (code reuse, refresh reuse).
|
|
6133
|
-
revokedAt:
|
|
6205
|
+
revokedAt: utcTimestamp21("revoked_at"),
|
|
6134
6206
|
// Refresh only: set when a rotation issued the successor. A row with
|
|
6135
6207
|
// this set is spent, and presenting it revokes the grant.
|
|
6136
|
-
replacedAt:
|
|
6208
|
+
replacedAt: utcTimestamp21("replaced_at"),
|
|
6137
6209
|
// Last successful verification, updated fire-and-forget like ops tokens
|
|
6138
|
-
lastUsedAt:
|
|
6139
|
-
...
|
|
6210
|
+
lastUsedAt: utcTimestamp21("last_used_at"),
|
|
6211
|
+
...timestamps22()
|
|
6140
6212
|
},
|
|
6141
6213
|
(table) => [
|
|
6142
6214
|
// Revocation addresses every token under one grant.
|
|
6143
|
-
|
|
6215
|
+
index20("oauth2_token_grant_idx").on(table.grant)
|
|
6144
6216
|
]
|
|
6145
6217
|
);
|
|
6146
6218
|
}
|
|
@@ -6148,7 +6220,7 @@ var init_oauth2_tokens = __esm({
|
|
|
6148
6220
|
|
|
6149
6221
|
// src/server/entities/auth-metadata.ts
|
|
6150
6222
|
import { sql as sql3 } from "drizzle-orm";
|
|
6151
|
-
import { text as
|
|
6223
|
+
import { text as text25, timestamp } from "drizzle-orm/pg-core";
|
|
6152
6224
|
var authMetadata;
|
|
6153
6225
|
var init_auth_metadata = __esm({
|
|
6154
6226
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -6158,9 +6230,9 @@ var init_auth_metadata = __esm({
|
|
|
6158
6230
|
"auth_metadata",
|
|
6159
6231
|
{
|
|
6160
6232
|
// Metadata key (primary key)
|
|
6161
|
-
key:
|
|
6233
|
+
key: text25("key").primaryKey(),
|
|
6162
6234
|
// Metadata value
|
|
6163
|
-
value:
|
|
6235
|
+
value: text25("value").notNull(),
|
|
6164
6236
|
// Last updated timestamp — stamped by the database on insert and on update
|
|
6165
6237
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
|
|
6166
6238
|
}
|
|
@@ -6169,8 +6241,8 @@ var init_auth_metadata = __esm({
|
|
|
6169
6241
|
});
|
|
6170
6242
|
|
|
6171
6243
|
// src/server/entities/ops-tokens.ts
|
|
6172
|
-
import { text as
|
|
6173
|
-
import { id as
|
|
6244
|
+
import { text as text26 } from "drizzle-orm/pg-core";
|
|
6245
|
+
import { id as id25, timestamps as timestamps23, utcTimestamp as utcTimestamp22 } from "@spfn/core/db";
|
|
6174
6246
|
var opsTokens;
|
|
6175
6247
|
var init_ops_tokens = __esm({
|
|
6176
6248
|
"src/server/entities/ops-tokens.ts"() {
|
|
@@ -6179,22 +6251,22 @@ var init_ops_tokens = __esm({
|
|
|
6179
6251
|
opsTokens = authSchema.table(
|
|
6180
6252
|
"ops_tokens",
|
|
6181
6253
|
{
|
|
6182
|
-
id:
|
|
6254
|
+
id: id25(),
|
|
6183
6255
|
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
6184
|
-
name:
|
|
6256
|
+
name: text26("name").notNull(),
|
|
6185
6257
|
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
6186
6258
|
// here, and the unique constraint doubles as the lookup index.
|
|
6187
|
-
tokenHash:
|
|
6259
|
+
tokenHash: text26("token_hash").notNull().unique(),
|
|
6188
6260
|
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
6189
6261
|
// '*' grants every scope.
|
|
6190
|
-
scopes:
|
|
6262
|
+
scopes: text26("scopes").array().notNull(),
|
|
6191
6263
|
// null = the token does not expire
|
|
6192
|
-
expiresAt:
|
|
6264
|
+
expiresAt: utcTimestamp22("expires_at"),
|
|
6193
6265
|
// null = active; a timestamp revokes the token permanently
|
|
6194
|
-
revokedAt:
|
|
6266
|
+
revokedAt: utcTimestamp22("revoked_at"),
|
|
6195
6267
|
// Last successful verification, updated fire-and-forget
|
|
6196
|
-
lastUsedAt:
|
|
6197
|
-
...
|
|
6268
|
+
lastUsedAt: utcTimestamp22("last_used_at"),
|
|
6269
|
+
...timestamps23()
|
|
6198
6270
|
}
|
|
6199
6271
|
);
|
|
6200
6272
|
}
|
|
@@ -6218,6 +6290,7 @@ var init_entities = __esm({
|
|
|
6218
6290
|
init_mfa_totp();
|
|
6219
6291
|
init_mfa_recovery_codes();
|
|
6220
6292
|
init_mfa_verifications();
|
|
6293
|
+
init_mfa_challenges();
|
|
6221
6294
|
init_device_authorizations();
|
|
6222
6295
|
init_user_invitations();
|
|
6223
6296
|
init_account_deletion_requests();
|
|
@@ -6245,12 +6318,26 @@ var init_users_repository = __esm({
|
|
|
6245
6318
|
init_entities();
|
|
6246
6319
|
init_email();
|
|
6247
6320
|
UsersRepository = class extends BaseRepository {
|
|
6321
|
+
/**
|
|
6322
|
+
* The account's current key generation.
|
|
6323
|
+
*
|
|
6324
|
+
* Read from the primary, and this is the one place that matters: the value
|
|
6325
|
+
* is stamped onto a second-factor challenge that must die with the
|
|
6326
|
+
* generation it was minted in, and a replica lagging behind a revoke-all
|
|
6327
|
+
* would stamp the generation that has just ended.
|
|
6328
|
+
*
|
|
6329
|
+
* Write primary 사용
|
|
6330
|
+
*/
|
|
6331
|
+
async currentKeyEpoch(id26) {
|
|
6332
|
+
const result = await this.db.select({ keyEpoch: users.keyEpoch }).from(users).where(eq(users.id, id26)).limit(1);
|
|
6333
|
+
return result[0]?.keyEpoch ?? 0;
|
|
6334
|
+
}
|
|
6248
6335
|
/**
|
|
6249
6336
|
* ID로 사용자 조회
|
|
6250
6337
|
* Read replica 사용
|
|
6251
6338
|
*/
|
|
6252
|
-
async findById(
|
|
6253
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
6339
|
+
async findById(id26) {
|
|
6340
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id26)).limit(1);
|
|
6254
6341
|
return result[0] ?? null;
|
|
6255
6342
|
}
|
|
6256
6343
|
/**
|
|
@@ -6260,8 +6347,8 @@ var init_users_repository = __esm({
|
|
|
6260
6347
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
6261
6348
|
* 계속 사용할 것.
|
|
6262
6349
|
*/
|
|
6263
|
-
async findByIdOnPrimary(
|
|
6264
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
6350
|
+
async findByIdOnPrimary(id26) {
|
|
6351
|
+
const result = await this.db.select().from(users).where(eq(users.id, id26)).limit(1);
|
|
6265
6352
|
return result[0] ?? null;
|
|
6266
6353
|
}
|
|
6267
6354
|
/**
|
|
@@ -6277,8 +6364,8 @@ var init_users_repository = __esm({
|
|
|
6277
6364
|
* Only meaningful inside a transaction — the lock is released at commit.
|
|
6278
6365
|
* Write primary.
|
|
6279
6366
|
*/
|
|
6280
|
-
async lockById(
|
|
6281
|
-
await this.db.select({ id: users.id }).from(users).where(eq(users.id,
|
|
6367
|
+
async lockById(id26) {
|
|
6368
|
+
await this.db.select({ id: users.id }).from(users).where(eq(users.id, id26)).for("update");
|
|
6282
6369
|
}
|
|
6283
6370
|
/**
|
|
6284
6371
|
* 이메일로 사용자 조회
|
|
@@ -6348,13 +6435,13 @@ var init_users_repository = __esm({
|
|
|
6348
6435
|
*
|
|
6349
6436
|
* roleId가 null인 유저는 role: null 반환
|
|
6350
6437
|
*/
|
|
6351
|
-
async findByIdWithRole(
|
|
6438
|
+
async findByIdWithRole(id26) {
|
|
6352
6439
|
const result = await this.readDb.select({
|
|
6353
6440
|
user: users,
|
|
6354
6441
|
roleName: roles.name,
|
|
6355
6442
|
roleDisplayName: roles.displayName,
|
|
6356
6443
|
rolePriority: roles.priority
|
|
6357
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
6444
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id26)).limit(1);
|
|
6358
6445
|
const row = result[0];
|
|
6359
6446
|
if (!row) {
|
|
6360
6447
|
return null;
|
|
@@ -6429,9 +6516,9 @@ var init_users_repository = __esm({
|
|
|
6429
6516
|
* 사용자 정보 업데이트
|
|
6430
6517
|
* Write primary 사용
|
|
6431
6518
|
*/
|
|
6432
|
-
async updateById(
|
|
6519
|
+
async updateById(id26, data) {
|
|
6433
6520
|
const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
|
|
6434
|
-
const result = await this.db.update(users).set(patch).where(eq(users.id,
|
|
6521
|
+
const result = await this.db.update(users).set(patch).where(eq(users.id, id26)).returning();
|
|
6435
6522
|
return result[0] ?? null;
|
|
6436
6523
|
}
|
|
6437
6524
|
/**
|
|
@@ -6443,10 +6530,10 @@ var init_users_repository = __esm({
|
|
|
6443
6530
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
6444
6531
|
* Write primary 사용
|
|
6445
6532
|
*/
|
|
6446
|
-
async reactivateFromPendingDeletion(
|
|
6533
|
+
async reactivateFromPendingDeletion(id26) {
|
|
6447
6534
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
6448
6535
|
and(
|
|
6449
|
-
eq(users.id,
|
|
6536
|
+
eq(users.id, id26),
|
|
6450
6537
|
eq(users.status, "pending_deletion")
|
|
6451
6538
|
)
|
|
6452
6539
|
).returning();
|
|
@@ -6456,32 +6543,32 @@ var init_users_repository = __esm({
|
|
|
6456
6543
|
* 비밀번호 업데이트
|
|
6457
6544
|
* Write primary 사용
|
|
6458
6545
|
*/
|
|
6459
|
-
async updatePassword(
|
|
6546
|
+
async updatePassword(id26, passwordHash, clearPasswordChangeRequired = true) {
|
|
6460
6547
|
const updateData = {
|
|
6461
6548
|
passwordHash
|
|
6462
6549
|
};
|
|
6463
6550
|
if (clearPasswordChangeRequired) {
|
|
6464
6551
|
updateData.passwordChangeRequired = false;
|
|
6465
6552
|
}
|
|
6466
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
6553
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id26)).returning();
|
|
6467
6554
|
return result[0] ?? null;
|
|
6468
6555
|
}
|
|
6469
6556
|
/**
|
|
6470
6557
|
* 마지막 로그인 시간 업데이트
|
|
6471
6558
|
* Write primary 사용
|
|
6472
6559
|
*/
|
|
6473
|
-
async updateLastLogin(
|
|
6560
|
+
async updateLastLogin(id26) {
|
|
6474
6561
|
const result = await this.db.update(users).set({
|
|
6475
6562
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
6476
|
-
}).where(eq(users.id,
|
|
6563
|
+
}).where(eq(users.id, id26)).returning();
|
|
6477
6564
|
return result[0] ?? null;
|
|
6478
6565
|
}
|
|
6479
6566
|
/**
|
|
6480
6567
|
* 사용자 삭제
|
|
6481
6568
|
* Write primary 사용
|
|
6482
6569
|
*/
|
|
6483
|
-
async deleteById(
|
|
6484
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
6570
|
+
async deleteById(id26) {
|
|
6571
|
+
const result = await this.db.delete(users).where(eq(users.id, id26)).returning();
|
|
6485
6572
|
return result[0] ?? null;
|
|
6486
6573
|
}
|
|
6487
6574
|
/**
|
|
@@ -6839,12 +6926,15 @@ function assertPasskeyConfig(env21 = passkeyEnvSource()) {
|
|
|
6839
6926
|
);
|
|
6840
6927
|
}
|
|
6841
6928
|
}
|
|
6929
|
+
function minutesOr(configured2, fallback) {
|
|
6930
|
+
const minutes = Number(configured2);
|
|
6931
|
+
return Number.isFinite(minutes) && minutes > 0 ? minutes : fallback;
|
|
6932
|
+
}
|
|
6842
6933
|
function getMfaConfig() {
|
|
6843
|
-
const configuredMinutes = Number(process.env.SPFN_AUTH_MFA_STEP_UP_MINUTES);
|
|
6844
|
-
const minutes = Number.isFinite(configuredMinutes) && configuredMinutes > 0 ? configuredMinutes : DEFAULT_STEP_UP_MINUTES;
|
|
6845
6934
|
return {
|
|
6846
6935
|
issuer: process.env.SPFN_AUTH_MFA_ISSUER?.trim() || process.env.SPFN_AUTH_PASSKEY_RP_NAME?.trim() || mfaIssuerFromAppUrl() || DEFAULT_MFA_ISSUER,
|
|
6847
|
-
stepUpWindowMs:
|
|
6936
|
+
stepUpWindowMs: minutesOr(process.env.SPFN_AUTH_MFA_STEP_UP_MINUTES, DEFAULT_STEP_UP_MINUTES) * 6e4,
|
|
6937
|
+
challengeTtlMs: minutesOr(process.env.SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES, DEFAULT_CHALLENGE_TTL_MINUTES) * 6e4
|
|
6848
6938
|
};
|
|
6849
6939
|
}
|
|
6850
6940
|
function mfaIssuerFromAppUrl() {
|
|
@@ -6858,7 +6948,7 @@ function mfaIssuerFromAppUrl() {
|
|
|
6858
6948
|
return null;
|
|
6859
6949
|
}
|
|
6860
6950
|
}
|
|
6861
|
-
var COOKIE_NAMES, globalConfig, CSRF_MODES, unrecognizedCsrfModeReported, PACKAGE_CSRF_EXEMPT_PATHS, DEFAULT_SESSION_RENEW_PATH, PASSKEY_USER_VERIFICATIONS, DEFAULT_CHALLENGE_TTL_SECONDS, DEFAULT_RECENT_AUTH_MINUTES, PASSKEY_VARS, DEFAULT_MFA_ISSUER, DEFAULT_STEP_UP_MINUTES;
|
|
6951
|
+
var COOKIE_NAMES, globalConfig, CSRF_MODES, unrecognizedCsrfModeReported, PACKAGE_CSRF_EXEMPT_PATHS, DEFAULT_SESSION_RENEW_PATH, PASSKEY_USER_VERIFICATIONS, DEFAULT_CHALLENGE_TTL_SECONDS, DEFAULT_RECENT_AUTH_MINUTES, PASSKEY_VARS, DEFAULT_MFA_ISSUER, DEFAULT_STEP_UP_MINUTES, DEFAULT_CHALLENGE_TTL_MINUTES;
|
|
6862
6952
|
var init_config = __esm({
|
|
6863
6953
|
"src/server/lib/config.ts"() {
|
|
6864
6954
|
"use strict";
|
|
@@ -6878,6 +6968,18 @@ var init_config = __esm({
|
|
|
6878
6968
|
get OAUTH_PENDING() {
|
|
6879
6969
|
return `spfn_oauth_pending${getCookieSuffix()}`;
|
|
6880
6970
|
},
|
|
6971
|
+
/**
|
|
6972
|
+
* Pending second-factor session (privateKey, keyId, challengeHash) (#95)
|
|
6973
|
+
*
|
|
6974
|
+
* Its own name and its own audience, separate from OAUTH_PENDING. The two
|
|
6975
|
+
* coexist: a person who starts a social login in one tab while a password
|
|
6976
|
+
* step-up is outstanding in another has both flows live, and one name would
|
|
6977
|
+
* mean the second overwrote the first — sealing a session with a private key
|
|
6978
|
+
* that does not match the key being activated.
|
|
6979
|
+
*/
|
|
6980
|
+
get MFA_PENDING() {
|
|
6981
|
+
return `spfn_mfa_pending${getCookieSuffix()}`;
|
|
6982
|
+
},
|
|
6881
6983
|
/** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
|
|
6882
6984
|
get OAUTH_CSRF() {
|
|
6883
6985
|
return `spfn_oauth_csrf${getCookieSuffix()}`;
|
|
@@ -6920,6 +7022,7 @@ var init_config = __esm({
|
|
|
6920
7022
|
];
|
|
6921
7023
|
DEFAULT_MFA_ISSUER = "SPFN";
|
|
6922
7024
|
DEFAULT_STEP_UP_MINUTES = 10;
|
|
7025
|
+
DEFAULT_CHALLENGE_TTL_MINUTES = 10;
|
|
6923
7026
|
}
|
|
6924
7027
|
});
|
|
6925
7028
|
|
|
@@ -6932,6 +7035,7 @@ var init_keys_repository = __esm({
|
|
|
6932
7035
|
"use strict";
|
|
6933
7036
|
init_user_public_keys();
|
|
6934
7037
|
init_users();
|
|
7038
|
+
init_mfa_challenges();
|
|
6935
7039
|
init_config();
|
|
6936
7040
|
LAST_USED_THROTTLE_MS = 6e4;
|
|
6937
7041
|
KeysRepository = class extends BaseRepository2 {
|
|
@@ -6979,6 +7083,12 @@ var init_keys_repository = __esm({
|
|
|
6979
7083
|
* `lastSeenIp` is not selected, deliberately. The concurrent-use moment is
|
|
6980
7084
|
* what the owner acts on; the trail of addresses behind it is stored PII the
|
|
6981
7085
|
* account surface has no use for.
|
|
7086
|
+
*
|
|
7087
|
+
* A key still waiting on a second factor is excluded in **both** modes
|
|
7088
|
+
* (#95). It is not a device that can sign, so it does not belong in the
|
|
7089
|
+
* default list; and it is not a device the owner signed out either, so it
|
|
7090
|
+
* does not belong in the revoked one — where it would read exactly like a
|
|
7091
|
+
* revoked key with no revocation time, which is a different thing entirely.
|
|
6982
7092
|
* Read replica 사용
|
|
6983
7093
|
*/
|
|
6984
7094
|
async listForUser(userId, includeRevoked = false) {
|
|
@@ -6997,12 +7107,11 @@ var init_keys_repository = __esm({
|
|
|
6997
7107
|
registeredUserAgent: userPublicKeys.registeredUserAgent,
|
|
6998
7108
|
binding: userPublicKeys.binding,
|
|
6999
7109
|
concurrentUseAt: userPublicKeys.concurrentUseAt
|
|
7000
|
-
}).from(userPublicKeys).where(
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
).orderBy(desc(userPublicKeys.createdAt));
|
|
7110
|
+
}).from(userPublicKeys).where(and2(
|
|
7111
|
+
eq2(userPublicKeys.userId, userId),
|
|
7112
|
+
isNull(userPublicKeys.pendingMfaChallengeId),
|
|
7113
|
+
includeRevoked ? void 0 : eq2(userPublicKeys.isActive, true)
|
|
7114
|
+
)).orderBy(desc(userPublicKeys.createdAt));
|
|
7006
7115
|
}
|
|
7007
7116
|
/**
|
|
7008
7117
|
* 공개키 생성
|
|
@@ -7071,6 +7180,17 @@ var init_keys_repository = __esm({
|
|
|
7071
7180
|
* active keys still had its generation ended, and an outstanding
|
|
7072
7181
|
* sign-out-everywhere link must die with it.
|
|
7073
7182
|
*
|
|
7183
|
+
* The two second-factor CTEs are here for the same "no caller can forget"
|
|
7184
|
+
* reason (#95). A pending key is `is_active = false`, so the UPDATE below
|
|
7185
|
+
* does not touch it and neither would anything else: the owner who sees an
|
|
7186
|
+
* unexpected second-factor prompt and does exactly what the notice says —
|
|
7187
|
+
* change the password, sign out everywhere, open the revoke-all link, run a
|
|
7188
|
+
* reset — would otherwise leave the attacker's pending key and its live
|
|
7189
|
+
* challenge untouched. Deleting the key cascades onto its challenge; the
|
|
7190
|
+
* expiry beside it catches a challenge whose key is already gone. A pending
|
|
7191
|
+
* row and an active row are disjoint sets, so the two statements never
|
|
7192
|
+
* contend for the same row.
|
|
7193
|
+
*
|
|
7074
7194
|
* Write primary 사용
|
|
7075
7195
|
*/
|
|
7076
7196
|
async revokeActive(userId, reason, keepKeyId) {
|
|
@@ -7080,6 +7200,18 @@ var init_keys_repository = __esm({
|
|
|
7080
7200
|
UPDATE ${users}
|
|
7081
7201
|
SET key_epoch = key_epoch + 1
|
|
7082
7202
|
WHERE id = ${userId}
|
|
7203
|
+
),
|
|
7204
|
+
challenges_expired AS (
|
|
7205
|
+
UPDATE ${mfaChallenges}
|
|
7206
|
+
SET expires_at = now()
|
|
7207
|
+
WHERE user_id = ${userId}
|
|
7208
|
+
AND verified_at IS NULL
|
|
7209
|
+
AND expires_at > now()
|
|
7210
|
+
),
|
|
7211
|
+
pending_dropped AS (
|
|
7212
|
+
DELETE FROM ${userPublicKeys}
|
|
7213
|
+
WHERE user_id = ${userId}
|
|
7214
|
+
AND pending_mfa_challenge_id IS NOT NULL
|
|
7083
7215
|
)
|
|
7084
7216
|
UPDATE ${userPublicKeys} t
|
|
7085
7217
|
SET is_active = false,
|
|
@@ -7234,7 +7366,7 @@ var init_keys_repository = __esm({
|
|
|
7234
7366
|
* replica-lagged address compared in application code both misses real
|
|
7235
7367
|
* switches and invents ones that did not happen.
|
|
7236
7368
|
*/
|
|
7237
|
-
async updateLastUsedById(
|
|
7369
|
+
async updateLastUsedById(id26, identity, ip) {
|
|
7238
7370
|
const now = /* @__PURE__ */ new Date();
|
|
7239
7371
|
const nowParam = sql5`${now.toISOString()}::timestamptz`;
|
|
7240
7372
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
@@ -7275,7 +7407,7 @@ var init_keys_repository = __esm({
|
|
|
7275
7407
|
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
7276
7408
|
} : {}
|
|
7277
7409
|
}).where(and2(
|
|
7278
|
-
eq2(userPublicKeys.id,
|
|
7410
|
+
eq2(userPublicKeys.id, id26),
|
|
7279
7411
|
or(lastUsedIsStale, identityChanged, sql5`(${ipChanged} AND ${notStampedThisWindow})`)
|
|
7280
7412
|
));
|
|
7281
7413
|
}
|
|
@@ -7326,8 +7458,8 @@ var init_verification_codes_repository = __esm({
|
|
|
7326
7458
|
* ID로 인증 코드 조회
|
|
7327
7459
|
* Read replica 사용
|
|
7328
7460
|
*/
|
|
7329
|
-
async findById(
|
|
7330
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
7461
|
+
async findById(id26) {
|
|
7462
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id26)).limit(1);
|
|
7331
7463
|
return result[0] ?? null;
|
|
7332
7464
|
}
|
|
7333
7465
|
/**
|
|
@@ -7341,22 +7473,22 @@ var init_verification_codes_repository = __esm({
|
|
|
7341
7473
|
* 인증 코드 사용 처리
|
|
7342
7474
|
* Write primary 사용
|
|
7343
7475
|
*/
|
|
7344
|
-
async markAsUsed(
|
|
7476
|
+
async markAsUsed(id26) {
|
|
7345
7477
|
const result = await this.db.update(verificationCodes).set({
|
|
7346
7478
|
usedAt: /* @__PURE__ */ new Date()
|
|
7347
|
-
}).where(eq3(verificationCodes.id,
|
|
7479
|
+
}).where(eq3(verificationCodes.id, id26)).returning();
|
|
7348
7480
|
return result[0] ?? null;
|
|
7349
7481
|
}
|
|
7350
7482
|
/**
|
|
7351
7483
|
* 시도 횟수 증가
|
|
7352
7484
|
* Write primary 사용
|
|
7353
7485
|
*/
|
|
7354
|
-
async incrementAttempts(
|
|
7355
|
-
const code = await this.findById(
|
|
7486
|
+
async incrementAttempts(id26) {
|
|
7487
|
+
const code = await this.findById(id26);
|
|
7356
7488
|
if (!code) return null;
|
|
7357
7489
|
const result = await this.db.update(verificationCodes).set({
|
|
7358
7490
|
attempts: code.attempts + 1
|
|
7359
|
-
}).where(eq3(verificationCodes.id,
|
|
7491
|
+
}).where(eq3(verificationCodes.id, id26)).returning();
|
|
7360
7492
|
return result[0] ?? null;
|
|
7361
7493
|
}
|
|
7362
7494
|
/**
|
|
@@ -7440,10 +7572,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7440
7572
|
*
|
|
7441
7573
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
7442
7574
|
*/
|
|
7443
|
-
async issue(
|
|
7575
|
+
async issue(id26, tokenHash) {
|
|
7444
7576
|
const result = await this.db.update(signupLinkTokens).set({ tokenHash }).where(
|
|
7445
7577
|
and4(
|
|
7446
|
-
eq4(signupLinkTokens.id,
|
|
7578
|
+
eq4(signupLinkTokens.id, id26),
|
|
7447
7579
|
isNull3(signupLinkTokens.consumedAt),
|
|
7448
7580
|
isNull3(signupLinkTokens.supersededAt),
|
|
7449
7581
|
isNull3(signupLinkTokens.completedAt),
|
|
@@ -7479,14 +7611,14 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7479
7611
|
*
|
|
7480
7612
|
* @returns the updated row, or null if another request claimed it first
|
|
7481
7613
|
*/
|
|
7482
|
-
async claimLink(
|
|
7614
|
+
async claimLink(id26, setupSecretHash, setupExpiresAt) {
|
|
7483
7615
|
const result = await this.db.update(signupLinkTokens).set({
|
|
7484
7616
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
7485
7617
|
setupSecretHash,
|
|
7486
7618
|
setupExpiresAt
|
|
7487
7619
|
}).where(
|
|
7488
7620
|
and4(
|
|
7489
|
-
eq4(signupLinkTokens.id,
|
|
7621
|
+
eq4(signupLinkTokens.id, id26),
|
|
7490
7622
|
isNull3(signupLinkTokens.consumedAt),
|
|
7491
7623
|
isNull3(signupLinkTokens.supersededAt)
|
|
7492
7624
|
)
|
|
@@ -7499,10 +7631,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7499
7631
|
*
|
|
7500
7632
|
* @returns the updated row, or null if another request completed it first
|
|
7501
7633
|
*/
|
|
7502
|
-
async claimSetupSession(
|
|
7634
|
+
async claimSetupSession(id26) {
|
|
7503
7635
|
const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
7504
7636
|
and4(
|
|
7505
|
-
eq4(signupLinkTokens.id,
|
|
7637
|
+
eq4(signupLinkTokens.id, id26),
|
|
7506
7638
|
isNull3(signupLinkTokens.completedAt),
|
|
7507
7639
|
isNull3(signupLinkTokens.supersededAt)
|
|
7508
7640
|
)
|
|
@@ -7584,10 +7716,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7584
7716
|
*
|
|
7585
7717
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
7586
7718
|
*/
|
|
7587
|
-
async issue(
|
|
7719
|
+
async issue(id26, tokenHash) {
|
|
7588
7720
|
const result = await this.db.update(passwordResetTokens).set({ tokenHash }).where(
|
|
7589
7721
|
and5(
|
|
7590
|
-
eq5(passwordResetTokens.id,
|
|
7722
|
+
eq5(passwordResetTokens.id, id26),
|
|
7591
7723
|
isNull4(passwordResetTokens.consumedAt),
|
|
7592
7724
|
isNull4(passwordResetTokens.supersededAt),
|
|
7593
7725
|
isNull4(passwordResetTokens.completedAt),
|
|
@@ -7635,14 +7767,14 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7635
7767
|
*
|
|
7636
7768
|
* @returns the updated row, or null if another request claimed it first
|
|
7637
7769
|
*/
|
|
7638
|
-
async consume(
|
|
7770
|
+
async consume(id26, setupSecretHash, setupExpiresAt) {
|
|
7639
7771
|
const result = await this.db.update(passwordResetTokens).set({
|
|
7640
7772
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
7641
7773
|
setupSecretHash,
|
|
7642
7774
|
setupExpiresAt
|
|
7643
7775
|
}).where(
|
|
7644
7776
|
and5(
|
|
7645
|
-
eq5(passwordResetTokens.id,
|
|
7777
|
+
eq5(passwordResetTokens.id, id26),
|
|
7646
7778
|
isNull4(passwordResetTokens.consumedAt),
|
|
7647
7779
|
isNull4(passwordResetTokens.supersededAt)
|
|
7648
7780
|
)
|
|
@@ -7655,10 +7787,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7655
7787
|
*
|
|
7656
7788
|
* @returns the updated row, or null if another request completed it first
|
|
7657
7789
|
*/
|
|
7658
|
-
async complete(
|
|
7790
|
+
async complete(id26) {
|
|
7659
7791
|
const result = await this.db.update(passwordResetTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
7660
7792
|
and5(
|
|
7661
|
-
eq5(passwordResetTokens.id,
|
|
7793
|
+
eq5(passwordResetTokens.id, id26),
|
|
7662
7794
|
isNull4(passwordResetTokens.completedAt),
|
|
7663
7795
|
isNull4(passwordResetTokens.supersededAt)
|
|
7664
7796
|
)
|
|
@@ -7856,9 +7988,9 @@ var init_passkeys_repository = __esm({
|
|
|
7856
7988
|
* Owner-scoped, so an id belonging to someone else answers null rather than
|
|
7857
7989
|
* a row — a management route can only ever say "not yours".
|
|
7858
7990
|
*/
|
|
7859
|
-
async findLiveByIdAndUserId(
|
|
7991
|
+
async findLiveByIdAndUserId(id26, userId) {
|
|
7860
7992
|
const result = await this.db.select().from(passkeys).where(and7(
|
|
7861
|
-
eq7(passkeys.id,
|
|
7993
|
+
eq7(passkeys.id, id26),
|
|
7862
7994
|
eq7(passkeys.userId, userId),
|
|
7863
7995
|
isNull6(passkeys.revokedAt)
|
|
7864
7996
|
)).limit(1);
|
|
@@ -7868,17 +8000,17 @@ var init_passkeys_repository = __esm({
|
|
|
7868
8000
|
* Record a successful assertion.
|
|
7869
8001
|
* Write primary.
|
|
7870
8002
|
*/
|
|
7871
|
-
async recordUse(
|
|
7872
|
-
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id,
|
|
8003
|
+
async recordUse(id26, counter) {
|
|
8004
|
+
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id, id26));
|
|
7873
8005
|
}
|
|
7874
8006
|
/**
|
|
7875
8007
|
* Rename, but only a credential this user still owns and has not revoked.
|
|
7876
8008
|
*
|
|
7877
8009
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
7878
8010
|
*/
|
|
7879
|
-
async renameByIdAndUserId(
|
|
8011
|
+
async renameByIdAndUserId(id26, userId, label) {
|
|
7880
8012
|
const result = await this.db.update(passkeys).set({ label }).where(and7(
|
|
7881
|
-
eq7(passkeys.id,
|
|
8013
|
+
eq7(passkeys.id, id26),
|
|
7882
8014
|
eq7(passkeys.userId, userId),
|
|
7883
8015
|
isNull6(passkeys.revokedAt)
|
|
7884
8016
|
)).returning();
|
|
@@ -7893,9 +8025,9 @@ var init_passkeys_repository = __esm({
|
|
|
7893
8025
|
*
|
|
7894
8026
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
7895
8027
|
*/
|
|
7896
|
-
async revokeByIdAndUserId(
|
|
8028
|
+
async revokeByIdAndUserId(id26, userId, reason) {
|
|
7897
8029
|
const result = await this.db.update(passkeys).set({ revokedAt: /* @__PURE__ */ new Date(), revokedReason: reason }).where(and7(
|
|
7898
|
-
eq7(passkeys.id,
|
|
8030
|
+
eq7(passkeys.id, id26),
|
|
7899
8031
|
eq7(passkeys.userId, userId),
|
|
7900
8032
|
isNull6(passkeys.revokedAt)
|
|
7901
8033
|
)).returning();
|
|
@@ -7910,9 +8042,9 @@ var init_passkeys_repository = __esm({
|
|
|
7910
8042
|
*
|
|
7911
8043
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
7912
8044
|
*/
|
|
7913
|
-
async markSecondFactorByIdAndUserId(
|
|
8045
|
+
async markSecondFactorByIdAndUserId(id26, userId, secondFactor) {
|
|
7914
8046
|
const result = await this.db.update(passkeys).set({ secondFactor }).where(and7(
|
|
7915
|
-
eq7(passkeys.id,
|
|
8047
|
+
eq7(passkeys.id, id26),
|
|
7916
8048
|
eq7(passkeys.userId, userId),
|
|
7917
8049
|
isNull6(passkeys.revokedAt)
|
|
7918
8050
|
)).returning();
|
|
@@ -8133,8 +8265,8 @@ var init_mfa_recovery_codes_repository = __esm({
|
|
|
8133
8265
|
*
|
|
8134
8266
|
* @returns true when this call spent the row
|
|
8135
8267
|
*/
|
|
8136
|
-
async consume(
|
|
8137
|
-
const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id,
|
|
8268
|
+
async consume(id26) {
|
|
8269
|
+
const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id, id26), isNull9(mfaRecoveryCodes.usedAt))).returning();
|
|
8138
8270
|
return result.length > 0;
|
|
8139
8271
|
}
|
|
8140
8272
|
/** Drop every generation the account has. Write primary. */
|
|
@@ -8186,9 +8318,161 @@ var init_mfa_verifications_repository = __esm({
|
|
|
8186
8318
|
}
|
|
8187
8319
|
});
|
|
8188
8320
|
|
|
8189
|
-
// src/server/repositories/mfa-
|
|
8321
|
+
// src/server/repositories/mfa-challenges.repository.ts
|
|
8190
8322
|
import { BaseRepository as BaseRepository12 } from "@spfn/core/db";
|
|
8191
|
-
import { and as and12, eq as eq12, isNotNull, isNull as isNull10 } from "drizzle-orm";
|
|
8323
|
+
import { and as and12, eq as eq12, gt as gt6, inArray, isNotNull, isNull as isNull10, lte, or as or3, sql as sql8 } from "drizzle-orm";
|
|
8324
|
+
var MfaChallengesRepository, mfaChallengesRepository;
|
|
8325
|
+
var init_mfa_challenges_repository = __esm({
|
|
8326
|
+
"src/server/repositories/mfa-challenges.repository.ts"() {
|
|
8327
|
+
"use strict";
|
|
8328
|
+
init_mfa_challenges();
|
|
8329
|
+
init_user_public_keys();
|
|
8330
|
+
MfaChallengesRepository = class extends BaseRepository12 {
|
|
8331
|
+
/** Park a fresh challenge. The caller has already written the pending key. */
|
|
8332
|
+
async create(data) {
|
|
8333
|
+
return await this._create(mfaChallenges, data);
|
|
8334
|
+
}
|
|
8335
|
+
/**
|
|
8336
|
+
* The challenge this secret names, whatever state it is in.
|
|
8337
|
+
*
|
|
8338
|
+
* Reads the primary rather than the replica: `verify` decides whether a key
|
|
8339
|
+
* becomes usable, and a replica lagging behind the 202 that created the row
|
|
8340
|
+
* would refuse a challenge the caller was handed a moment ago.
|
|
8341
|
+
*/
|
|
8342
|
+
async findByHash(challengeHash) {
|
|
8343
|
+
const result = await this.db.select().from(mfaChallenges).where(eq12(mfaChallenges.challengeHash, challengeHash)).limit(1);
|
|
8344
|
+
return result[0] ?? null;
|
|
8345
|
+
}
|
|
8346
|
+
/**
|
|
8347
|
+
* The live challenge already outstanding for this key, if there is one.
|
|
8348
|
+
*
|
|
8349
|
+
* What makes a retried registration resume rather than collide: a native
|
|
8350
|
+
* client reusing its keyId, or an OAuth state replayed from the back button,
|
|
8351
|
+
* gets the challenge it was already given instead of a 409 about a key it
|
|
8352
|
+
* does own.
|
|
8353
|
+
*/
|
|
8354
|
+
async findLiveByKeyId(userId, keyId) {
|
|
8355
|
+
const result = await this.db.select().from(mfaChallenges).where(and12(
|
|
8356
|
+
eq12(mfaChallenges.userId, userId),
|
|
8357
|
+
eq12(mfaChallenges.keyId, keyId),
|
|
8358
|
+
isNull10(mfaChallenges.verifiedAt),
|
|
8359
|
+
gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
|
|
8360
|
+
)).limit(1);
|
|
8361
|
+
return result[0] ?? null;
|
|
8362
|
+
}
|
|
8363
|
+
/**
|
|
8364
|
+
* Spend the challenge, or answer false because somebody else already did.
|
|
8365
|
+
*
|
|
8366
|
+
* A conditional UPDATE and not a read followed by a write: two verifies
|
|
8367
|
+
* arriving together would both pass a read, and a challenge is single use.
|
|
8368
|
+
* Every condition the caller checked is restated here, so the row that is
|
|
8369
|
+
* marked is a row that was still spendable at the moment it was marked.
|
|
8370
|
+
*/
|
|
8371
|
+
async markVerified(id26, keyEpoch) {
|
|
8372
|
+
const result = await this.db.update(mfaChallenges).set({ verifiedAt: /* @__PURE__ */ new Date() }).where(and12(
|
|
8373
|
+
eq12(mfaChallenges.id, id26),
|
|
8374
|
+
eq12(mfaChallenges.keyEpoch, keyEpoch),
|
|
8375
|
+
isNull10(mfaChallenges.verifiedAt),
|
|
8376
|
+
gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
|
|
8377
|
+
)).returning();
|
|
8378
|
+
return result.length > 0;
|
|
8379
|
+
}
|
|
8380
|
+
/**
|
|
8381
|
+
* Count a wrong proof, and say whether that was the last one allowed.
|
|
8382
|
+
*
|
|
8383
|
+
* The increment is on the row the presented secret actually hashed to, which
|
|
8384
|
+
* is the whole reason lookup is by hash: there is no id for a caller to name,
|
|
8385
|
+
* so no counter but their own can be moved.
|
|
8386
|
+
*/
|
|
8387
|
+
async countFailure(id26) {
|
|
8388
|
+
const result = await this.db.update(mfaChallenges).set({ attempts: sql8`${mfaChallenges.attempts} + 1` }).where(eq12(mfaChallenges.id, id26)).returning({ attempts: mfaChallenges.attempts });
|
|
8389
|
+
return (result[0]?.attempts ?? 0) >= MFA_CHALLENGE_ATTEMPT_LIMIT;
|
|
8390
|
+
}
|
|
8391
|
+
/**
|
|
8392
|
+
* Point an inactive key at the challenge that gates it.
|
|
8393
|
+
*
|
|
8394
|
+
* A second statement rather than a column on the insert, because the
|
|
8395
|
+
* challenge references the key: the key row has to exist before the
|
|
8396
|
+
* challenge can, so the back-reference is written once both do. Both run
|
|
8397
|
+
* inside the registering route's transaction.
|
|
8398
|
+
*/
|
|
8399
|
+
async markKeyPending(keyId, challengeId) {
|
|
8400
|
+
await this.db.update(userPublicKeys).set({ pendingMfaChallengeId: challengeId }).where(eq12(userPublicKeys.keyId, keyId));
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8403
|
+
* Give a live challenge a fresh secret, leaving everything else alone.
|
|
8404
|
+
*
|
|
8405
|
+
* What a retried registration gets. The row keeps its id, its expiry and its
|
|
8406
|
+
* spent attempts, so retrying is not a way to reset the counter or buy
|
|
8407
|
+
* another ten minutes; only the secret is new, because the old one was never
|
|
8408
|
+
* stored and cannot be handed out twice.
|
|
8409
|
+
*/
|
|
8410
|
+
async resecret(id26, challengeHash) {
|
|
8411
|
+
const result = await this.db.update(mfaChallenges).set({ challengeHash }).where(and12(
|
|
8412
|
+
eq12(mfaChallenges.id, id26),
|
|
8413
|
+
isNull10(mfaChallenges.verifiedAt),
|
|
8414
|
+
gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
|
|
8415
|
+
)).returning({ id: mfaChallenges.id });
|
|
8416
|
+
return result.length > 0;
|
|
8417
|
+
}
|
|
8418
|
+
/**
|
|
8419
|
+
* Delete the pending key a challenge was gating, which takes the challenge
|
|
8420
|
+
* with it through the cascade.
|
|
8421
|
+
*
|
|
8422
|
+
* Scoped to `pending_mfa_challenge_id`, so a key that has since been
|
|
8423
|
+
* activated is never deleted by a late failure or a sweep.
|
|
8424
|
+
*/
|
|
8425
|
+
async dropPendingKey(keyId, challengeId) {
|
|
8426
|
+
await this.db.delete(userPublicKeys).where(and12(
|
|
8427
|
+
eq12(userPublicKeys.keyId, keyId),
|
|
8428
|
+
eq12(userPublicKeys.pendingMfaChallengeId, challengeId)
|
|
8429
|
+
));
|
|
8430
|
+
}
|
|
8431
|
+
/**
|
|
8432
|
+
* Mark a key active and no longer pending — what a verified challenge buys.
|
|
8433
|
+
*
|
|
8434
|
+
* Conditioned on the challenge id, so this can only activate the key that
|
|
8435
|
+
* challenge was minted for.
|
|
8436
|
+
*/
|
|
8437
|
+
async activateKey(keyId, challengeId) {
|
|
8438
|
+
await this.db.update(userPublicKeys).set({ isActive: true, pendingMfaChallengeId: null }).where(and12(
|
|
8439
|
+
eq12(userPublicKeys.keyId, keyId),
|
|
8440
|
+
eq12(userPublicKeys.pendingMfaChallengeId, challengeId)
|
|
8441
|
+
));
|
|
8442
|
+
}
|
|
8443
|
+
/**
|
|
8444
|
+
* Clear out challenges nobody finished, and the keys they were gating.
|
|
8445
|
+
*
|
|
8446
|
+
* Counted from the read rather than from either delete, because the two
|
|
8447
|
+
* overlap: deleting a pending key cascades onto its challenge, so the second
|
|
8448
|
+
* statement finds only the challenges whose key had already gone some other
|
|
8449
|
+
* way — and a count taken there would report a sweep that did nothing.
|
|
8450
|
+
*
|
|
8451
|
+
* A spent challenge is kept for the same span as an expired one, so a replay
|
|
8452
|
+
* inside the window meets "already verified" from a row rather than
|
|
8453
|
+
* "unknown" from an absence. The 401 is the same either way; the record is
|
|
8454
|
+
* not.
|
|
8455
|
+
*
|
|
8456
|
+
* @returns how many challenge rows are gone
|
|
8457
|
+
*/
|
|
8458
|
+
async sweepFinished(cutoff) {
|
|
8459
|
+
const finished = or3(lte(mfaChallenges.expiresAt, cutoff), isNotNull(mfaChallenges.verifiedAt));
|
|
8460
|
+
const doomed = await this.readDb.select({ id: mfaChallenges.id }).from(mfaChallenges).where(finished);
|
|
8461
|
+
if (doomed.length === 0) {
|
|
8462
|
+
return 0;
|
|
8463
|
+
}
|
|
8464
|
+
await this.db.delete(userPublicKeys).where(inArray(userPublicKeys.pendingMfaChallengeId, doomed.map((row) => row.id)));
|
|
8465
|
+
await this.db.delete(mfaChallenges).where(finished);
|
|
8466
|
+
return doomed.length;
|
|
8467
|
+
}
|
|
8468
|
+
};
|
|
8469
|
+
mfaChallengesRepository = new MfaChallengesRepository();
|
|
8470
|
+
}
|
|
8471
|
+
});
|
|
8472
|
+
|
|
8473
|
+
// src/server/repositories/mfa-enrolment.repository.ts
|
|
8474
|
+
import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
|
|
8475
|
+
import { and as and13, eq as eq13, isNotNull as isNotNull2, isNull as isNull11 } from "drizzle-orm";
|
|
8192
8476
|
import { unionAll } from "drizzle-orm/pg-core";
|
|
8193
8477
|
var MfaEnrolmentRepository, mfaEnrolmentRepository;
|
|
8194
8478
|
var init_mfa_enrolment_repository = __esm({
|
|
@@ -8196,19 +8480,19 @@ var init_mfa_enrolment_repository = __esm({
|
|
|
8196
8480
|
"use strict";
|
|
8197
8481
|
init_mfa_totp();
|
|
8198
8482
|
init_passkeys();
|
|
8199
|
-
MfaEnrolmentRepository = class extends
|
|
8483
|
+
MfaEnrolmentRepository = class extends BaseRepository13 {
|
|
8200
8484
|
/** Whether the account has a confirmed TOTP or a live second-factor passkey. */
|
|
8201
8485
|
async isEnrolled(userId) {
|
|
8202
|
-
const confirmedTotp = this.readDb.select({ owner: mfaTotp.userId }).from(mfaTotp).where(
|
|
8486
|
+
const confirmedTotp = this.readDb.select({ owner: mfaTotp.userId }).from(mfaTotp).where(and13(eq13(mfaTotp.userId, userId), isNotNull2(mfaTotp.confirmedAt)));
|
|
8203
8487
|
const rows = await unionAll(confirmedTotp, this.liveSecondFactorPasskeys(userId)).limit(1);
|
|
8204
8488
|
return rows.length > 0;
|
|
8205
8489
|
}
|
|
8206
8490
|
/** The other half of the union, as its own read for `status` to reuse. */
|
|
8207
8491
|
liveSecondFactorPasskeys(userId) {
|
|
8208
|
-
return this.readDb.select({ owner: passkeys.userId }).from(passkeys).where(
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8492
|
+
return this.readDb.select({ owner: passkeys.userId }).from(passkeys).where(and13(
|
|
8493
|
+
eq13(passkeys.userId, userId),
|
|
8494
|
+
eq13(passkeys.secondFactor, true),
|
|
8495
|
+
isNull11(passkeys.revokedAt)
|
|
8212
8496
|
));
|
|
8213
8497
|
}
|
|
8214
8498
|
/** Whether at least one live passkey is marked as a second factor. */
|
|
@@ -8222,15 +8506,15 @@ var init_mfa_enrolment_repository = __esm({
|
|
|
8222
8506
|
});
|
|
8223
8507
|
|
|
8224
8508
|
// src/server/repositories/device-authorizations.repository.ts
|
|
8225
|
-
import { BaseRepository as
|
|
8226
|
-
import { eq as
|
|
8509
|
+
import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
|
|
8510
|
+
import { eq as eq14, and as and14, gt as gt7, inArray as inArray2, sql as sql9 } from "drizzle-orm";
|
|
8227
8511
|
var notExpired, DeviceAuthorizationsRepository, deviceAuthorizationsRepository;
|
|
8228
8512
|
var init_device_authorizations_repository = __esm({
|
|
8229
8513
|
"src/server/repositories/device-authorizations.repository.ts"() {
|
|
8230
8514
|
"use strict";
|
|
8231
8515
|
init_device_authorizations();
|
|
8232
|
-
notExpired = () =>
|
|
8233
|
-
DeviceAuthorizationsRepository = class extends
|
|
8516
|
+
notExpired = () => gt7(deviceAuthorizations.expiresAt, sql9`now()`);
|
|
8517
|
+
DeviceAuthorizationsRepository = class extends BaseRepository14 {
|
|
8234
8518
|
/**
|
|
8235
8519
|
* Insert a pending authorization, unless one of its codes is already taken.
|
|
8236
8520
|
*
|
|
@@ -8258,7 +8542,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8258
8542
|
* Read replica.
|
|
8259
8543
|
*/
|
|
8260
8544
|
async findByUserCode(userCode) {
|
|
8261
|
-
const result = await this.readDb.select().from(deviceAuthorizations).where(
|
|
8545
|
+
const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.userCode, userCode)).limit(1);
|
|
8262
8546
|
return result[0] ?? null;
|
|
8263
8547
|
}
|
|
8264
8548
|
/**
|
|
@@ -8268,7 +8552,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8268
8552
|
* Read replica.
|
|
8269
8553
|
*/
|
|
8270
8554
|
async findByDeviceCodeHash(deviceCodeHash) {
|
|
8271
|
-
const result = await this.readDb.select().from(deviceAuthorizations).where(
|
|
8555
|
+
const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
8272
8556
|
return result[0] ?? null;
|
|
8273
8557
|
}
|
|
8274
8558
|
/**
|
|
@@ -8280,11 +8564,11 @@ var init_device_authorizations_repository = __esm({
|
|
|
8280
8564
|
*
|
|
8281
8565
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8282
8566
|
*/
|
|
8283
|
-
async approve(
|
|
8567
|
+
async approve(id26, userId) {
|
|
8284
8568
|
const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8569
|
+
and14(
|
|
8570
|
+
eq14(deviceAuthorizations.id, id26),
|
|
8571
|
+
eq14(deviceAuthorizations.status, "pending"),
|
|
8288
8572
|
notExpired()
|
|
8289
8573
|
)
|
|
8290
8574
|
).returning();
|
|
@@ -8297,11 +8581,11 @@ var init_device_authorizations_repository = __esm({
|
|
|
8297
8581
|
*
|
|
8298
8582
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8299
8583
|
*/
|
|
8300
|
-
async deny(
|
|
8584
|
+
async deny(id26) {
|
|
8301
8585
|
const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8586
|
+
and14(
|
|
8587
|
+
eq14(deviceAuthorizations.id, id26),
|
|
8588
|
+
eq14(deviceAuthorizations.status, "pending"),
|
|
8305
8589
|
notExpired()
|
|
8306
8590
|
)
|
|
8307
8591
|
).returning();
|
|
@@ -8334,9 +8618,9 @@ var init_device_authorizations_repository = __esm({
|
|
|
8334
8618
|
*/
|
|
8335
8619
|
async denyAllActiveByUserId(userId) {
|
|
8336
8620
|
return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8621
|
+
and14(
|
|
8622
|
+
eq14(deviceAuthorizations.userId, userId),
|
|
8623
|
+
inArray2(deviceAuthorizations.status, ["pending", "approved"])
|
|
8340
8624
|
)
|
|
8341
8625
|
).returning();
|
|
8342
8626
|
}
|
|
@@ -8353,9 +8637,9 @@ var init_device_authorizations_repository = __esm({
|
|
|
8353
8637
|
*/
|
|
8354
8638
|
async consumeApproved(deviceCodeHash) {
|
|
8355
8639
|
const result = await this.db.update(deviceAuthorizations).set({ status: "consumed", consumedAt: /* @__PURE__ */ new Date() }).where(
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8640
|
+
and14(
|
|
8641
|
+
eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash),
|
|
8642
|
+
eq14(deviceAuthorizations.status, "approved"),
|
|
8359
8643
|
notExpired()
|
|
8360
8644
|
)
|
|
8361
8645
|
).returning();
|
|
@@ -8367,26 +8651,26 @@ var init_device_authorizations_repository = __esm({
|
|
|
8367
8651
|
});
|
|
8368
8652
|
|
|
8369
8653
|
// src/server/repositories/roles.repository.ts
|
|
8370
|
-
import { BaseRepository as
|
|
8371
|
-
import { eq as
|
|
8654
|
+
import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
|
|
8655
|
+
import { eq as eq15, asc } from "drizzle-orm";
|
|
8372
8656
|
var RolesRepository, rolesRepository;
|
|
8373
8657
|
var init_roles_repository = __esm({
|
|
8374
8658
|
"src/server/repositories/roles.repository.ts"() {
|
|
8375
8659
|
"use strict";
|
|
8376
8660
|
init_roles();
|
|
8377
|
-
RolesRepository = class extends
|
|
8661
|
+
RolesRepository = class extends BaseRepository15 {
|
|
8378
8662
|
/**
|
|
8379
8663
|
* ID로 역할 조회
|
|
8380
8664
|
*/
|
|
8381
|
-
async findById(
|
|
8382
|
-
const result = await this.readDb.select().from(roles).where(
|
|
8665
|
+
async findById(id26) {
|
|
8666
|
+
const result = await this.readDb.select().from(roles).where(eq15(roles.id, id26)).limit(1);
|
|
8383
8667
|
return result[0] ?? null;
|
|
8384
8668
|
}
|
|
8385
8669
|
/**
|
|
8386
8670
|
* Name으로 역할 조회
|
|
8387
8671
|
*/
|
|
8388
8672
|
async findByName(name) {
|
|
8389
|
-
const result = await this.readDb.select().from(roles).where(
|
|
8673
|
+
const result = await this.readDb.select().from(roles).where(eq15(roles.name, name)).limit(1);
|
|
8390
8674
|
return result[0] ?? null;
|
|
8391
8675
|
}
|
|
8392
8676
|
/**
|
|
@@ -8399,7 +8683,7 @@ var init_roles_repository = __esm({
|
|
|
8399
8683
|
* 활성 역할만 조회
|
|
8400
8684
|
*/
|
|
8401
8685
|
async findActive() {
|
|
8402
|
-
return this.readDb.select().from(roles).where(
|
|
8686
|
+
return this.readDb.select().from(roles).where(eq15(roles.isActive, true)).orderBy(asc(roles.priority));
|
|
8403
8687
|
}
|
|
8404
8688
|
/**
|
|
8405
8689
|
* 역할 생성
|
|
@@ -8410,15 +8694,15 @@ var init_roles_repository = __esm({
|
|
|
8410
8694
|
/**
|
|
8411
8695
|
* 역할 업데이트
|
|
8412
8696
|
*/
|
|
8413
|
-
async updateById(
|
|
8414
|
-
const result = await this.db.update(roles).set(data).where(
|
|
8697
|
+
async updateById(id26, data) {
|
|
8698
|
+
const result = await this.db.update(roles).set(data).where(eq15(roles.id, id26)).returning();
|
|
8415
8699
|
return result[0] ?? null;
|
|
8416
8700
|
}
|
|
8417
8701
|
/**
|
|
8418
8702
|
* 역할 삭제
|
|
8419
8703
|
*/
|
|
8420
|
-
async deleteById(
|
|
8421
|
-
const result = await this.db.delete(roles).where(
|
|
8704
|
+
async deleteById(id26) {
|
|
8705
|
+
const result = await this.db.delete(roles).where(eq15(roles.id, id26)).returning();
|
|
8422
8706
|
return result[0] ?? null;
|
|
8423
8707
|
}
|
|
8424
8708
|
};
|
|
@@ -8427,26 +8711,26 @@ var init_roles_repository = __esm({
|
|
|
8427
8711
|
});
|
|
8428
8712
|
|
|
8429
8713
|
// src/server/repositories/permissions.repository.ts
|
|
8430
|
-
import { BaseRepository as
|
|
8431
|
-
import { asc as asc2, eq as
|
|
8714
|
+
import { BaseRepository as BaseRepository16 } from "@spfn/core/db";
|
|
8715
|
+
import { asc as asc2, eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8432
8716
|
var PermissionsRepository, permissionsRepository;
|
|
8433
8717
|
var init_permissions_repository = __esm({
|
|
8434
8718
|
"src/server/repositories/permissions.repository.ts"() {
|
|
8435
8719
|
"use strict";
|
|
8436
8720
|
init_permissions();
|
|
8437
|
-
PermissionsRepository = class extends
|
|
8721
|
+
PermissionsRepository = class extends BaseRepository16 {
|
|
8438
8722
|
/**
|
|
8439
8723
|
* ID로 권한 조회
|
|
8440
8724
|
*/
|
|
8441
|
-
async findById(
|
|
8442
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
8725
|
+
async findById(id26) {
|
|
8726
|
+
const result = await this.readDb.select().from(permissions).where(eq16(permissions.id, id26)).limit(1);
|
|
8443
8727
|
return result[0] ?? null;
|
|
8444
8728
|
}
|
|
8445
8729
|
/**
|
|
8446
8730
|
* Name으로 권한 조회
|
|
8447
8731
|
*/
|
|
8448
8732
|
async findByName(name) {
|
|
8449
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
8733
|
+
const result = await this.readDb.select().from(permissions).where(eq16(permissions.name, name)).limit(1);
|
|
8450
8734
|
return result[0] ?? null;
|
|
8451
8735
|
}
|
|
8452
8736
|
/**
|
|
@@ -8454,7 +8738,7 @@ var init_permissions_repository = __esm({
|
|
|
8454
8738
|
*/
|
|
8455
8739
|
async findByNames(names) {
|
|
8456
8740
|
if (names.length === 0) return [];
|
|
8457
|
-
return this.readDb.select().from(permissions).where(
|
|
8741
|
+
return this.readDb.select().from(permissions).where(inArray3(permissions.name, names));
|
|
8458
8742
|
}
|
|
8459
8743
|
/**
|
|
8460
8744
|
* 모든 권한 조회
|
|
@@ -8466,13 +8750,13 @@ var init_permissions_repository = __esm({
|
|
|
8466
8750
|
* 활성 권한만 조회
|
|
8467
8751
|
*/
|
|
8468
8752
|
async findActive() {
|
|
8469
|
-
return this.readDb.select().from(permissions).where(
|
|
8753
|
+
return this.readDb.select().from(permissions).where(eq16(permissions.isActive, true)).orderBy(asc2(permissions.name));
|
|
8470
8754
|
}
|
|
8471
8755
|
/**
|
|
8472
8756
|
* 카테고리별 권한 조회
|
|
8473
8757
|
*/
|
|
8474
8758
|
async findByCategory(category) {
|
|
8475
|
-
return this.readDb.select().from(permissions).where(
|
|
8759
|
+
return this.readDb.select().from(permissions).where(eq16(permissions.category, category)).orderBy(asc2(permissions.name));
|
|
8476
8760
|
}
|
|
8477
8761
|
/**
|
|
8478
8762
|
* 권한 생성
|
|
@@ -8490,15 +8774,15 @@ var init_permissions_repository = __esm({
|
|
|
8490
8774
|
/**
|
|
8491
8775
|
* 권한 업데이트
|
|
8492
8776
|
*/
|
|
8493
|
-
async updateById(
|
|
8494
|
-
const result = await this.db.update(permissions).set(data).where(
|
|
8777
|
+
async updateById(id26, data) {
|
|
8778
|
+
const result = await this.db.update(permissions).set(data).where(eq16(permissions.id, id26)).returning();
|
|
8495
8779
|
return result[0] ?? null;
|
|
8496
8780
|
}
|
|
8497
8781
|
/**
|
|
8498
8782
|
* 권한 삭제
|
|
8499
8783
|
*/
|
|
8500
|
-
async deleteById(
|
|
8501
|
-
const result = await this.db.delete(permissions).where(
|
|
8784
|
+
async deleteById(id26) {
|
|
8785
|
+
const result = await this.db.delete(permissions).where(eq16(permissions.id, id26)).returning();
|
|
8502
8786
|
return result[0] ?? null;
|
|
8503
8787
|
}
|
|
8504
8788
|
};
|
|
@@ -8507,25 +8791,25 @@ var init_permissions_repository = __esm({
|
|
|
8507
8791
|
});
|
|
8508
8792
|
|
|
8509
8793
|
// src/server/repositories/role-permissions.repository.ts
|
|
8510
|
-
import { BaseRepository as
|
|
8511
|
-
import { and as
|
|
8794
|
+
import { BaseRepository as BaseRepository17 } from "@spfn/core/db";
|
|
8795
|
+
import { and as and15, eq as eq17 } from "drizzle-orm";
|
|
8512
8796
|
var RolePermissionsRepository, rolePermissionsRepository;
|
|
8513
8797
|
var init_role_permissions_repository = __esm({
|
|
8514
8798
|
"src/server/repositories/role-permissions.repository.ts"() {
|
|
8515
8799
|
"use strict";
|
|
8516
8800
|
init_role_permissions();
|
|
8517
|
-
RolePermissionsRepository = class extends
|
|
8801
|
+
RolePermissionsRepository = class extends BaseRepository17 {
|
|
8518
8802
|
/**
|
|
8519
8803
|
* 역할 ID로 모든 권한 조회
|
|
8520
8804
|
*/
|
|
8521
8805
|
async findByRoleId(roleId) {
|
|
8522
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
8806
|
+
return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.roleId, roleId));
|
|
8523
8807
|
}
|
|
8524
8808
|
/**
|
|
8525
8809
|
* 권한 ID로 모든 역할 조회
|
|
8526
8810
|
*/
|
|
8527
8811
|
async findByPermissionId(permissionId) {
|
|
8528
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
8812
|
+
return this.readDb.select().from(rolePermissions).where(eq17(rolePermissions.permissionId, permissionId));
|
|
8529
8813
|
}
|
|
8530
8814
|
/**
|
|
8531
8815
|
* 역할-권한 매핑 생성
|
|
@@ -8545,9 +8829,9 @@ var init_role_permissions_repository = __esm({
|
|
|
8545
8829
|
*/
|
|
8546
8830
|
async deleteByRoleIdAndPermissionId(roleId, permissionId) {
|
|
8547
8831
|
const result = await this.db.delete(rolePermissions).where(
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8832
|
+
and15(
|
|
8833
|
+
eq17(rolePermissions.roleId, roleId),
|
|
8834
|
+
eq17(rolePermissions.permissionId, permissionId)
|
|
8551
8835
|
)
|
|
8552
8836
|
).returning();
|
|
8553
8837
|
return result[0] ?? null;
|
|
@@ -8556,7 +8840,7 @@ var init_role_permissions_repository = __esm({
|
|
|
8556
8840
|
* 역할의 모든 권한 매핑 삭제
|
|
8557
8841
|
*/
|
|
8558
8842
|
async deleteByRoleId(roleId) {
|
|
8559
|
-
const result = await this.db.delete(rolePermissions).where(
|
|
8843
|
+
const result = await this.db.delete(rolePermissions).where(eq17(rolePermissions.roleId, roleId)).returning();
|
|
8560
8844
|
return result.length;
|
|
8561
8845
|
}
|
|
8562
8846
|
/**
|
|
@@ -8577,19 +8861,19 @@ var init_role_permissions_repository = __esm({
|
|
|
8577
8861
|
});
|
|
8578
8862
|
|
|
8579
8863
|
// src/server/repositories/user-permissions.repository.ts
|
|
8580
|
-
import { BaseRepository as
|
|
8581
|
-
import { eq as
|
|
8864
|
+
import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
|
|
8865
|
+
import { eq as eq18, and as and16, or as or4, isNull as isNull12, isNotNull as isNotNull3, lt as lt6, gt as gt8 } from "drizzle-orm";
|
|
8582
8866
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
8583
8867
|
var init_user_permissions_repository = __esm({
|
|
8584
8868
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
8585
8869
|
"use strict";
|
|
8586
8870
|
init_user_permissions();
|
|
8587
|
-
UserPermissionsRepository = class extends
|
|
8871
|
+
UserPermissionsRepository = class extends BaseRepository18 {
|
|
8588
8872
|
/**
|
|
8589
8873
|
* 사용자 ID로 모든 권한 오버라이드 조회
|
|
8590
8874
|
*/
|
|
8591
8875
|
async findByUserId(userId) {
|
|
8592
|
-
return this.readDb.select().from(userPermissions).where(
|
|
8876
|
+
return this.readDb.select().from(userPermissions).where(eq18(userPermissions.userId, userId));
|
|
8593
8877
|
}
|
|
8594
8878
|
/**
|
|
8595
8879
|
* 사용자 ID로 유효한 권한 오버라이드만 조회
|
|
@@ -8598,11 +8882,11 @@ var init_user_permissions_repository = __esm({
|
|
|
8598
8882
|
async findValidByUserId(userId) {
|
|
8599
8883
|
const now = /* @__PURE__ */ new Date();
|
|
8600
8884
|
return this.readDb.select().from(userPermissions).where(
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8885
|
+
and16(
|
|
8886
|
+
eq18(userPermissions.userId, userId),
|
|
8887
|
+
or4(
|
|
8888
|
+
isNull12(userPermissions.expiresAt),
|
|
8889
|
+
gt8(userPermissions.expiresAt, now)
|
|
8606
8890
|
)
|
|
8607
8891
|
)
|
|
8608
8892
|
);
|
|
@@ -8612,9 +8896,9 @@ var init_user_permissions_repository = __esm({
|
|
|
8612
8896
|
*/
|
|
8613
8897
|
async findByUserIdAndPermissionId(userId, permissionId) {
|
|
8614
8898
|
const result = await this.readDb.select().from(userPermissions).where(
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8899
|
+
and16(
|
|
8900
|
+
eq18(userPermissions.userId, userId),
|
|
8901
|
+
eq18(userPermissions.permissionId, permissionId)
|
|
8618
8902
|
)
|
|
8619
8903
|
).limit(1);
|
|
8620
8904
|
return result[0] ?? null;
|
|
@@ -8628,8 +8912,8 @@ var init_user_permissions_repository = __esm({
|
|
|
8628
8912
|
/**
|
|
8629
8913
|
* 사용자 권한 오버라이드 업데이트
|
|
8630
8914
|
*/
|
|
8631
|
-
async updateById(
|
|
8632
|
-
const result = await this.db.update(userPermissions).set(data).where(
|
|
8915
|
+
async updateById(id26, data) {
|
|
8916
|
+
const result = await this.db.update(userPermissions).set(data).where(eq18(userPermissions.id, id26)).returning();
|
|
8633
8917
|
return result[0] ?? null;
|
|
8634
8918
|
}
|
|
8635
8919
|
/**
|
|
@@ -8637,9 +8921,9 @@ var init_user_permissions_repository = __esm({
|
|
|
8637
8921
|
*/
|
|
8638
8922
|
async deleteByUserIdAndPermissionId(userId, permissionId) {
|
|
8639
8923
|
const result = await this.db.delete(userPermissions).where(
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8924
|
+
and16(
|
|
8925
|
+
eq18(userPermissions.userId, userId),
|
|
8926
|
+
eq18(userPermissions.permissionId, permissionId)
|
|
8643
8927
|
)
|
|
8644
8928
|
).returning();
|
|
8645
8929
|
return result[0] ?? null;
|
|
@@ -8648,7 +8932,7 @@ var init_user_permissions_repository = __esm({
|
|
|
8648
8932
|
* 사용자의 모든 권한 오버라이드 삭제
|
|
8649
8933
|
*/
|
|
8650
8934
|
async deleteByUserId(userId) {
|
|
8651
|
-
const result = await this.db.delete(userPermissions).where(
|
|
8935
|
+
const result = await this.db.delete(userPermissions).where(eq18(userPermissions.userId, userId)).returning();
|
|
8652
8936
|
return result.length;
|
|
8653
8937
|
}
|
|
8654
8938
|
/**
|
|
@@ -8657,8 +8941,8 @@ var init_user_permissions_repository = __esm({
|
|
|
8657
8941
|
async deleteExpired() {
|
|
8658
8942
|
const now = /* @__PURE__ */ new Date();
|
|
8659
8943
|
const result = await this.db.delete(userPermissions).where(
|
|
8660
|
-
|
|
8661
|
-
|
|
8944
|
+
and16(
|
|
8945
|
+
isNotNull3(userPermissions.expiresAt),
|
|
8662
8946
|
lt6(userPermissions.expiresAt, now)
|
|
8663
8947
|
)
|
|
8664
8948
|
).returning();
|
|
@@ -8670,33 +8954,33 @@ var init_user_permissions_repository = __esm({
|
|
|
8670
8954
|
});
|
|
8671
8955
|
|
|
8672
8956
|
// src/server/repositories/user-profiles.repository.ts
|
|
8673
|
-
import { BaseRepository as
|
|
8674
|
-
import { eq as
|
|
8957
|
+
import { BaseRepository as BaseRepository19 } from "@spfn/core/db";
|
|
8958
|
+
import { eq as eq19 } from "drizzle-orm";
|
|
8675
8959
|
var UserProfilesRepository, userProfilesRepository;
|
|
8676
8960
|
var init_user_profiles_repository = __esm({
|
|
8677
8961
|
"src/server/repositories/user-profiles.repository.ts"() {
|
|
8678
8962
|
"use strict";
|
|
8679
8963
|
init_user_profiles();
|
|
8680
|
-
UserProfilesRepository = class extends
|
|
8964
|
+
UserProfilesRepository = class extends BaseRepository19 {
|
|
8681
8965
|
/**
|
|
8682
8966
|
* ID로 프로필 조회
|
|
8683
8967
|
*/
|
|
8684
|
-
async findById(
|
|
8685
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
8968
|
+
async findById(id26) {
|
|
8969
|
+
const result = await this.readDb.select().from(userProfiles).where(eq19(userProfiles.id, id26)).limit(1);
|
|
8686
8970
|
return result[0] ?? null;
|
|
8687
8971
|
}
|
|
8688
8972
|
/**
|
|
8689
8973
|
* User ID로 locale만 조회 (경량)
|
|
8690
8974
|
*/
|
|
8691
8975
|
async findLocaleByUserId(userId) {
|
|
8692
|
-
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(
|
|
8976
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1);
|
|
8693
8977
|
return result[0]?.locale || "en";
|
|
8694
8978
|
}
|
|
8695
8979
|
/**
|
|
8696
8980
|
* User ID로 프로필 조회
|
|
8697
8981
|
*/
|
|
8698
8982
|
async findByUserId(userId) {
|
|
8699
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
8983
|
+
const result = await this.readDb.select().from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1);
|
|
8700
8984
|
return result[0] ?? null;
|
|
8701
8985
|
}
|
|
8702
8986
|
/**
|
|
@@ -8708,29 +8992,29 @@ var init_user_profiles_repository = __esm({
|
|
|
8708
8992
|
/**
|
|
8709
8993
|
* 프로필 업데이트 (by ID)
|
|
8710
8994
|
*/
|
|
8711
|
-
async updateById(
|
|
8712
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
8995
|
+
async updateById(id26, data) {
|
|
8996
|
+
const result = await this.db.update(userProfiles).set(data).where(eq19(userProfiles.id, id26)).returning();
|
|
8713
8997
|
return result[0] ?? null;
|
|
8714
8998
|
}
|
|
8715
8999
|
/**
|
|
8716
9000
|
* 프로필 업데이트 (by User ID)
|
|
8717
9001
|
*/
|
|
8718
9002
|
async updateByUserId(userId, data) {
|
|
8719
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
9003
|
+
const result = await this.db.update(userProfiles).set(data).where(eq19(userProfiles.userId, userId)).returning();
|
|
8720
9004
|
return result[0] ?? null;
|
|
8721
9005
|
}
|
|
8722
9006
|
/**
|
|
8723
9007
|
* 프로필 삭제 (by ID)
|
|
8724
9008
|
*/
|
|
8725
|
-
async deleteById(
|
|
8726
|
-
const result = await this.db.delete(userProfiles).where(
|
|
9009
|
+
async deleteById(id26) {
|
|
9010
|
+
const result = await this.db.delete(userProfiles).where(eq19(userProfiles.id, id26)).returning();
|
|
8727
9011
|
return result[0] ?? null;
|
|
8728
9012
|
}
|
|
8729
9013
|
/**
|
|
8730
9014
|
* 프로필 삭제 (by User ID)
|
|
8731
9015
|
*/
|
|
8732
9016
|
async deleteByUserId(userId) {
|
|
8733
|
-
const result = await this.db.delete(userProfiles).where(
|
|
9017
|
+
const result = await this.db.delete(userProfiles).where(eq19(userProfiles.userId, userId)).returning();
|
|
8734
9018
|
return result[0] ?? null;
|
|
8735
9019
|
}
|
|
8736
9020
|
/**
|
|
@@ -8772,7 +9056,7 @@ var init_user_profiles_repository = __esm({
|
|
|
8772
9056
|
metadata: userProfiles.metadata,
|
|
8773
9057
|
createdAt: userProfiles.createdAt,
|
|
8774
9058
|
updatedAt: userProfiles.updatedAt
|
|
8775
|
-
}).from(userProfiles).where(
|
|
9059
|
+
}).from(userProfiles).where(eq19(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
8776
9060
|
if (!profile) {
|
|
8777
9061
|
return null;
|
|
8778
9062
|
}
|
|
@@ -8800,8 +9084,8 @@ var init_user_profiles_repository = __esm({
|
|
|
8800
9084
|
});
|
|
8801
9085
|
|
|
8802
9086
|
// src/server/repositories/invitations.repository.ts
|
|
8803
|
-
import { eq as
|
|
8804
|
-
import { BaseRepository as
|
|
9087
|
+
import { eq as eq20, and as and17, lt as lt7, desc as desc3, sql as sql10 } from "drizzle-orm";
|
|
9088
|
+
import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
|
|
8805
9089
|
var InvitationsRepository, invitationsRepository;
|
|
8806
9090
|
var init_invitations_repository = __esm({
|
|
8807
9091
|
"src/server/repositories/invitations.repository.ts"() {
|
|
@@ -8810,19 +9094,19 @@ var init_invitations_repository = __esm({
|
|
|
8810
9094
|
init_roles();
|
|
8811
9095
|
init_user_invitations();
|
|
8812
9096
|
init_email();
|
|
8813
|
-
InvitationsRepository = class extends
|
|
9097
|
+
InvitationsRepository = class extends BaseRepository20 {
|
|
8814
9098
|
/**
|
|
8815
9099
|
* ID로 초대 조회
|
|
8816
9100
|
*/
|
|
8817
|
-
async findById(
|
|
8818
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
9101
|
+
async findById(id26) {
|
|
9102
|
+
const result = await this.readDb.select().from(userInvitations).where(eq20(userInvitations.id, id26)).limit(1);
|
|
8819
9103
|
return result[0] ?? null;
|
|
8820
9104
|
}
|
|
8821
9105
|
/**
|
|
8822
9106
|
* Token으로 초대 조회
|
|
8823
9107
|
*/
|
|
8824
9108
|
async findByToken(token) {
|
|
8825
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
9109
|
+
const result = await this.readDb.select().from(userInvitations).where(eq20(userInvitations.token, token)).limit(1);
|
|
8826
9110
|
return result[0] ?? null;
|
|
8827
9111
|
}
|
|
8828
9112
|
/**
|
|
@@ -8830,9 +9114,9 @@ var init_invitations_repository = __esm({
|
|
|
8830
9114
|
*/
|
|
8831
9115
|
async findPendingByEmail(email) {
|
|
8832
9116
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
9117
|
+
and17(
|
|
9118
|
+
eq20(userInvitations.email, normalizeEmail(email)),
|
|
9119
|
+
eq20(userInvitations.status, "pending")
|
|
8836
9120
|
)
|
|
8837
9121
|
).limit(1);
|
|
8838
9122
|
return result[0] ?? null;
|
|
@@ -8841,13 +9125,13 @@ var init_invitations_repository = __esm({
|
|
|
8841
9125
|
* 초대자 ID로 모든 초대 조회
|
|
8842
9126
|
*/
|
|
8843
9127
|
async findByInvitedBy(invitedBy) {
|
|
8844
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9128
|
+
return this.readDb.select().from(userInvitations).where(eq20(userInvitations.invitedBy, invitedBy));
|
|
8845
9129
|
}
|
|
8846
9130
|
/**
|
|
8847
9131
|
* 상태별 초대 조회
|
|
8848
9132
|
*/
|
|
8849
9133
|
async findByStatus(status) {
|
|
8850
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9134
|
+
return this.readDb.select().from(userInvitations).where(eq20(userInvitations.status, status));
|
|
8851
9135
|
}
|
|
8852
9136
|
/**
|
|
8853
9137
|
* 초대 생성
|
|
@@ -8858,7 +9142,7 @@ var init_invitations_repository = __esm({
|
|
|
8858
9142
|
/**
|
|
8859
9143
|
* 초대 상태 업데이트
|
|
8860
9144
|
*/
|
|
8861
|
-
async updateStatus(
|
|
9145
|
+
async updateStatus(id26, status, timestamp2) {
|
|
8862
9146
|
const updates = {
|
|
8863
9147
|
status
|
|
8864
9148
|
};
|
|
@@ -8869,14 +9153,14 @@ var init_invitations_repository = __esm({
|
|
|
8869
9153
|
updates.cancelledAt = timestamp2;
|
|
8870
9154
|
}
|
|
8871
9155
|
}
|
|
8872
|
-
const result = await this.db.update(userInvitations).set(updates).where(
|
|
9156
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq20(userInvitations.id, id26)).returning();
|
|
8873
9157
|
return result[0] ?? null;
|
|
8874
9158
|
}
|
|
8875
9159
|
/**
|
|
8876
9160
|
* 초대 삭제
|
|
8877
9161
|
*/
|
|
8878
|
-
async deleteById(
|
|
8879
|
-
const result = await this.db.delete(userInvitations).where(
|
|
9162
|
+
async deleteById(id26) {
|
|
9163
|
+
const result = await this.db.delete(userInvitations).where(eq20(userInvitations.id, id26)).returning();
|
|
8880
9164
|
return result[0] ?? null;
|
|
8881
9165
|
}
|
|
8882
9166
|
/**
|
|
@@ -8885,8 +9169,8 @@ var init_invitations_repository = __esm({
|
|
|
8885
9169
|
async updateExpiredInvitations() {
|
|
8886
9170
|
const now = /* @__PURE__ */ new Date();
|
|
8887
9171
|
const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
|
|
8888
|
-
|
|
8889
|
-
|
|
9172
|
+
and17(
|
|
9173
|
+
eq20(userInvitations.status, "pending"),
|
|
8890
9174
|
lt7(userInvitations.expiresAt, now)
|
|
8891
9175
|
)
|
|
8892
9176
|
).returning();
|
|
@@ -8918,7 +9202,7 @@ var init_invitations_repository = __esm({
|
|
|
8918
9202
|
id: users.id,
|
|
8919
9203
|
email: users.email
|
|
8920
9204
|
}
|
|
8921
|
-
}).from(userInvitations).innerJoin(roles,
|
|
9205
|
+
}).from(userInvitations).innerJoin(roles, eq20(userInvitations.roleId, roles.id)).innerJoin(users, eq20(userInvitations.invitedBy, users.id)).where(eq20(userInvitations.token, token)).limit(1);
|
|
8922
9206
|
return result[0] ?? null;
|
|
8923
9207
|
}
|
|
8924
9208
|
/**
|
|
@@ -8929,13 +9213,13 @@ var init_invitations_repository = __esm({
|
|
|
8929
9213
|
const offset = (page - 1) * limit;
|
|
8930
9214
|
const conditions = [];
|
|
8931
9215
|
if (status) {
|
|
8932
|
-
conditions.push(
|
|
9216
|
+
conditions.push(eq20(userInvitations.status, status));
|
|
8933
9217
|
}
|
|
8934
9218
|
if (invitedBy) {
|
|
8935
|
-
conditions.push(
|
|
9219
|
+
conditions.push(eq20(userInvitations.invitedBy, invitedBy));
|
|
8936
9220
|
}
|
|
8937
|
-
const whereClause = conditions.length > 0 ?
|
|
8938
|
-
const countResult = await this.readDb.select({ count:
|
|
9221
|
+
const whereClause = conditions.length > 0 ? and17(...conditions) : void 0;
|
|
9222
|
+
const countResult = await this.readDb.select({ count: sql10`count(*)` }).from(userInvitations).where(whereClause);
|
|
8939
9223
|
const total = Number(countResult[0]?.count || 0);
|
|
8940
9224
|
const results = await this.readDb.select({
|
|
8941
9225
|
id: userInvitations.id,
|
|
@@ -8959,7 +9243,7 @@ var init_invitations_repository = __esm({
|
|
|
8959
9243
|
id: users.id,
|
|
8960
9244
|
email: users.email
|
|
8961
9245
|
}
|
|
8962
|
-
}).from(userInvitations).innerJoin(roles,
|
|
9246
|
+
}).from(userInvitations).innerJoin(roles, eq20(userInvitations.roleId, roles.id)).innerJoin(users, eq20(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc3(userInvitations.createdAt)).limit(limit).offset(offset);
|
|
8963
9247
|
return {
|
|
8964
9248
|
invitations: results,
|
|
8965
9249
|
total,
|
|
@@ -8971,31 +9255,31 @@ var init_invitations_repository = __esm({
|
|
|
8971
9255
|
/**
|
|
8972
9256
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
8973
9257
|
*/
|
|
8974
|
-
async updateById(
|
|
9258
|
+
async updateById(id26, data) {
|
|
8975
9259
|
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
8976
|
-
const result = await this.db.update(userInvitations).set(patch).where(
|
|
9260
|
+
const result = await this.db.update(userInvitations).set(patch).where(eq20(userInvitations.id, id26)).returning();
|
|
8977
9261
|
return result[0] ?? null;
|
|
8978
9262
|
}
|
|
8979
9263
|
/**
|
|
8980
9264
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
8981
9265
|
*/
|
|
8982
|
-
async resend(
|
|
9266
|
+
async resend(id26, newExpiresAt) {
|
|
8983
9267
|
const result = await this.db.update(userInvitations).set({
|
|
8984
9268
|
status: "pending",
|
|
8985
9269
|
expiresAt: newExpiresAt
|
|
8986
|
-
}).where(
|
|
9270
|
+
}).where(eq20(userInvitations.id, id26)).returning();
|
|
8987
9271
|
return result[0] ?? null;
|
|
8988
9272
|
}
|
|
8989
9273
|
/**
|
|
8990
9274
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
8991
9275
|
*/
|
|
8992
|
-
async cancel(
|
|
9276
|
+
async cancel(id26, cancelledBy, reason, currentMetadata) {
|
|
8993
9277
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
8994
9278
|
const result = await this.db.update(userInvitations).set({
|
|
8995
9279
|
status: "cancelled",
|
|
8996
9280
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
8997
9281
|
metadata: newMetadata
|
|
8998
|
-
}).where(
|
|
9282
|
+
}).where(eq20(userInvitations.id, id26)).returning();
|
|
8999
9283
|
return result[0] ?? null;
|
|
9000
9284
|
}
|
|
9001
9285
|
};
|
|
@@ -9366,6 +9650,22 @@ var init_schema5 = __esm({
|
|
|
9366
9650
|
examples: [5, 10, 30]
|
|
9367
9651
|
})
|
|
9368
9652
|
},
|
|
9653
|
+
SPFN_AUTH_MFA_CONFIRM_PATH: {
|
|
9654
|
+
...envString({
|
|
9655
|
+
description: "App page that asks for the second factor after a 202 sign-in, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. createOAuthCallbackHandler redirects the browser there with ?challenge= when a social sign-in needs a step-up; the page posts that challenge and a code to /_auth/mfa/verify. It is a page in your app, not an API route.",
|
|
9656
|
+
default: "/auth/mfa",
|
|
9657
|
+
required: false,
|
|
9658
|
+
examples: ["/auth/mfa", "/sign-in/two-factor"]
|
|
9659
|
+
})
|
|
9660
|
+
},
|
|
9661
|
+
SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES: {
|
|
9662
|
+
...envNumber({
|
|
9663
|
+
description: "How long a new-device second-factor challenge stays spendable. A sign-in on an enrolled account from a device it has never seen answers 202 with a challenge instead of a session, and the key it registered is inactive until POST /_auth/mfa/verify spends it. This is how long the person has to reach for their authenticator \u2014 and how long an attacker holding only the password has. The Next.js proxy seals its pending cookie for the same span.",
|
|
9664
|
+
default: 10,
|
|
9665
|
+
required: false,
|
|
9666
|
+
examples: [5, 10, 15]
|
|
9667
|
+
})
|
|
9668
|
+
},
|
|
9369
9669
|
// ============================================================================
|
|
9370
9670
|
// Session binding (#97)
|
|
9371
9671
|
// ============================================================================
|
|
@@ -9819,15 +10119,15 @@ var init_token_cipher = __esm({
|
|
|
9819
10119
|
});
|
|
9820
10120
|
|
|
9821
10121
|
// src/server/repositories/social-accounts.repository.ts
|
|
9822
|
-
import { eq as
|
|
9823
|
-
import { BaseRepository as
|
|
10122
|
+
import { eq as eq21, and as and18 } from "drizzle-orm";
|
|
10123
|
+
import { BaseRepository as BaseRepository21 } from "@spfn/core/db";
|
|
9824
10124
|
var SocialAccountsRepository, socialAccountsRepository;
|
|
9825
10125
|
var init_social_accounts_repository = __esm({
|
|
9826
10126
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
9827
10127
|
"use strict";
|
|
9828
10128
|
init_entities();
|
|
9829
10129
|
init_token_cipher();
|
|
9830
|
-
SocialAccountsRepository = class extends
|
|
10130
|
+
SocialAccountsRepository = class extends BaseRepository21 {
|
|
9831
10131
|
/**
|
|
9832
10132
|
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
9833
10133
|
*
|
|
@@ -9855,10 +10155,10 @@ var init_social_accounts_repository = __esm({
|
|
|
9855
10155
|
if (refresh?.needsRotation) {
|
|
9856
10156
|
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
9857
10157
|
}
|
|
9858
|
-
await this.db.update(userSocialAccounts).set(heal).where(
|
|
9859
|
-
|
|
9860
|
-
access?.needsRotation && account.accessToken !== null ?
|
|
9861
|
-
refresh?.needsRotation && account.refreshToken !== null ?
|
|
10158
|
+
await this.db.update(userSocialAccounts).set(heal).where(and18(
|
|
10159
|
+
eq21(userSocialAccounts.id, account.id),
|
|
10160
|
+
access?.needsRotation && account.accessToken !== null ? eq21(userSocialAccounts.accessToken, account.accessToken) : void 0,
|
|
10161
|
+
refresh?.needsRotation && account.refreshToken !== null ? eq21(userSocialAccounts.refreshToken, account.refreshToken) : void 0
|
|
9862
10162
|
));
|
|
9863
10163
|
} catch {
|
|
9864
10164
|
}
|
|
@@ -9875,9 +10175,9 @@ var init_social_accounts_repository = __esm({
|
|
|
9875
10175
|
*/
|
|
9876
10176
|
async findByProviderAndProviderId(provider, providerUserId) {
|
|
9877
10177
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
10178
|
+
and18(
|
|
10179
|
+
eq21(userSocialAccounts.provider, provider),
|
|
10180
|
+
eq21(userSocialAccounts.providerUserId, providerUserId)
|
|
9881
10181
|
)
|
|
9882
10182
|
).limit(1);
|
|
9883
10183
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -9887,7 +10187,7 @@ var init_social_accounts_repository = __esm({
|
|
|
9887
10187
|
* Read replica 사용
|
|
9888
10188
|
*/
|
|
9889
10189
|
async findByUserId(userId) {
|
|
9890
|
-
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10190
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq21(userSocialAccounts.userId, userId));
|
|
9891
10191
|
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
9892
10192
|
}
|
|
9893
10193
|
/**
|
|
@@ -9896,9 +10196,9 @@ var init_social_accounts_repository = __esm({
|
|
|
9896
10196
|
*/
|
|
9897
10197
|
async findByUserIdAndProvider(userId, provider) {
|
|
9898
10198
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
10199
|
+
and18(
|
|
10200
|
+
eq21(userSocialAccounts.userId, userId),
|
|
10201
|
+
eq21(userSocialAccounts.provider, provider)
|
|
9902
10202
|
)
|
|
9903
10203
|
).limit(1);
|
|
9904
10204
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -9924,11 +10224,11 @@ var init_social_accounts_repository = __esm({
|
|
|
9924
10224
|
* 토큰 정보 업데이트
|
|
9925
10225
|
* Write primary 사용
|
|
9926
10226
|
*/
|
|
9927
|
-
async updateTokens(
|
|
10227
|
+
async updateTokens(id26, data) {
|
|
9928
10228
|
const accounts = await this.db.select({
|
|
9929
10229
|
provider: userSocialAccounts.provider,
|
|
9930
10230
|
providerUserId: userSocialAccounts.providerUserId
|
|
9931
|
-
}).from(userSocialAccounts).where(
|
|
10231
|
+
}).from(userSocialAccounts).where(eq21(userSocialAccounts.id, id26)).limit(1);
|
|
9932
10232
|
const account = accounts[0];
|
|
9933
10233
|
if (!account) {
|
|
9934
10234
|
return null;
|
|
@@ -9942,15 +10242,15 @@ var init_social_accounts_repository = __esm({
|
|
|
9942
10242
|
...data,
|
|
9943
10243
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
9944
10244
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
9945
|
-
}).where(
|
|
10245
|
+
}).where(eq21(userSocialAccounts.id, id26)).returning();
|
|
9946
10246
|
return this.decryptAccount(result[0] ?? null);
|
|
9947
10247
|
}
|
|
9948
10248
|
/**
|
|
9949
10249
|
* 소셜 계정 삭제
|
|
9950
10250
|
* Write primary 사용
|
|
9951
10251
|
*/
|
|
9952
|
-
async deleteById(
|
|
9953
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
10252
|
+
async deleteById(id26) {
|
|
10253
|
+
const result = await this.db.delete(userSocialAccounts).where(eq21(userSocialAccounts.id, id26)).returning();
|
|
9954
10254
|
return result[0] ?? null;
|
|
9955
10255
|
}
|
|
9956
10256
|
/**
|
|
@@ -9959,9 +10259,9 @@ var init_social_accounts_repository = __esm({
|
|
|
9959
10259
|
*/
|
|
9960
10260
|
async deleteByUserIdAndProvider(userId, provider) {
|
|
9961
10261
|
const result = await this.db.delete(userSocialAccounts).where(
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
10262
|
+
and18(
|
|
10263
|
+
eq21(userSocialAccounts.userId, userId),
|
|
10264
|
+
eq21(userSocialAccounts.provider, provider)
|
|
9965
10265
|
)
|
|
9966
10266
|
).returning();
|
|
9967
10267
|
return result[0] ?? null;
|
|
@@ -9974,7 +10274,7 @@ var init_social_accounts_repository = __esm({
|
|
|
9974
10274
|
* Write primary 사용
|
|
9975
10275
|
*/
|
|
9976
10276
|
async deleteAllByUserId(userId) {
|
|
9977
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
10277
|
+
const result = await this.db.delete(userSocialAccounts).where(eq21(userSocialAccounts.userId, userId)).returning();
|
|
9978
10278
|
return result.length;
|
|
9979
10279
|
}
|
|
9980
10280
|
};
|
|
@@ -9983,19 +10283,19 @@ var init_social_accounts_repository = __esm({
|
|
|
9983
10283
|
});
|
|
9984
10284
|
|
|
9985
10285
|
// src/server/repositories/auth-metadata.repository.ts
|
|
9986
|
-
import { BaseRepository as
|
|
9987
|
-
import { eq as
|
|
10286
|
+
import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
|
|
10287
|
+
import { eq as eq22 } from "drizzle-orm";
|
|
9988
10288
|
var AuthMetadataRepository, authMetadataRepository;
|
|
9989
10289
|
var init_auth_metadata_repository = __esm({
|
|
9990
10290
|
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
9991
10291
|
"use strict";
|
|
9992
10292
|
init_auth_metadata();
|
|
9993
|
-
AuthMetadataRepository = class extends
|
|
10293
|
+
AuthMetadataRepository = class extends BaseRepository22 {
|
|
9994
10294
|
/**
|
|
9995
10295
|
* 키로 값 조회
|
|
9996
10296
|
*/
|
|
9997
10297
|
async get(key) {
|
|
9998
|
-
const result = await this.readDb.select().from(authMetadata).where(
|
|
10298
|
+
const result = await this.readDb.select().from(authMetadata).where(eq22(authMetadata.key, key)).limit(1);
|
|
9999
10299
|
return result[0]?.value ?? null;
|
|
10000
10300
|
}
|
|
10001
10301
|
/**
|
|
@@ -10018,20 +10318,20 @@ var init_auth_metadata_repository = __esm({
|
|
|
10018
10318
|
});
|
|
10019
10319
|
|
|
10020
10320
|
// src/server/repositories/account-deletion-requests.repository.ts
|
|
10021
|
-
import { eq as
|
|
10022
|
-
import { BaseRepository as
|
|
10321
|
+
import { eq as eq23, and as and19, lte as lte2 } from "drizzle-orm";
|
|
10322
|
+
import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
|
|
10023
10323
|
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
10024
10324
|
var init_account_deletion_requests_repository = __esm({
|
|
10025
10325
|
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
10026
10326
|
"use strict";
|
|
10027
10327
|
init_account_deletion_requests();
|
|
10028
|
-
AccountDeletionRequestsRepository = class extends
|
|
10328
|
+
AccountDeletionRequestsRepository = class extends BaseRepository23 {
|
|
10029
10329
|
/**
|
|
10030
10330
|
* ID로 요청 조회
|
|
10031
10331
|
* Read replica 사용
|
|
10032
10332
|
*/
|
|
10033
|
-
async findById(
|
|
10034
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
10333
|
+
async findById(id26) {
|
|
10334
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq23(accountDeletionRequests.id, id26)).limit(1);
|
|
10035
10335
|
return result[0] ?? null;
|
|
10036
10336
|
}
|
|
10037
10337
|
/**
|
|
@@ -10040,9 +10340,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10040
10340
|
*/
|
|
10041
10341
|
async findPendingByUserId(userId) {
|
|
10042
10342
|
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10343
|
+
and19(
|
|
10344
|
+
eq23(accountDeletionRequests.userId, userId),
|
|
10345
|
+
eq23(accountDeletionRequests.status, "pending")
|
|
10046
10346
|
)
|
|
10047
10347
|
).limit(1);
|
|
10048
10348
|
return result[0] ?? null;
|
|
@@ -10056,9 +10356,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10056
10356
|
*/
|
|
10057
10357
|
async findPendingByUserIdOnPrimary(userId) {
|
|
10058
10358
|
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10359
|
+
and19(
|
|
10360
|
+
eq23(accountDeletionRequests.userId, userId),
|
|
10361
|
+
eq23(accountDeletionRequests.status, "pending")
|
|
10062
10362
|
)
|
|
10063
10363
|
).limit(1);
|
|
10064
10364
|
return result[0] ?? null;
|
|
@@ -10069,9 +10369,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10069
10369
|
*/
|
|
10070
10370
|
async findDueForPurge(now) {
|
|
10071
10371
|
return this.readDb.select().from(accountDeletionRequests).where(
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10372
|
+
and19(
|
|
10373
|
+
eq23(accountDeletionRequests.status, "pending"),
|
|
10374
|
+
lte2(accountDeletionRequests.purgeScheduledAt, now)
|
|
10075
10375
|
)
|
|
10076
10376
|
);
|
|
10077
10377
|
}
|
|
@@ -10090,14 +10390,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10090
10390
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
10091
10391
|
* Write primary 사용
|
|
10092
10392
|
*/
|
|
10093
|
-
async markCancelled(
|
|
10393
|
+
async markCancelled(id26) {
|
|
10094
10394
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10095
10395
|
status: "cancelled",
|
|
10096
10396
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
10097
10397
|
}).where(
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10398
|
+
and19(
|
|
10399
|
+
eq23(accountDeletionRequests.id, id26),
|
|
10400
|
+
eq23(accountDeletionRequests.status, "pending")
|
|
10101
10401
|
)
|
|
10102
10402
|
).returning();
|
|
10103
10403
|
return result[0] ?? null;
|
|
@@ -10112,15 +10412,15 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10112
10412
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
10113
10413
|
* Write primary 사용
|
|
10114
10414
|
*/
|
|
10115
|
-
async markCompleted(
|
|
10415
|
+
async markCompleted(id26, purgeStrategy) {
|
|
10116
10416
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10117
10417
|
status: "completed",
|
|
10118
10418
|
completedAt: /* @__PURE__ */ new Date(),
|
|
10119
10419
|
purgeStrategy
|
|
10120
10420
|
}).where(
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10421
|
+
and19(
|
|
10422
|
+
eq23(accountDeletionRequests.id, id26),
|
|
10423
|
+
eq23(accountDeletionRequests.status, "pending")
|
|
10124
10424
|
)
|
|
10125
10425
|
).returning();
|
|
10126
10426
|
return result[0] ?? null;
|
|
@@ -10131,14 +10431,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10131
10431
|
});
|
|
10132
10432
|
|
|
10133
10433
|
// src/server/repositories/ops-tokens.repository.ts
|
|
10134
|
-
import { and as
|
|
10135
|
-
import { BaseRepository as
|
|
10434
|
+
import { and as and20, desc as desc4, eq as eq24, isNull as isNull13 } from "drizzle-orm";
|
|
10435
|
+
import { BaseRepository as BaseRepository24 } from "@spfn/core/db";
|
|
10136
10436
|
var OpsTokensRepository, opsTokensRepository;
|
|
10137
10437
|
var init_ops_tokens_repository = __esm({
|
|
10138
10438
|
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
10139
10439
|
"use strict";
|
|
10140
10440
|
init_ops_tokens();
|
|
10141
|
-
OpsTokensRepository = class extends
|
|
10441
|
+
OpsTokensRepository = class extends BaseRepository24 {
|
|
10142
10442
|
/**
|
|
10143
10443
|
* Lookup by the secret's hash — the verification path.
|
|
10144
10444
|
*
|
|
@@ -10148,7 +10448,7 @@ var init_ops_tokens_repository = __esm({
|
|
|
10148
10448
|
* and revocation is documented as taking effect immediately.
|
|
10149
10449
|
*/
|
|
10150
10450
|
async findByTokenHash(tokenHash) {
|
|
10151
|
-
const result = await this.db.select().from(opsTokens).where(
|
|
10451
|
+
const result = await this.db.select().from(opsTokens).where(eq24(opsTokens.tokenHash, tokenHash)).limit(1);
|
|
10152
10452
|
return result[0] ?? null;
|
|
10153
10453
|
}
|
|
10154
10454
|
async create(data) {
|
|
@@ -10163,13 +10463,13 @@ var init_ops_tokens_repository = __esm({
|
|
|
10163
10463
|
* token is already revoked — the first revocation's timestamp is never
|
|
10164
10464
|
* overwritten.
|
|
10165
10465
|
*/
|
|
10166
|
-
async revokeById(
|
|
10167
|
-
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10466
|
+
async revokeById(id26) {
|
|
10467
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and20(eq24(opsTokens.id, id26), isNull13(opsTokens.revokedAt))).returning();
|
|
10168
10468
|
return result[0] ?? null;
|
|
10169
10469
|
}
|
|
10170
10470
|
/** Fire-and-forget from the verification path. */
|
|
10171
|
-
async updateLastUsedById(
|
|
10172
|
-
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
10471
|
+
async updateLastUsedById(id26) {
|
|
10472
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq24(opsTokens.id, id26));
|
|
10173
10473
|
}
|
|
10174
10474
|
};
|
|
10175
10475
|
opsTokensRepository = new OpsTokensRepository();
|
|
@@ -10177,15 +10477,15 @@ var init_ops_tokens_repository = __esm({
|
|
|
10177
10477
|
});
|
|
10178
10478
|
|
|
10179
10479
|
// src/server/repositories/oauth2-clients.repository.ts
|
|
10180
|
-
import { and as
|
|
10181
|
-
import { BaseRepository as
|
|
10480
|
+
import { and as and21, eq as eq25, gt as gt9, lt as lt8, sql as sql11 } from "drizzle-orm";
|
|
10481
|
+
import { BaseRepository as BaseRepository25, runInTransaction } from "@spfn/core/db";
|
|
10182
10482
|
var OAuth2ClientsRepository, oauth2ClientsRepository;
|
|
10183
10483
|
var init_oauth2_clients_repository = __esm({
|
|
10184
10484
|
"src/server/repositories/oauth2-clients.repository.ts"() {
|
|
10185
10485
|
"use strict";
|
|
10186
10486
|
init_oauth2_clients();
|
|
10187
10487
|
init_oauth2_grants();
|
|
10188
|
-
OAuth2ClientsRepository = class extends
|
|
10488
|
+
OAuth2ClientsRepository = class extends BaseRepository25 {
|
|
10189
10489
|
async create(data) {
|
|
10190
10490
|
const result = await this.db.insert(oauth2Clients).values(data).returning();
|
|
10191
10491
|
return result[0];
|
|
@@ -10199,12 +10499,12 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10199
10499
|
* at the one moment a CLI is being connected.
|
|
10200
10500
|
*/
|
|
10201
10501
|
async findByClientId(clientId) {
|
|
10202
|
-
const result = await this.db.select().from(oauth2Clients).where(
|
|
10502
|
+
const result = await this.db.select().from(oauth2Clients).where(eq25(oauth2Clients.clientId, clientId)).limit(1);
|
|
10203
10503
|
return result[0] ?? null;
|
|
10204
10504
|
}
|
|
10205
10505
|
/** Fire-and-forget from the token-issuing path. */
|
|
10206
|
-
async updateLastUsedById(
|
|
10207
|
-
await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
10506
|
+
async updateLastUsedById(id26) {
|
|
10507
|
+
await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq25(oauth2Clients.id, id26));
|
|
10208
10508
|
}
|
|
10209
10509
|
/**
|
|
10210
10510
|
* Register a client unless this address is already at its standing cap.
|
|
@@ -10226,7 +10526,7 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10226
10526
|
*/
|
|
10227
10527
|
async createWithinStandingCap(data, limit) {
|
|
10228
10528
|
return await runInTransaction(async () => {
|
|
10229
|
-
await this.db.execute(
|
|
10529
|
+
await this.db.execute(sql11`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
|
|
10230
10530
|
const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
|
|
10231
10531
|
return standing < limit.max ? await this.create(data) : null;
|
|
10232
10532
|
});
|
|
@@ -10248,11 +10548,11 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10248
10548
|
* count exists to see.
|
|
10249
10549
|
*/
|
|
10250
10550
|
async countRecentUngrantedByIp(ip, windowMs) {
|
|
10251
|
-
const result = await this.db.select({ count:
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10551
|
+
const result = await this.db.select({ count: sql11`count(*)::int` }).from(oauth2Clients).where(
|
|
10552
|
+
and21(
|
|
10553
|
+
eq25(oauth2Clients.createdIp, ip),
|
|
10554
|
+
gt9(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
|
|
10555
|
+
sql11`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
10256
10556
|
)
|
|
10257
10557
|
);
|
|
10258
10558
|
return result[0]?.count ?? 0;
|
|
@@ -10269,9 +10569,9 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10269
10569
|
*/
|
|
10270
10570
|
async deleteStaleUngranted(before) {
|
|
10271
10571
|
const deleted = await this.db.delete(oauth2Clients).where(
|
|
10272
|
-
|
|
10572
|
+
and21(
|
|
10273
10573
|
lt8(oauth2Clients.createdAt, before),
|
|
10274
|
-
|
|
10574
|
+
sql11`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
10275
10575
|
)
|
|
10276
10576
|
).returning({ id: oauth2Clients.id });
|
|
10277
10577
|
return deleted.length;
|
|
@@ -10282,8 +10582,8 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10282
10582
|
});
|
|
10283
10583
|
|
|
10284
10584
|
// src/server/repositories/oauth2-grants.repository.ts
|
|
10285
|
-
import { and as
|
|
10286
|
-
import { BaseRepository as
|
|
10585
|
+
import { and as and22, desc as desc5, eq as eq26, inArray as inArray4, isNull as isNull14, sql as sql12 } from "drizzle-orm";
|
|
10586
|
+
import { BaseRepository as BaseRepository26 } from "@spfn/core/db";
|
|
10287
10587
|
var OAuth2GrantsRepository, oauth2GrantsRepository;
|
|
10288
10588
|
var init_oauth2_grants_repository = __esm({
|
|
10289
10589
|
"src/server/repositories/oauth2-grants.repository.ts"() {
|
|
@@ -10291,7 +10591,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10291
10591
|
init_oauth2_grants();
|
|
10292
10592
|
init_oauth2_clients();
|
|
10293
10593
|
init_oauth2_tokens();
|
|
10294
|
-
OAuth2GrantsRepository = class extends
|
|
10594
|
+
OAuth2GrantsRepository = class extends BaseRepository26 {
|
|
10295
10595
|
/**
|
|
10296
10596
|
* Record a consent, or refresh the scopes of the one already there.
|
|
10297
10597
|
*
|
|
@@ -10318,13 +10618,13 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10318
10618
|
*
|
|
10319
10619
|
* Read primary: revocation is documented as taking effect immediately.
|
|
10320
10620
|
*/
|
|
10321
|
-
async findWithClientById(
|
|
10322
|
-
const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients,
|
|
10621
|
+
async findWithClientById(id26) {
|
|
10622
|
+
const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq26(oauth2Grants.client, oauth2Clients.id)).where(eq26(oauth2Grants.id, id26)).limit(1);
|
|
10323
10623
|
return result[0] ?? null;
|
|
10324
10624
|
}
|
|
10325
10625
|
/** What the user's "connected apps" screen lists. Read replica. */
|
|
10326
10626
|
async listActiveByUserId(userId) {
|
|
10327
|
-
return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients,
|
|
10627
|
+
return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq26(oauth2Grants.client, oauth2Clients.id)).where(and22(eq26(oauth2Grants.user, userId), isNull14(oauth2Grants.revokedAt))).orderBy(desc5(oauth2Grants.createdAt));
|
|
10328
10628
|
}
|
|
10329
10629
|
/**
|
|
10330
10630
|
* Revoke one live grant belonging to one user.
|
|
@@ -10337,19 +10637,19 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10337
10637
|
* @returns the revoked row, or null if there was no live grant of that id
|
|
10338
10638
|
* for that user
|
|
10339
10639
|
*/
|
|
10340
|
-
async revokeByIdForUser(
|
|
10640
|
+
async revokeByIdForUser(id26, userId) {
|
|
10341
10641
|
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10642
|
+
and22(
|
|
10643
|
+
eq26(oauth2Grants.id, id26),
|
|
10644
|
+
eq26(oauth2Grants.user, userId),
|
|
10645
|
+
isNull14(oauth2Grants.revokedAt)
|
|
10346
10646
|
)
|
|
10347
10647
|
).returning();
|
|
10348
10648
|
return result[0] ?? null;
|
|
10349
10649
|
}
|
|
10350
10650
|
/** Revoke one grant by id, whoever it belongs to — the replay detections. */
|
|
10351
|
-
async revokeById(
|
|
10352
|
-
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10651
|
+
async revokeById(id26) {
|
|
10652
|
+
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and22(eq26(oauth2Grants.id, id26), isNull14(oauth2Grants.revokedAt))).returning();
|
|
10353
10653
|
return result[0] ?? null;
|
|
10354
10654
|
}
|
|
10355
10655
|
/**
|
|
@@ -10359,7 +10659,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10359
10659
|
* @returns the grant ids this call revoked
|
|
10360
10660
|
*/
|
|
10361
10661
|
async revokeAllActiveByUserId(userId) {
|
|
10362
|
-
const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10662
|
+
const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and22(eq26(oauth2Grants.user, userId), isNull14(oauth2Grants.revokedAt))).returning({ id: oauth2Grants.id });
|
|
10363
10663
|
return revoked.map((row) => row.id);
|
|
10364
10664
|
}
|
|
10365
10665
|
/**
|
|
@@ -10375,7 +10675,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10375
10675
|
if (grantIds.length === 0) {
|
|
10376
10676
|
return 0;
|
|
10377
10677
|
}
|
|
10378
|
-
const revoked = await this.db.update(oauth2Tokens).set({ revokedAt:
|
|
10678
|
+
const revoked = await this.db.update(oauth2Tokens).set({ revokedAt: sql12`now()` }).where(and22(inArray4(oauth2Tokens.grant, grantIds), isNull14(oauth2Tokens.revokedAt))).returning({ id: oauth2Tokens.id });
|
|
10379
10679
|
return revoked.length;
|
|
10380
10680
|
}
|
|
10381
10681
|
};
|
|
@@ -10384,14 +10684,14 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10384
10684
|
});
|
|
10385
10685
|
|
|
10386
10686
|
// src/server/repositories/oauth2-authorization-codes.repository.ts
|
|
10387
|
-
import { and as
|
|
10388
|
-
import { BaseRepository as
|
|
10687
|
+
import { and as and23, eq as eq27, gt as gt10, isNull as isNull15, sql as sql13 } from "drizzle-orm";
|
|
10688
|
+
import { BaseRepository as BaseRepository27 } from "@spfn/core/db";
|
|
10389
10689
|
var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
|
|
10390
10690
|
var init_oauth2_authorization_codes_repository = __esm({
|
|
10391
10691
|
"src/server/repositories/oauth2-authorization-codes.repository.ts"() {
|
|
10392
10692
|
"use strict";
|
|
10393
10693
|
init_oauth2_authorization_codes();
|
|
10394
|
-
OAuth2AuthorizationCodesRepository = class extends
|
|
10694
|
+
OAuth2AuthorizationCodesRepository = class extends BaseRepository27 {
|
|
10395
10695
|
async create(data) {
|
|
10396
10696
|
const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
|
|
10397
10697
|
return result[0];
|
|
@@ -10403,11 +10703,11 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10403
10703
|
* spent, or past its 60 seconds
|
|
10404
10704
|
*/
|
|
10405
10705
|
async consume(codeHash) {
|
|
10406
|
-
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt:
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10706
|
+
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql13`now()` }).where(
|
|
10707
|
+
and23(
|
|
10708
|
+
eq27(oauth2AuthorizationCodes.codeHash, codeHash),
|
|
10709
|
+
isNull15(oauth2AuthorizationCodes.usedAt),
|
|
10710
|
+
gt10(oauth2AuthorizationCodes.expiresAt, sql13`now()`)
|
|
10411
10711
|
)
|
|
10412
10712
|
).returning();
|
|
10413
10713
|
return result[0] ?? null;
|
|
@@ -10418,7 +10718,7 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10418
10718
|
* would be indistinguishable from a code that never existed.
|
|
10419
10719
|
*/
|
|
10420
10720
|
async findByCodeHash(codeHash) {
|
|
10421
|
-
const result = await this.db.select().from(oauth2AuthorizationCodes).where(
|
|
10721
|
+
const result = await this.db.select().from(oauth2AuthorizationCodes).where(eq27(oauth2AuthorizationCodes.codeHash, codeHash)).limit(1);
|
|
10422
10722
|
return result[0] ?? null;
|
|
10423
10723
|
}
|
|
10424
10724
|
};
|
|
@@ -10427,14 +10727,14 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10427
10727
|
});
|
|
10428
10728
|
|
|
10429
10729
|
// src/server/repositories/oauth2-tokens.repository.ts
|
|
10430
|
-
import { and as
|
|
10431
|
-
import { BaseRepository as
|
|
10730
|
+
import { and as and24, eq as eq28, gt as gt11, isNull as isNull16, sql as sql14 } from "drizzle-orm";
|
|
10731
|
+
import { BaseRepository as BaseRepository28 } from "@spfn/core/db";
|
|
10432
10732
|
var OAuth2TokensRepository, oauth2TokensRepository;
|
|
10433
10733
|
var init_oauth2_tokens_repository = __esm({
|
|
10434
10734
|
"src/server/repositories/oauth2-tokens.repository.ts"() {
|
|
10435
10735
|
"use strict";
|
|
10436
10736
|
init_oauth2_tokens();
|
|
10437
|
-
OAuth2TokensRepository = class extends
|
|
10737
|
+
OAuth2TokensRepository = class extends BaseRepository28 {
|
|
10438
10738
|
async create(data) {
|
|
10439
10739
|
const result = await this.db.insert(oauth2Tokens).values(data).returning();
|
|
10440
10740
|
return result[0];
|
|
@@ -10448,7 +10748,7 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10448
10748
|
* Unfiltered, so the service can tell revoked from expired from unknown.
|
|
10449
10749
|
*/
|
|
10450
10750
|
async findByTokenHash(tokenHash) {
|
|
10451
|
-
const result = await this.db.select().from(oauth2Tokens).where(
|
|
10751
|
+
const result = await this.db.select().from(oauth2Tokens).where(eq28(oauth2Tokens.tokenHash, tokenHash)).limit(1);
|
|
10452
10752
|
return result[0] ?? null;
|
|
10453
10753
|
}
|
|
10454
10754
|
/**
|
|
@@ -10458,13 +10758,13 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10458
10758
|
* rotated, revoked, or expired
|
|
10459
10759
|
*/
|
|
10460
10760
|
async rotate(tokenHash) {
|
|
10461
|
-
const result = await this.db.update(oauth2Tokens).set({ replacedAt:
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10761
|
+
const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql14`now()` }).where(
|
|
10762
|
+
and24(
|
|
10763
|
+
eq28(oauth2Tokens.tokenHash, tokenHash),
|
|
10764
|
+
eq28(oauth2Tokens.kind, "refresh"),
|
|
10765
|
+
isNull16(oauth2Tokens.replacedAt),
|
|
10766
|
+
isNull16(oauth2Tokens.revokedAt),
|
|
10767
|
+
gt11(oauth2Tokens.expiresAt, sql14`now()`)
|
|
10468
10768
|
)
|
|
10469
10769
|
).returning();
|
|
10470
10770
|
return result[0] ?? null;
|
|
@@ -10477,12 +10777,12 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10477
10777
|
* about whether the value it presented ever existed.
|
|
10478
10778
|
*/
|
|
10479
10779
|
async revokeByTokenHash(tokenHash) {
|
|
10480
|
-
const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10780
|
+
const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and24(eq28(oauth2Tokens.tokenHash, tokenHash), isNull16(oauth2Tokens.revokedAt))).returning();
|
|
10481
10781
|
return result[0] ?? null;
|
|
10482
10782
|
}
|
|
10483
10783
|
/** Fire-and-forget from the verification path, as ops tokens do. */
|
|
10484
|
-
async updateLastUsedById(
|
|
10485
|
-
await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
10784
|
+
async updateLastUsedById(id26) {
|
|
10785
|
+
await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq28(oauth2Tokens.id, id26));
|
|
10486
10786
|
}
|
|
10487
10787
|
};
|
|
10488
10788
|
oauth2TokensRepository = new OAuth2TokensRepository();
|
|
@@ -10504,6 +10804,7 @@ var init_repositories = __esm({
|
|
|
10504
10804
|
init_mfa_totp_repository();
|
|
10505
10805
|
init_mfa_recovery_codes_repository();
|
|
10506
10806
|
init_mfa_verifications_repository();
|
|
10807
|
+
init_mfa_challenges_repository();
|
|
10507
10808
|
init_mfa_enrolment_repository();
|
|
10508
10809
|
init_device_authorizations_repository();
|
|
10509
10810
|
init_roles_repository();
|
|
@@ -10611,7 +10912,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
10611
10912
|
}
|
|
10612
10913
|
async function setRolePermissions(roleId, permissionIds) {
|
|
10613
10914
|
const roleIdNum = Number(roleId);
|
|
10614
|
-
const permissionIdNums = permissionIds.map((
|
|
10915
|
+
const permissionIdNums = permissionIds.map((id26) => Number(id26));
|
|
10615
10916
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
10616
10917
|
}
|
|
10617
10918
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -10631,7 +10932,7 @@ async function getRolePermissions(roleId) {
|
|
|
10631
10932
|
}
|
|
10632
10933
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
10633
10934
|
const perms = await Promise.all(
|
|
10634
|
-
permissionIds.map((
|
|
10935
|
+
permissionIds.map((id26) => permissionsRepository.findById(id26))
|
|
10635
10936
|
);
|
|
10636
10937
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
10637
10938
|
}
|
|
@@ -10895,8 +11196,8 @@ async function listOAuth2GrantsService(userId) {
|
|
|
10895
11196
|
lastUsedAtMillis: client.lastUsedAt?.getTime()
|
|
10896
11197
|
}));
|
|
10897
11198
|
}
|
|
10898
|
-
async function revokeOAuth2GrantService(
|
|
10899
|
-
const revoked = await oauth2GrantsRepository.revokeByIdForUser(
|
|
11199
|
+
async function revokeOAuth2GrantService(id26, userId) {
|
|
11200
|
+
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id26, userId);
|
|
10900
11201
|
if (!revoked) {
|
|
10901
11202
|
throw new OAuth2GrantNotFoundError();
|
|
10902
11203
|
}
|
|
@@ -11283,10 +11584,10 @@ function uaFamily(userAgent) {
|
|
|
11283
11584
|
// src/server/services/key.service.ts
|
|
11284
11585
|
init_repositories();
|
|
11285
11586
|
import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/auth/errors";
|
|
11286
|
-
import { ValidationError as
|
|
11587
|
+
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
11287
11588
|
|
|
11288
11589
|
// src/server/services/device-registration.service.ts
|
|
11289
|
-
import { onAfterCommit
|
|
11590
|
+
import { onAfterCommit } from "@spfn/core/db";
|
|
11290
11591
|
|
|
11291
11592
|
// src/server/events/index.ts
|
|
11292
11593
|
init_esm();
|
|
@@ -11433,11 +11734,31 @@ var authPasswordResetEvent = defineEvent(
|
|
|
11433
11734
|
})
|
|
11434
11735
|
);
|
|
11435
11736
|
|
|
11737
|
+
// src/server/services/device-registration.service.ts
|
|
11738
|
+
init_repositories();
|
|
11739
|
+
var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
|
|
11740
|
+
async function emitDeviceRegistered(row, channel) {
|
|
11741
|
+
const mfaEnrolled = await mfaEnrolmentRepository.isEnrolled(row.userId);
|
|
11742
|
+
onAfterCommit(() => authDeviceRegisteredEvent.emit({
|
|
11743
|
+
userId: String(row.userId),
|
|
11744
|
+
keyId: row.keyId,
|
|
11745
|
+
algorithm: row.algorithm,
|
|
11746
|
+
fingerprintPrefix: row.fingerprint.slice(0, DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH),
|
|
11747
|
+
deviceName: row.deviceName ?? void 0,
|
|
11748
|
+
platform: row.platform ?? void 0,
|
|
11749
|
+
ip: row.registeredIp ?? void 0,
|
|
11750
|
+
userAgent: row.registeredUserAgent ?? void 0,
|
|
11751
|
+
createdAtMillis: row.createdAt.getTime(),
|
|
11752
|
+
channel,
|
|
11753
|
+
mfaEnrolled
|
|
11754
|
+
}));
|
|
11755
|
+
}
|
|
11756
|
+
|
|
11436
11757
|
// src/server/services/mfa.service.ts
|
|
11437
11758
|
init_logger();
|
|
11438
11759
|
init_config();
|
|
11439
|
-
import { onAfterCommit, runInTransaction as runInTransaction2 } from "@spfn/core/db";
|
|
11440
|
-
import { ValidationError } from "@spfn/core/errors";
|
|
11760
|
+
import { onAfterCommit as onAfterCommit2, runInTransaction as runInTransaction2 } from "@spfn/core/db";
|
|
11761
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
11441
11762
|
import {
|
|
11442
11763
|
MfaAlreadyEnrolledError,
|
|
11443
11764
|
MfaNotEnrolledError,
|
|
@@ -11534,16 +11855,16 @@ function encodeBase32(bytes) {
|
|
|
11534
11855
|
}
|
|
11535
11856
|
return bits > 0 ? output + BASE32_ALPHABET[value << 5 - bits & 31] : output;
|
|
11536
11857
|
}
|
|
11537
|
-
function decodeBase32(
|
|
11858
|
+
function decodeBase32(text27) {
|
|
11538
11859
|
let bits = 0;
|
|
11539
11860
|
let value = 0;
|
|
11540
11861
|
const bytes = [];
|
|
11541
|
-
for (const character of
|
|
11542
|
-
const
|
|
11543
|
-
if (
|
|
11862
|
+
for (const character of text27.replace(/=+$/, "").toUpperCase()) {
|
|
11863
|
+
const index21 = BASE32_ALPHABET.indexOf(character);
|
|
11864
|
+
if (index21 < 0) {
|
|
11544
11865
|
throw new Error("Value is not RFC 4648 base32");
|
|
11545
11866
|
}
|
|
11546
|
-
value = value << 5 |
|
|
11867
|
+
value = value << 5 | index21;
|
|
11547
11868
|
bits += 5;
|
|
11548
11869
|
if (bits >= 8) {
|
|
11549
11870
|
bytes.push(value >>> bits - 8 & 255);
|
|
@@ -11724,6 +12045,87 @@ async function verifyAuthentication(params) {
|
|
|
11724
12045
|
|
|
11725
12046
|
// src/server/services/mfa.service.ts
|
|
11726
12047
|
init_repositories();
|
|
12048
|
+
|
|
12049
|
+
// src/server/services/login-result.ts
|
|
12050
|
+
function loginBindingFields(registered) {
|
|
12051
|
+
if (registered.binding !== "passkey" || !registered.expiresAt) {
|
|
12052
|
+
return {};
|
|
12053
|
+
}
|
|
12054
|
+
return { sessionBinding: "passkey", keyExpiresAtMillis: registered.expiresAt.getTime() };
|
|
12055
|
+
}
|
|
12056
|
+
|
|
12057
|
+
// src/server/services/user.service.ts
|
|
12058
|
+
init_repositories();
|
|
12059
|
+
import { ValidationError } from "@spfn/core/errors";
|
|
12060
|
+
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
12061
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
12062
|
+
async function getUserByIdService(userId) {
|
|
12063
|
+
return await usersRepository.findById(userId);
|
|
12064
|
+
}
|
|
12065
|
+
async function getUserByEmailService(email) {
|
|
12066
|
+
return await usersRepository.findByEmail(email);
|
|
12067
|
+
}
|
|
12068
|
+
async function getUserByPhoneService(phone) {
|
|
12069
|
+
return await usersRepository.findByPhone(phone);
|
|
12070
|
+
}
|
|
12071
|
+
async function updateLastLoginService(userId) {
|
|
12072
|
+
await usersRepository.updateLastLogin(userId);
|
|
12073
|
+
}
|
|
12074
|
+
async function updateUserService(userId, updates) {
|
|
12075
|
+
await usersRepository.updateById(userId, updates);
|
|
12076
|
+
}
|
|
12077
|
+
function getReservedUsernames() {
|
|
12078
|
+
const raw = env9.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
12079
|
+
if (!raw) {
|
|
12080
|
+
return /* @__PURE__ */ new Set();
|
|
12081
|
+
}
|
|
12082
|
+
return new Set(
|
|
12083
|
+
raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
12084
|
+
);
|
|
12085
|
+
}
|
|
12086
|
+
function isReservedUsername(username) {
|
|
12087
|
+
return getReservedUsernames().has(username.toLowerCase());
|
|
12088
|
+
}
|
|
12089
|
+
function validateUsernameLength(username) {
|
|
12090
|
+
const min = env9.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
12091
|
+
const max2 = env9.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
12092
|
+
if (username.length < min) {
|
|
12093
|
+
throw new ValidationError({
|
|
12094
|
+
message: `Username must be at least ${min} characters`,
|
|
12095
|
+
details: { minLength: min, actual: username.length }
|
|
12096
|
+
});
|
|
12097
|
+
}
|
|
12098
|
+
if (username.length > max2) {
|
|
12099
|
+
throw new ValidationError({
|
|
12100
|
+
message: `Username must be at most ${max2} characters`,
|
|
12101
|
+
details: { maxLength: max2, actual: username.length }
|
|
12102
|
+
});
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
async function checkUsernameAvailableService(username) {
|
|
12106
|
+
validateUsernameLength(username);
|
|
12107
|
+
if (isReservedUsername(username)) {
|
|
12108
|
+
return false;
|
|
12109
|
+
}
|
|
12110
|
+
const existing = await usersRepository.findByUsername(username);
|
|
12111
|
+
return !existing;
|
|
12112
|
+
}
|
|
12113
|
+
async function updateUsernameService(userId, username) {
|
|
12114
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
12115
|
+
if (username !== null) {
|
|
12116
|
+
validateUsernameLength(username);
|
|
12117
|
+
if (isReservedUsername(username)) {
|
|
12118
|
+
throw new ReservedUsernameError({ username });
|
|
12119
|
+
}
|
|
12120
|
+
const existing = await usersRepository.findByUsername(username);
|
|
12121
|
+
if (existing && existing.id !== userIdNum) {
|
|
12122
|
+
throw new UsernameAlreadyTakenError({ username });
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12125
|
+
return await usersRepository.updateById(userIdNum, { username });
|
|
12126
|
+
}
|
|
12127
|
+
|
|
12128
|
+
// src/server/services/mfa.service.ts
|
|
11727
12129
|
init_mfa_totp();
|
|
11728
12130
|
|
|
11729
12131
|
// src/server/services/webauthn-challenge.service.ts
|
|
@@ -11752,9 +12154,9 @@ function presentedChallenge(clientDataJSON) {
|
|
|
11752
12154
|
const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
|
|
11753
12155
|
return typeof decoded?.challenge === "string" ? decoded.challenge : "";
|
|
11754
12156
|
}
|
|
11755
|
-
function parseJson(
|
|
12157
|
+
function parseJson(text27) {
|
|
11756
12158
|
try {
|
|
11757
|
-
return JSON.parse(
|
|
12159
|
+
return JSON.parse(text27);
|
|
11758
12160
|
} catch {
|
|
11759
12161
|
return null;
|
|
11760
12162
|
}
|
|
@@ -11774,10 +12176,110 @@ async function assertStepUp(params) {
|
|
|
11774
12176
|
if (verification?.userId === params.userId && Date.now() - verification.verifiedAt.getTime() <= window) {
|
|
11775
12177
|
return;
|
|
11776
12178
|
}
|
|
11777
|
-
throw new StepUpRequiredError();
|
|
12179
|
+
throw new StepUpRequiredError();
|
|
12180
|
+
}
|
|
12181
|
+
function carryStepUpVerification(userId, fromKeyId, toKeyId) {
|
|
12182
|
+
return mfaVerificationsRepository.moveToKeyId(userId, fromKeyId, toKeyId);
|
|
12183
|
+
}
|
|
12184
|
+
async function openStepUpChallengeService(params) {
|
|
12185
|
+
const { secret, hash: hash2 } = mintCredential();
|
|
12186
|
+
const expiresAt = new Date(Date.now() + getMfaConfig().challengeTtlMs);
|
|
12187
|
+
const row = await mfaChallengesRepository.create({
|
|
12188
|
+
userId: params.userId,
|
|
12189
|
+
challengeHash: hash2,
|
|
12190
|
+
keyId: params.keyId,
|
|
12191
|
+
channel: params.channel,
|
|
12192
|
+
keyEpoch: params.keyEpoch,
|
|
12193
|
+
expiresAt,
|
|
12194
|
+
loginEvent: params.loginEvent ?? null
|
|
12195
|
+
});
|
|
12196
|
+
await mfaChallengesRepository.markKeyPending(params.keyId, row.id);
|
|
12197
|
+
return { secret, expiresAtMillis: expiresAt.getTime() };
|
|
12198
|
+
}
|
|
12199
|
+
async function resumeStepUpChallengeService(userId, keyId) {
|
|
12200
|
+
const live = await mfaChallengesRepository.findLiveByKeyId(userId, keyId);
|
|
12201
|
+
if (!live) {
|
|
12202
|
+
return null;
|
|
12203
|
+
}
|
|
12204
|
+
const { secret, hash: hash2 } = mintCredential();
|
|
12205
|
+
return await mfaChallengesRepository.resecret(live.id, hash2) ? { secret, expiresAtMillis: live.expiresAt.getTime() } : null;
|
|
12206
|
+
}
|
|
12207
|
+
async function spendableChallenge(challengeHash) {
|
|
12208
|
+
const row = await mfaChallengesRepository.findByHash(challengeHash);
|
|
12209
|
+
if (!row || row.verifiedAt || row.expiresAt.getTime() <= Date.now()) {
|
|
12210
|
+
throw new MfaVerificationFailedError();
|
|
12211
|
+
}
|
|
12212
|
+
if (row.keyEpoch !== await usersRepository.currentKeyEpoch(row.userId)) {
|
|
12213
|
+
throw new MfaVerificationFailedError();
|
|
12214
|
+
}
|
|
12215
|
+
return row;
|
|
12216
|
+
}
|
|
12217
|
+
async function countChallengeFailure(row) {
|
|
12218
|
+
if (await mfaChallengesRepository.countFailure(row.id)) {
|
|
12219
|
+
await mfaChallengesRepository.dropPendingKey(row.keyId, row.id);
|
|
12220
|
+
authLogger.service.warn("Second-factor challenge discarded after repeated wrong proofs", {
|
|
12221
|
+
userId: row.userId
|
|
12222
|
+
});
|
|
12223
|
+
}
|
|
12224
|
+
throw new MfaVerificationFailedError();
|
|
12225
|
+
}
|
|
12226
|
+
async function releaseDeferredAnnouncements(row, key) {
|
|
12227
|
+
await updateLastLoginService(row.userId);
|
|
12228
|
+
await emitDeviceRegistered(key, row.channel);
|
|
12229
|
+
if (!row.loginEvent) {
|
|
12230
|
+
return;
|
|
12231
|
+
}
|
|
12232
|
+
const mfaEnrolled = await mfaEnrolledForUser(row.userId);
|
|
12233
|
+
onAfterCommit2(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
|
|
12234
|
+
}
|
|
12235
|
+
async function verifyMfaChallengeService(params) {
|
|
12236
|
+
const challengeHash = hashCredential(params.challenge);
|
|
12237
|
+
const row = await spendableChallenge(challengeHash);
|
|
12238
|
+
const method = await attemptSecondFactor({
|
|
12239
|
+
userId: row.userId,
|
|
12240
|
+
keyId: row.keyId,
|
|
12241
|
+
code: params.code,
|
|
12242
|
+
recoveryCode: params.recoveryCode,
|
|
12243
|
+
response: params.response
|
|
12244
|
+
});
|
|
12245
|
+
if (!method) {
|
|
12246
|
+
await countChallengeFailure(row);
|
|
12247
|
+
}
|
|
12248
|
+
return await runInTransaction2(async () => {
|
|
12249
|
+
if (!await mfaChallengesRepository.markVerified(row.id, row.keyEpoch)) {
|
|
12250
|
+
throw new MfaVerificationFailedError();
|
|
12251
|
+
}
|
|
12252
|
+
await mfaChallengesRepository.activateKey(row.keyId, row.id);
|
|
12253
|
+
await mfaVerificationsRepository.record(row.keyId, row.userId, method);
|
|
12254
|
+
return { ...await settleStepUpLogin(row), keyId: row.keyId, challengeHash };
|
|
12255
|
+
}, { context: "auth:mfa-verify" });
|
|
12256
|
+
}
|
|
12257
|
+
async function settleStepUpLogin(row) {
|
|
12258
|
+
const user = await usersRepository.findByIdOnPrimary(row.userId);
|
|
12259
|
+
const key = await keysRepository.findByKeyIdAndUserId(row.keyId, row.userId);
|
|
12260
|
+
if (!user || !key) {
|
|
12261
|
+
throw new MfaVerificationFailedError();
|
|
12262
|
+
}
|
|
12263
|
+
await releaseDeferredAnnouncements(row, key);
|
|
12264
|
+
return {
|
|
12265
|
+
mfaRequired: false,
|
|
12266
|
+
userId: String(user.id),
|
|
12267
|
+
publicId: user.publicId,
|
|
12268
|
+
email: user.email || void 0,
|
|
12269
|
+
phone: user.phone || void 0,
|
|
12270
|
+
passwordChangeRequired: user.passwordChangeRequired,
|
|
12271
|
+
...loginBindingFields(key)
|
|
12272
|
+
};
|
|
11778
12273
|
}
|
|
11779
|
-
function
|
|
11780
|
-
|
|
12274
|
+
async function startMfaChallengeAssertionService(challenge) {
|
|
12275
|
+
const row = await spendableChallenge(hashCredential(challenge));
|
|
12276
|
+
return await buildAuthenticationOptions({
|
|
12277
|
+
config: getPasskeyConfig(),
|
|
12278
|
+
challenge: await mintChallenge("mfa", row.userId)
|
|
12279
|
+
});
|
|
12280
|
+
}
|
|
12281
|
+
async function sweepMfaChallengesService() {
|
|
12282
|
+
return { deleted: await mfaChallengesRepository.sweepFinished(/* @__PURE__ */ new Date()) };
|
|
11781
12283
|
}
|
|
11782
12284
|
async function startTotpEnrolmentService(userId) {
|
|
11783
12285
|
const existing = await mfaTotpRepository.findByUserId(userId);
|
|
@@ -11822,7 +12324,7 @@ async function countConfirmFailure(userId) {
|
|
|
11822
12324
|
function readSecret(secretEnc, userId) {
|
|
11823
12325
|
const { value, needsRotation } = decryptMfaSecret(secretEnc, userId);
|
|
11824
12326
|
if (needsRotation) {
|
|
11825
|
-
|
|
12327
|
+
onAfterCommit2(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
|
|
11826
12328
|
}
|
|
11827
12329
|
return value;
|
|
11828
12330
|
}
|
|
@@ -11884,10 +12386,10 @@ async function stepUpService(params) {
|
|
|
11884
12386
|
const method = await verifySecondFactor(params);
|
|
11885
12387
|
await mfaVerificationsRepository.record(params.keyId, params.userId, method);
|
|
11886
12388
|
}
|
|
11887
|
-
async function
|
|
12389
|
+
async function attemptSecondFactor(params) {
|
|
11888
12390
|
const presented = [params.code, params.recoveryCode, params.response].filter((value) => value !== void 0);
|
|
11889
12391
|
if (presented.length !== 1) {
|
|
11890
|
-
throw new
|
|
12392
|
+
throw new ValidationError2({
|
|
11891
12393
|
message: "Send exactly one of code, recoveryCode or response."
|
|
11892
12394
|
});
|
|
11893
12395
|
}
|
|
@@ -11896,10 +12398,17 @@ async function verifySecondFactor(params) {
|
|
|
11896
12398
|
}
|
|
11897
12399
|
return params.recoveryCode !== void 0 ? await verifyRecoveryProof(params.userId, params.recoveryCode) : await verifyPasskeyProof(params.userId, params.response);
|
|
11898
12400
|
}
|
|
12401
|
+
async function verifySecondFactor(params) {
|
|
12402
|
+
const method = await attemptSecondFactor(params);
|
|
12403
|
+
if (!method) {
|
|
12404
|
+
throw new MfaVerificationFailedError();
|
|
12405
|
+
}
|
|
12406
|
+
return method;
|
|
12407
|
+
}
|
|
11899
12408
|
async function verifyTotpProof(userId, code) {
|
|
11900
12409
|
const row = await mfaTotpRepository.findByUserId(userId);
|
|
11901
12410
|
if (!row?.confirmedAt) {
|
|
11902
|
-
|
|
12411
|
+
return null;
|
|
11903
12412
|
}
|
|
11904
12413
|
const step = verifyTotp({
|
|
11905
12414
|
secret: readSecret(row.secretEnc, userId),
|
|
@@ -11907,7 +12416,7 @@ async function verifyTotpProof(userId, code) {
|
|
|
11907
12416
|
lastUsedStep: row.lastUsedStep
|
|
11908
12417
|
});
|
|
11909
12418
|
if (step === null) {
|
|
11910
|
-
|
|
12419
|
+
return null;
|
|
11911
12420
|
}
|
|
11912
12421
|
await mfaTotpRepository.recordUsedStep(userId, step);
|
|
11913
12422
|
return "totp";
|
|
@@ -11921,17 +12430,17 @@ async function verifyRecoveryProof(userId, submitted) {
|
|
|
11921
12430
|
return "recovery";
|
|
11922
12431
|
}
|
|
11923
12432
|
}
|
|
11924
|
-
|
|
12433
|
+
return null;
|
|
11925
12434
|
}
|
|
11926
12435
|
async function verifyPasskeyProof(userId, response) {
|
|
11927
12436
|
const challenge = presentedChallenge(response.response.clientDataJSON);
|
|
11928
12437
|
if (!await spendChallenge(challenge, "mfa", userId)) {
|
|
11929
|
-
|
|
12438
|
+
return null;
|
|
11930
12439
|
}
|
|
11931
12440
|
const marked = await passkeysRepository.listLiveSecondFactorByUserId(userId);
|
|
11932
12441
|
const passkey = marked.find((row) => row.credentialId === response.id);
|
|
11933
12442
|
if (!passkey) {
|
|
11934
|
-
|
|
12443
|
+
return null;
|
|
11935
12444
|
}
|
|
11936
12445
|
const outcome = await verifyAuthentication({
|
|
11937
12446
|
config: getPasskeyConfig(),
|
|
@@ -11945,7 +12454,7 @@ async function verifyPasskeyProof(userId, response) {
|
|
|
11945
12454
|
}
|
|
11946
12455
|
});
|
|
11947
12456
|
if (!outcome.verified) {
|
|
11948
|
-
|
|
12457
|
+
return null;
|
|
11949
12458
|
}
|
|
11950
12459
|
await passkeysRepository.recordUse(passkey.id, outcome.newCounter);
|
|
11951
12460
|
return "passkey";
|
|
@@ -11955,26 +12464,14 @@ async function sweepUnconfirmedMfaService() {
|
|
|
11955
12464
|
return { deleted: await mfaTotpRepository.deleteUnconfirmedBefore(cutoff) };
|
|
11956
12465
|
}
|
|
11957
12466
|
|
|
11958
|
-
// src/server/services/device-registration.service.ts
|
|
11959
|
-
var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
|
|
11960
|
-
async function emitDeviceRegistered(row, channel) {
|
|
11961
|
-
const mfaEnrolled = await mfaEnrolledForUser(row.userId);
|
|
11962
|
-
onAfterCommit2(() => authDeviceRegisteredEvent.emit({
|
|
11963
|
-
userId: String(row.userId),
|
|
11964
|
-
keyId: row.keyId,
|
|
11965
|
-
algorithm: row.algorithm,
|
|
11966
|
-
fingerprintPrefix: row.fingerprint.slice(0, DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH),
|
|
11967
|
-
deviceName: row.deviceName ?? void 0,
|
|
11968
|
-
platform: row.platform ?? void 0,
|
|
11969
|
-
ip: row.registeredIp ?? void 0,
|
|
11970
|
-
userAgent: row.registeredUserAgent ?? void 0,
|
|
11971
|
-
createdAtMillis: row.createdAt.getTime(),
|
|
11972
|
-
channel,
|
|
11973
|
-
mfaEnrolled
|
|
11974
|
-
}));
|
|
11975
|
-
}
|
|
11976
|
-
|
|
11977
12467
|
// src/server/services/key.service.ts
|
|
12468
|
+
init_mfa_challenges();
|
|
12469
|
+
function registeredBinding(result) {
|
|
12470
|
+
if (result.pending) {
|
|
12471
|
+
throw new Error("This registration channel cannot require a second factor");
|
|
12472
|
+
}
|
|
12473
|
+
return result;
|
|
12474
|
+
}
|
|
11978
12475
|
var KEY_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
11979
12476
|
var DEFAULT_KEY_ALGORITHM = "ES256";
|
|
11980
12477
|
function getKeyExpiryDate(binding) {
|
|
@@ -11988,21 +12485,36 @@ function getKeyExpiryDate(binding) {
|
|
|
11988
12485
|
function isExpired(expiresAt) {
|
|
11989
12486
|
return expiresAt !== null && /* @__PURE__ */ new Date() > expiresAt;
|
|
11990
12487
|
}
|
|
12488
|
+
async function decideStepUp(params) {
|
|
12489
|
+
const channel = MFA_CHALLENGE_CHANNELS.find((candidate) => candidate === params.channel);
|
|
12490
|
+
if (!channel || params.replacesKeyId) {
|
|
12491
|
+
return null;
|
|
12492
|
+
}
|
|
12493
|
+
return await mfaEnrolledForUser(params.userId) ? channel : null;
|
|
12494
|
+
}
|
|
12495
|
+
async function answerForExistingKey(existing, userId) {
|
|
12496
|
+
if (existing.userId === userId && existing.isActive) {
|
|
12497
|
+
return { pending: false, ...await reRegisterOwnActiveKey(existing, userId) };
|
|
12498
|
+
}
|
|
12499
|
+
const resumed = existing.userId === userId && existing.pendingMfaChallengeId ? await resumeStepUpChallengeService(userId, existing.keyId) : null;
|
|
12500
|
+
if (resumed) {
|
|
12501
|
+
return { pending: true, challenge: resumed };
|
|
12502
|
+
}
|
|
12503
|
+
throw new KeyIdAlreadyRegisteredError();
|
|
12504
|
+
}
|
|
11991
12505
|
async function registerPublicKeyService(params) {
|
|
11992
12506
|
const { userId, keyId, publicKey, fingerprint, algorithm = DEFAULT_KEY_ALGORITHM, deviceName, platform } = params;
|
|
11993
12507
|
const binding = params.binding ?? "none";
|
|
11994
12508
|
const existing = await keysRepository.findByKeyId(keyId);
|
|
11995
12509
|
if (existing) {
|
|
11996
|
-
|
|
11997
|
-
return await reRegisterOwnActiveKey(existing, userId);
|
|
11998
|
-
}
|
|
11999
|
-
throw new KeyIdAlreadyRegisteredError();
|
|
12510
|
+
return await answerForExistingKey(existing, userId);
|
|
12000
12511
|
}
|
|
12001
12512
|
const isValidFingerprint = verifyKeyFingerprint(publicKey, fingerprint);
|
|
12002
12513
|
if (!isValidFingerprint) {
|
|
12003
12514
|
throw new InvalidKeyFingerprintError();
|
|
12004
12515
|
}
|
|
12005
12516
|
assertKeyMatchesAlgorithm(publicKey, algorithm);
|
|
12517
|
+
const stepUp = await decideStepUp(params);
|
|
12006
12518
|
const row = await keysRepository.create({
|
|
12007
12519
|
userId,
|
|
12008
12520
|
keyId,
|
|
@@ -12015,15 +12527,24 @@ async function registerPublicKeyService(params) {
|
|
|
12015
12527
|
registeredIp: params.ip ?? null,
|
|
12016
12528
|
registeredUserAgent: params.userAgent ?? null,
|
|
12017
12529
|
registeredUaFamily: params.userAgent ? uaFamily(params.userAgent) : null,
|
|
12018
|
-
isActive:
|
|
12530
|
+
isActive: stepUp === null,
|
|
12019
12531
|
expiresAt: getKeyExpiryDate(binding)
|
|
12020
12532
|
});
|
|
12533
|
+
if (stepUp) {
|
|
12534
|
+
return { pending: true, challenge: await openStepUpChallengeService({
|
|
12535
|
+
userId,
|
|
12536
|
+
keyId,
|
|
12537
|
+
channel: stepUp,
|
|
12538
|
+
keyEpoch: await usersRepository.currentKeyEpoch(userId),
|
|
12539
|
+
loginEvent: params.loginEvent
|
|
12540
|
+
}) };
|
|
12541
|
+
}
|
|
12021
12542
|
if (params.replacesKeyId) {
|
|
12022
12543
|
await carryStepUpVerification(userId, params.replacesKeyId, keyId);
|
|
12023
12544
|
} else {
|
|
12024
12545
|
await emitDeviceRegistered(row, params.channel);
|
|
12025
12546
|
}
|
|
12026
|
-
return { binding, expiresAt: row.expiresAt };
|
|
12547
|
+
return { pending: false, binding, expiresAt: row.expiresAt };
|
|
12027
12548
|
}
|
|
12028
12549
|
async function reRegisterOwnActiveKey(existing, userId) {
|
|
12029
12550
|
if (existing.binding !== "passkey" && isExpired(existing.expiresAt)) {
|
|
@@ -12095,7 +12616,7 @@ async function listKeysService(params) {
|
|
|
12095
12616
|
async function revokeAllKeysService(params) {
|
|
12096
12617
|
const { userId, currentKeyId, includeCurrent = false, reason } = params;
|
|
12097
12618
|
if (!includeCurrent && !currentKeyId) {
|
|
12098
|
-
throw new
|
|
12619
|
+
throw new ValidationError3({ message: "currentKeyId is required unless includeCurrent is set" });
|
|
12099
12620
|
}
|
|
12100
12621
|
if (currentKeyId) {
|
|
12101
12622
|
await assertStepUp({ userId, keyId: currentKeyId });
|
|
@@ -12109,77 +12630,6 @@ async function revokeAllKeysService(params) {
|
|
|
12109
12630
|
// src/server/services/auth.service.ts
|
|
12110
12631
|
init_key_policy();
|
|
12111
12632
|
|
|
12112
|
-
// src/server/services/user.service.ts
|
|
12113
|
-
init_repositories();
|
|
12114
|
-
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
12115
|
-
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
12116
|
-
import { env as env9 } from "@spfn/auth/config";
|
|
12117
|
-
async function getUserByIdService(userId) {
|
|
12118
|
-
return await usersRepository.findById(userId);
|
|
12119
|
-
}
|
|
12120
|
-
async function getUserByEmailService(email) {
|
|
12121
|
-
return await usersRepository.findByEmail(email);
|
|
12122
|
-
}
|
|
12123
|
-
async function getUserByPhoneService(phone) {
|
|
12124
|
-
return await usersRepository.findByPhone(phone);
|
|
12125
|
-
}
|
|
12126
|
-
async function updateLastLoginService(userId) {
|
|
12127
|
-
await usersRepository.updateLastLogin(userId);
|
|
12128
|
-
}
|
|
12129
|
-
async function updateUserService(userId, updates) {
|
|
12130
|
-
await usersRepository.updateById(userId, updates);
|
|
12131
|
-
}
|
|
12132
|
-
function getReservedUsernames() {
|
|
12133
|
-
const raw = env9.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
12134
|
-
if (!raw) {
|
|
12135
|
-
return /* @__PURE__ */ new Set();
|
|
12136
|
-
}
|
|
12137
|
-
return new Set(
|
|
12138
|
-
raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
12139
|
-
);
|
|
12140
|
-
}
|
|
12141
|
-
function isReservedUsername(username) {
|
|
12142
|
-
return getReservedUsernames().has(username.toLowerCase());
|
|
12143
|
-
}
|
|
12144
|
-
function validateUsernameLength(username) {
|
|
12145
|
-
const min = env9.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
12146
|
-
const max2 = env9.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
12147
|
-
if (username.length < min) {
|
|
12148
|
-
throw new ValidationError3({
|
|
12149
|
-
message: `Username must be at least ${min} characters`,
|
|
12150
|
-
details: { minLength: min, actual: username.length }
|
|
12151
|
-
});
|
|
12152
|
-
}
|
|
12153
|
-
if (username.length > max2) {
|
|
12154
|
-
throw new ValidationError3({
|
|
12155
|
-
message: `Username must be at most ${max2} characters`,
|
|
12156
|
-
details: { maxLength: max2, actual: username.length }
|
|
12157
|
-
});
|
|
12158
|
-
}
|
|
12159
|
-
}
|
|
12160
|
-
async function checkUsernameAvailableService(username) {
|
|
12161
|
-
validateUsernameLength(username);
|
|
12162
|
-
if (isReservedUsername(username)) {
|
|
12163
|
-
return false;
|
|
12164
|
-
}
|
|
12165
|
-
const existing = await usersRepository.findByUsername(username);
|
|
12166
|
-
return !existing;
|
|
12167
|
-
}
|
|
12168
|
-
async function updateUsernameService(userId, username) {
|
|
12169
|
-
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
12170
|
-
if (username !== null) {
|
|
12171
|
-
validateUsernameLength(username);
|
|
12172
|
-
if (isReservedUsername(username)) {
|
|
12173
|
-
throw new ReservedUsernameError({ username });
|
|
12174
|
-
}
|
|
12175
|
-
const existing = await usersRepository.findByUsername(username);
|
|
12176
|
-
if (existing && existing.id !== userIdNum) {
|
|
12177
|
-
throw new UsernameAlreadyTakenError({ username });
|
|
12178
|
-
}
|
|
12179
|
-
}
|
|
12180
|
-
return await usersRepository.updateById(userIdNum, { username });
|
|
12181
|
-
}
|
|
12182
|
-
|
|
12183
12633
|
// src/server/services/account-deletion.service.ts
|
|
12184
12634
|
init_repositories();
|
|
12185
12635
|
import { ValidationError as ValidationError4, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
|
|
@@ -12258,8 +12708,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
12258
12708
|
throw new VerificationTokenTargetMismatchError();
|
|
12259
12709
|
}
|
|
12260
12710
|
}
|
|
12261
|
-
async function sendDeletionEmail(to, subject,
|
|
12262
|
-
const result = await sendEmail3({ to, subject, text:
|
|
12711
|
+
async function sendDeletionEmail(to, subject, text27) {
|
|
12712
|
+
const result = await sendEmail3({ to, subject, text: text27 });
|
|
12263
12713
|
if (!result.success) {
|
|
12264
12714
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
12265
12715
|
}
|
|
@@ -12508,12 +12958,6 @@ async function sweepDuePurges(now = /* @__PURE__ */ new Date()) {
|
|
|
12508
12958
|
}
|
|
12509
12959
|
|
|
12510
12960
|
// src/server/services/auth.service.ts
|
|
12511
|
-
function loginBindingFields(registered) {
|
|
12512
|
-
if (registered.binding !== "passkey" || !registered.expiresAt) {
|
|
12513
|
-
return {};
|
|
12514
|
-
}
|
|
12515
|
-
return { sessionBinding: "passkey", keyExpiresAtMillis: registered.expiresAt.getTime() };
|
|
12516
|
-
}
|
|
12517
12961
|
async function registerService(params) {
|
|
12518
12962
|
const { email, verificationToken } = params;
|
|
12519
12963
|
const phone = params.phone?.trim();
|
|
@@ -12629,10 +13073,19 @@ async function loginService(params) {
|
|
|
12629
13073
|
ip: params.ip,
|
|
12630
13074
|
userAgent: params.userAgent,
|
|
12631
13075
|
binding: decideKeyBinding(user.sessionBinding, params.webProxy),
|
|
12632
|
-
replacesKeyId
|
|
13076
|
+
replacesKeyId,
|
|
13077
|
+
loginEvent: {
|
|
13078
|
+
provider: email ? "email" : "phone",
|
|
13079
|
+
email: user.email || void 0,
|
|
13080
|
+
phone: user.phone || void 0
|
|
13081
|
+
}
|
|
12633
13082
|
});
|
|
13083
|
+
if (registered.pending) {
|
|
13084
|
+
return { mfaRequired: true, challenge: registered.challenge };
|
|
13085
|
+
}
|
|
12634
13086
|
await updateLastLoginService(user.id);
|
|
12635
13087
|
const result = {
|
|
13088
|
+
mfaRequired: false,
|
|
12636
13089
|
userId: String(user.id),
|
|
12637
13090
|
publicId: user.publicId,
|
|
12638
13091
|
email: user.email || void 0,
|
|
@@ -12641,7 +13094,7 @@ async function loginService(params) {
|
|
|
12641
13094
|
...loginBindingFields(registered)
|
|
12642
13095
|
};
|
|
12643
13096
|
await authLoginEvent.emit({
|
|
12644
|
-
userId:
|
|
13097
|
+
userId: String(user.id),
|
|
12645
13098
|
provider: email ? "email" : "phone",
|
|
12646
13099
|
email: result.email,
|
|
12647
13100
|
phone: result.phone,
|
|
@@ -12899,12 +13352,11 @@ async function replaceCredentials(row, user, params) {
|
|
|
12899
13352
|
userAgent: params.userAgent,
|
|
12900
13353
|
binding: decideKeyBinding(user.sessionBinding, params.webProxy)
|
|
12901
13354
|
});
|
|
12902
|
-
await updateLastLoginService(user.id);
|
|
12903
13355
|
if (!await passwordResetTokensRepository.complete(row.id)) {
|
|
12904
13356
|
authLogger.service.warn("Password reset session refused", { reason: "lost the claim race" });
|
|
12905
13357
|
throw new PasswordResetSessionError();
|
|
12906
13358
|
}
|
|
12907
|
-
return
|
|
13359
|
+
return registered;
|
|
12908
13360
|
}
|
|
12909
13361
|
async function completePasswordResetService(params) {
|
|
12910
13362
|
if (!params.publicKey || !params.keyId || !params.fingerprint) {
|
|
@@ -12923,17 +13375,22 @@ async function completePasswordResetService(params) {
|
|
|
12923
13375
|
authLogger.service.warn("Password reset session refused", { reason: "account is no longer active" });
|
|
12924
13376
|
throw new PasswordResetSessionError();
|
|
12925
13377
|
}
|
|
12926
|
-
const
|
|
13378
|
+
const registered = await replaceCredentials(row, user, params);
|
|
12927
13379
|
onAfterCommit4(() => authPasswordResetEvent.emit({
|
|
12928
13380
|
userId: String(user.id),
|
|
12929
13381
|
email: row.email
|
|
12930
13382
|
}));
|
|
13383
|
+
if (registered.pending) {
|
|
13384
|
+
return { mfaRequired: true, challenge: registered.challenge };
|
|
13385
|
+
}
|
|
13386
|
+
await updateLastLoginService(user.id);
|
|
12931
13387
|
return {
|
|
13388
|
+
mfaRequired: false,
|
|
12932
13389
|
userId: String(user.id),
|
|
12933
13390
|
publicId: user.publicId,
|
|
12934
13391
|
email: user.email || void 0,
|
|
12935
13392
|
phone: user.phone || void 0,
|
|
12936
|
-
...
|
|
13393
|
+
...loginBindingFields(registered)
|
|
12937
13394
|
};
|
|
12938
13395
|
}
|
|
12939
13396
|
|
|
@@ -13219,16 +13676,20 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13219
13676
|
});
|
|
13220
13677
|
await updateLastLoginService(user.id);
|
|
13221
13678
|
const result = {
|
|
13679
|
+
// A device-code approval is itself a second factor: the owner read the
|
|
13680
|
+
// code on a device that is already signed in and said yes, so this
|
|
13681
|
+
// channel never steps up (#95).
|
|
13682
|
+
mfaRequired: false,
|
|
13222
13683
|
userId: String(user.id),
|
|
13223
13684
|
publicId: user.publicId,
|
|
13224
13685
|
email: user.email || void 0,
|
|
13225
13686
|
phone: user.phone || void 0,
|
|
13226
13687
|
passwordChangeRequired: user.passwordChangeRequired,
|
|
13227
|
-
...loginBindingFields(registered)
|
|
13688
|
+
...loginBindingFields(registeredBinding(registered))
|
|
13228
13689
|
};
|
|
13229
|
-
const mfaEnrolled = await mfaEnrolledForUser(
|
|
13690
|
+
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13230
13691
|
onAfterCommit5(() => authLoginEvent.emit({
|
|
13231
|
-
userId:
|
|
13692
|
+
userId: String(user.id),
|
|
13232
13693
|
provider: "device",
|
|
13233
13694
|
email: result.email,
|
|
13234
13695
|
phone: result.phone,
|
|
@@ -13447,16 +13908,19 @@ async function startSession(user, params) {
|
|
|
13447
13908
|
});
|
|
13448
13909
|
await updateLastLoginService(user.id);
|
|
13449
13910
|
const result = {
|
|
13911
|
+
// A passkey sign-in never steps up: the assertion is already a second
|
|
13912
|
+
// factor, so this answer is always the session itself (#95).
|
|
13913
|
+
mfaRequired: false,
|
|
13450
13914
|
userId: String(user.id),
|
|
13451
13915
|
publicId: user.publicId,
|
|
13452
13916
|
email: user.email || void 0,
|
|
13453
13917
|
phone: user.phone || void 0,
|
|
13454
13918
|
passwordChangeRequired: user.passwordChangeRequired,
|
|
13455
|
-
...loginBindingFields(registered)
|
|
13919
|
+
...loginBindingFields(registeredBinding(registered))
|
|
13456
13920
|
};
|
|
13457
13921
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13458
13922
|
onAfterCommit6(() => authLoginEvent.emit({
|
|
13459
|
-
userId:
|
|
13923
|
+
userId: String(user.id),
|
|
13460
13924
|
provider: "passkey",
|
|
13461
13925
|
email: result.email,
|
|
13462
13926
|
phone: result.phone,
|
|
@@ -13727,7 +14191,7 @@ async function getUserPermissions(userId) {
|
|
|
13727
14191
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
13728
14192
|
if (permIds.length > 0) {
|
|
13729
14193
|
const rolePerms = await Promise.all(
|
|
13730
|
-
permIds.map((
|
|
14194
|
+
permIds.map((id26) => permissionsRepository.findById(id26))
|
|
13731
14195
|
);
|
|
13732
14196
|
for (const perm of rolePerms) {
|
|
13733
14197
|
if (perm && perm.isActive) {
|
|
@@ -13945,20 +14409,20 @@ async function acceptInvitation(params) {
|
|
|
13945
14409
|
async function listInvitations(params) {
|
|
13946
14410
|
return await invitationsRepository.list(params);
|
|
13947
14411
|
}
|
|
13948
|
-
async function cancelInvitation(
|
|
13949
|
-
const invitation = await invitationsRepository.findById(
|
|
14412
|
+
async function cancelInvitation(id26, cancelledBy, reason) {
|
|
14413
|
+
const invitation = await invitationsRepository.findById(id26);
|
|
13950
14414
|
if (!invitation) {
|
|
13951
14415
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
13952
14416
|
}
|
|
13953
14417
|
if (invitation.status !== "pending") {
|
|
13954
14418
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
13955
14419
|
}
|
|
13956
|
-
await invitationsRepository.cancel(
|
|
14420
|
+
await invitationsRepository.cancel(id26, cancelledBy, reason, invitation.metadata);
|
|
13957
14421
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
13958
14422
|
}
|
|
13959
|
-
async function deleteInvitation(
|
|
13960
|
-
await invitationsRepository.deleteById(
|
|
13961
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
14423
|
+
async function deleteInvitation(id26) {
|
|
14424
|
+
await invitationsRepository.deleteById(id26);
|
|
14425
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id26}`);
|
|
13962
14426
|
}
|
|
13963
14427
|
async function expireOldInvitations() {
|
|
13964
14428
|
const count2 = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -13967,8 +14431,8 @@ async function expireOldInvitations() {
|
|
|
13967
14431
|
}
|
|
13968
14432
|
return count2;
|
|
13969
14433
|
}
|
|
13970
|
-
async function resendInvitation(
|
|
13971
|
-
const invitation = await invitationsRepository.findById(
|
|
14434
|
+
async function resendInvitation(id26, expiresInDays = 7) {
|
|
14435
|
+
const invitation = await invitationsRepository.findById(id26);
|
|
13972
14436
|
if (!invitation) {
|
|
13973
14437
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
13974
14438
|
}
|
|
@@ -13976,7 +14440,7 @@ async function resendInvitation(id25, expiresInDays = 7) {
|
|
|
13976
14440
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
13977
14441
|
}
|
|
13978
14442
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
13979
|
-
const updated = await invitationsRepository.resend(
|
|
14443
|
+
const updated = await invitationsRepository.resend(id26, newExpiresAt);
|
|
13980
14444
|
if (!updated) {
|
|
13981
14445
|
throw new Error("Failed to update invitation");
|
|
13982
14446
|
}
|
|
@@ -14292,8 +14756,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
14292
14756
|
function registerOAuthProvider(provider) {
|
|
14293
14757
|
registry2.set(provider.id, provider);
|
|
14294
14758
|
}
|
|
14295
|
-
function getOAuthProvider(
|
|
14296
|
-
return registry2.get(
|
|
14759
|
+
function getOAuthProvider(id26) {
|
|
14760
|
+
return registry2.get(id26);
|
|
14297
14761
|
}
|
|
14298
14762
|
function getRegisteredProviders() {
|
|
14299
14763
|
return [...registry2.values()];
|
|
@@ -15140,12 +15604,32 @@ async function oauthCallbackService(params) {
|
|
|
15140
15604
|
channel: "oauth",
|
|
15141
15605
|
ip: params.ip,
|
|
15142
15606
|
userAgent: params.userAgent,
|
|
15143
|
-
binding: decideKeyBinding(user?.sessionBinding ?? "none", params.webProxy)
|
|
15607
|
+
binding: decideKeyBinding(user?.sessionBinding ?? "none", params.webProxy),
|
|
15608
|
+
loginEvent: {
|
|
15609
|
+
provider,
|
|
15610
|
+
email: user?.email || void 0,
|
|
15611
|
+
phone: user?.phone || void 0,
|
|
15612
|
+
metadata: stateData.metadata
|
|
15613
|
+
}
|
|
15144
15614
|
});
|
|
15145
|
-
await updateLastLoginService(userId);
|
|
15146
15615
|
const appUrl = env17.NEXT_PUBLIC_SPFN_APP_URL || env17.SPFN_APP_URL;
|
|
15147
15616
|
const callbackPath = env17.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
15148
15617
|
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
15618
|
+
const returnUrl = isSafeReturnPath(stateData.returnUrl) ? stateData.returnUrl : "/";
|
|
15619
|
+
if (registered.pending) {
|
|
15620
|
+
return {
|
|
15621
|
+
redirectUrl: buildRedirectUrl(callbackUrl, {
|
|
15622
|
+
mfaChallenge: registered.challenge.secret,
|
|
15623
|
+
returnUrl,
|
|
15624
|
+
isNewUser: String(isNewUser)
|
|
15625
|
+
}),
|
|
15626
|
+
userId: String(userId),
|
|
15627
|
+
keyId: stateData.keyId,
|
|
15628
|
+
isNewUser,
|
|
15629
|
+
mfaChallenge: registered.challenge.secret
|
|
15630
|
+
};
|
|
15631
|
+
}
|
|
15632
|
+
await updateLastLoginService(userId);
|
|
15149
15633
|
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
15150
15634
|
userId: String(userId),
|
|
15151
15635
|
keyId: stateData.keyId,
|
|
@@ -15153,7 +15637,7 @@ async function oauthCallbackService(params) {
|
|
|
15153
15637
|
// where it was sealed. Checking it again costs one comparison and keeps a
|
|
15154
15638
|
// destination that leaves the app out of the callback URL no matter which
|
|
15155
15639
|
// seam sealed it.
|
|
15156
|
-
returnUrl
|
|
15640
|
+
returnUrl,
|
|
15157
15641
|
isNewUser: String(isNewUser),
|
|
15158
15642
|
// The callback page and `createOAuthCallbackHandler` are the two seams
|
|
15159
15643
|
// that seal a session out of this redirect, and neither calls a route
|
|
@@ -15395,7 +15879,13 @@ async function persistNativeLogin(identity, params) {
|
|
|
15395
15879
|
isNewUser = result.isNewUser;
|
|
15396
15880
|
}
|
|
15397
15881
|
await assertActiveForOAuthSession(userId);
|
|
15398
|
-
|
|
15882
|
+
const eventPayload = {
|
|
15883
|
+
userId: String(userId),
|
|
15884
|
+
provider: params.provider,
|
|
15885
|
+
email: identity.email || void 0,
|
|
15886
|
+
metadata: params.metadata
|
|
15887
|
+
};
|
|
15888
|
+
const registered = await registerPublicKeyService({
|
|
15399
15889
|
userId,
|
|
15400
15890
|
keyId: params.keyId,
|
|
15401
15891
|
publicKey: params.publicKey,
|
|
@@ -15405,18 +15895,16 @@ async function persistNativeLogin(identity, params) {
|
|
|
15405
15895
|
platform: params.platform,
|
|
15406
15896
|
channel: "oauth-native",
|
|
15407
15897
|
ip: params.ip,
|
|
15408
|
-
userAgent: params.userAgent
|
|
15898
|
+
userAgent: params.userAgent,
|
|
15899
|
+
loginEvent: { provider: params.provider, email: identity.email || void 0, metadata: params.metadata }
|
|
15409
15900
|
});
|
|
15901
|
+
if (registered.pending) {
|
|
15902
|
+
return { mfaRequired: true, challenge: registered.challenge };
|
|
15903
|
+
}
|
|
15410
15904
|
await updateLastLoginService(userId);
|
|
15411
|
-
const eventPayload = {
|
|
15412
|
-
userId: String(userId),
|
|
15413
|
-
provider: params.provider,
|
|
15414
|
-
email: identity.email || void 0,
|
|
15415
|
-
metadata: params.metadata
|
|
15416
|
-
};
|
|
15417
15905
|
const mfaEnrolled = isNewUser ? false : await mfaEnrolledForUser(userId);
|
|
15418
15906
|
onAfterCommit7(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
|
|
15419
|
-
return { userId: String(userId), keyId: params.keyId, isNewUser };
|
|
15907
|
+
return { mfaRequired: false, userId: String(userId), keyId: params.keyId, isNewUser };
|
|
15420
15908
|
}, { context: "auth:oauth-native" });
|
|
15421
15909
|
}
|
|
15422
15910
|
|
|
@@ -15465,8 +15953,8 @@ async function verifyOpsTokenService(token) {
|
|
|
15465
15953
|
scopes: record.scopes
|
|
15466
15954
|
};
|
|
15467
15955
|
}
|
|
15468
|
-
async function revokeOpsTokenService(
|
|
15469
|
-
return await opsTokensRepository.revokeById(
|
|
15956
|
+
async function revokeOpsTokenService(id26) {
|
|
15957
|
+
return await opsTokensRepository.revokeById(id26);
|
|
15470
15958
|
}
|
|
15471
15959
|
async function listOpsTokensService() {
|
|
15472
15960
|
return await opsTokensRepository.list();
|
|
@@ -16324,13 +16812,16 @@ async function finishSessionRenewService(params) {
|
|
|
16324
16812
|
replacesKeyId: key.keyId
|
|
16325
16813
|
});
|
|
16326
16814
|
return {
|
|
16815
|
+
// A renewal replaces the key of a device that is already signed in, so
|
|
16816
|
+
// it is a rotation rather than an arrival and never steps up (#95).
|
|
16817
|
+
mfaRequired: false,
|
|
16327
16818
|
keyId: params.keyId,
|
|
16328
16819
|
userId: String(user.id),
|
|
16329
16820
|
publicId: user.publicId,
|
|
16330
16821
|
email: user.email || void 0,
|
|
16331
16822
|
phone: user.phone || void 0,
|
|
16332
16823
|
passwordChangeRequired: user.passwordChangeRequired,
|
|
16333
|
-
...loginBindingFields(registered)
|
|
16824
|
+
...loginBindingFields(registeredBinding(registered))
|
|
16334
16825
|
};
|
|
16335
16826
|
}
|
|
16336
16827
|
async function admitForRenewal(expiredKeyId) {
|
|
@@ -16579,7 +17070,8 @@ var login = route.post("/_auth/login").input({
|
|
|
16579
17070
|
})
|
|
16580
17071
|
}).use([rateLimitPolicy("auth-login", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
16581
17072
|
const { body } = await c.data();
|
|
16582
|
-
|
|
17073
|
+
const result = await loginService({ ...body, ...deviceProvenance(c.raw) });
|
|
17074
|
+
return result.mfaRequired ? c.accepted(result) : result;
|
|
16583
17075
|
});
|
|
16584
17076
|
var startDeviceAuth = route.post("/_auth/device/start").input({
|
|
16585
17077
|
// Bounded, unlike the interceptor bodies the authenticated enrolment
|
|
@@ -16820,7 +17312,8 @@ var completePasswordReset = route2.post("/_auth/password/reset/complete").input(
|
|
|
16820
17312
|
})
|
|
16821
17313
|
}).use([rateLimitPolicy2("auth-password-reset-complete", { limit: 10, windowMs: 6e4 }), Transactional2()]).skip(["auth"]).handler(async (c) => {
|
|
16822
17314
|
const { body } = await c.data();
|
|
16823
|
-
|
|
17315
|
+
const result = await completePasswordResetService({ ...body, ...deviceProvenance(c.raw) });
|
|
17316
|
+
return result.mfaRequired ? c.accepted(result) : result;
|
|
16824
17317
|
});
|
|
16825
17318
|
|
|
16826
17319
|
// src/server/routes/auth/revoke-all-link.ts
|
|
@@ -16916,6 +17409,36 @@ var mfaStatus = route4.get("/_auth/mfa/status").handler(async (c) => {
|
|
|
16916
17409
|
const { userId } = getAuth(c);
|
|
16917
17410
|
return await mfaStatusService(Number(userId));
|
|
16918
17411
|
});
|
|
17412
|
+
var CHALLENGE_LIMIT = { limit: 10, windowMs: 6e4 };
|
|
17413
|
+
var ChallengeSchema = Type.String({
|
|
17414
|
+
minLength: 16,
|
|
17415
|
+
maxLength: 256,
|
|
17416
|
+
description: "The challenge secret from the 202 sign-in answer or the OAuth callback query"
|
|
17417
|
+
});
|
|
17418
|
+
var mfaVerify = route4.post("/_auth/mfa/verify").input({
|
|
17419
|
+
body: Type.Object({
|
|
17420
|
+
challenge: ChallengeSchema,
|
|
17421
|
+
code: Type.Optional(TotpCodeSchema),
|
|
17422
|
+
recoveryCode: Type.Optional(RecoveryCodeSchema),
|
|
17423
|
+
response: Type.Optional(AssertionSchema)
|
|
17424
|
+
})
|
|
17425
|
+
}).use([rateLimitPolicy4("auth-mfa-verify", CHALLENGE_LIMIT)]).skip(["auth"]).handler(async (c) => {
|
|
17426
|
+
const { body } = await c.data();
|
|
17427
|
+
return await verifyMfaChallengeService({
|
|
17428
|
+
challenge: body.challenge,
|
|
17429
|
+
code: body.code,
|
|
17430
|
+
recoveryCode: body.recoveryCode,
|
|
17431
|
+
response: body.response
|
|
17432
|
+
});
|
|
17433
|
+
});
|
|
17434
|
+
var mfaVerifyOptions = route4.post("/_auth/mfa/verify/options").input({
|
|
17435
|
+
body: Type.Object({
|
|
17436
|
+
challenge: ChallengeSchema
|
|
17437
|
+
})
|
|
17438
|
+
}).use([rateLimitPolicy4("auth-mfa-verify", CHALLENGE_LIMIT)]).skip(["auth"]).handler(async (c) => {
|
|
17439
|
+
const { body } = await c.data();
|
|
17440
|
+
return await startMfaChallengeAssertionService(body.challenge);
|
|
17441
|
+
});
|
|
16919
17442
|
var mfaStepUp = route4.post("/_auth/mfa/step-up").input({
|
|
16920
17443
|
body: Type.Object({
|
|
16921
17444
|
code: Type.Optional(TotpCodeSchema),
|
|
@@ -17035,13 +17558,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
17035
17558
|
var INT64_MIN = -(2n ** 63n);
|
|
17036
17559
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
17037
17560
|
function parseCanonicalJson(bytes) {
|
|
17038
|
-
let
|
|
17561
|
+
let text27;
|
|
17039
17562
|
try {
|
|
17040
|
-
|
|
17563
|
+
text27 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
17041
17564
|
} catch {
|
|
17042
17565
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
17043
17566
|
}
|
|
17044
|
-
const parser = new Parser(
|
|
17567
|
+
const parser = new Parser(text27);
|
|
17045
17568
|
const value = parser.parseValue();
|
|
17046
17569
|
parser.skipWhitespace();
|
|
17047
17570
|
if (!parser.atEnd()) {
|
|
@@ -17062,8 +17585,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
17062
17585
|
return true;
|
|
17063
17586
|
}
|
|
17064
17587
|
var Parser = class {
|
|
17065
|
-
constructor(
|
|
17066
|
-
this.text =
|
|
17588
|
+
constructor(text27) {
|
|
17589
|
+
this.text = text27;
|
|
17067
17590
|
}
|
|
17068
17591
|
pos = 0;
|
|
17069
17592
|
atEnd() {
|
|
@@ -17653,8 +18176,8 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
17653
18176
|
|
|
17654
18177
|
// src/server/client-proof/contract-bundle.ts
|
|
17655
18178
|
init_wire_headers();
|
|
17656
|
-
var CONTRACT_VERSION = "0.
|
|
17657
|
-
var CONTRACT_SUPPORTED_RANGE = ">=0.
|
|
18179
|
+
var CONTRACT_VERSION = "0.13.0";
|
|
18180
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
|
|
17658
18181
|
function required(name, type) {
|
|
17659
18182
|
return { name, type, optional: false };
|
|
17660
18183
|
}
|
|
@@ -17768,18 +18291,100 @@ var CONTRACT_TYPES = [
|
|
|
17768
18291
|
optional("oldKeyId", "string")
|
|
17769
18292
|
]
|
|
17770
18293
|
},
|
|
18294
|
+
/**
|
|
18295
|
+
* The sign-in union, flattened the way the poll union was.
|
|
18296
|
+
*
|
|
18297
|
+
* `mfaRequired` is the discriminant and the only required field; everything
|
|
18298
|
+
* else belongs to one branch. False is a session and carries the five login
|
|
18299
|
+
* fields; true is a 202 carrying `challenge` and nothing else, which the
|
|
18300
|
+
* client spends at `auth.mfa.verify` (#95). This grammar has no union type,
|
|
18301
|
+
* so a required discriminant plus optional fields is the only way to say it
|
|
18302
|
+
* — and the typed web client infers one result type from the same
|
|
18303
|
+
* declaration, so it could not have been a union there either.
|
|
18304
|
+
*/
|
|
17771
18305
|
{
|
|
17772
18306
|
name: "LoginResponse",
|
|
17773
18307
|
fields: [
|
|
17774
|
-
required("
|
|
17775
|
-
|
|
18308
|
+
required("mfaRequired", "boolean"),
|
|
18309
|
+
optional("challenge", "MfaChallenge"),
|
|
18310
|
+
optional("userId", "string"),
|
|
18311
|
+
optional("publicId", "string"),
|
|
18312
|
+
optional("email", "string"),
|
|
18313
|
+
optional("phone", "string"),
|
|
18314
|
+
optional("passwordChangeRequired", "boolean"),
|
|
18315
|
+
optional("sessionBinding", "KeyBinding"),
|
|
18316
|
+
optional("keyExpiresAtMillis", "integer")
|
|
18317
|
+
]
|
|
18318
|
+
},
|
|
18319
|
+
/**
|
|
18320
|
+
* What a sign-in hands back instead of a session when the account has a
|
|
18321
|
+
* second factor and this device is new to it.
|
|
18322
|
+
*
|
|
18323
|
+
* `secret` is the challenge itself, returned once. The server stores only its
|
|
18324
|
+
* hash and addresses the row by that, so this value is not recoverable from
|
|
18325
|
+
* the database and is not a bearer credential for anything but the one
|
|
18326
|
+
* `auth.mfa.verify` call it belongs to.
|
|
18327
|
+
*/
|
|
18328
|
+
{
|
|
18329
|
+
name: "MfaChallenge",
|
|
18330
|
+
fields: [
|
|
18331
|
+
required("secret", "string"),
|
|
18332
|
+
required("expiresAtMillis", "integer")
|
|
18333
|
+
]
|
|
18334
|
+
},
|
|
18335
|
+
/**
|
|
18336
|
+
* Exactly one of `code` and `recoveryCode`, beside the challenge.
|
|
18337
|
+
*
|
|
18338
|
+
* The third form the server accepts — an assertion from a passkey the owner
|
|
18339
|
+
* marked as a second factor — is not declared: a WebAuthn assertion is a
|
|
18340
|
+
* nested browser object outside this grammar, and the passkey ceremonies are
|
|
18341
|
+
* not on this surface for the same reason.
|
|
18342
|
+
*/
|
|
18343
|
+
{
|
|
18344
|
+
name: "MfaVerifyRequest",
|
|
18345
|
+
fields: [
|
|
18346
|
+
required("challenge", "string"),
|
|
18347
|
+
optional("code", "string"),
|
|
18348
|
+
optional("recoveryCode", "string")
|
|
18349
|
+
]
|
|
18350
|
+
},
|
|
18351
|
+
/**
|
|
18352
|
+
* The sign-in the challenge was standing in for, plus what the proxy needs.
|
|
18353
|
+
*
|
|
18354
|
+
* `mfaRequired` is false on every 200 here — the step-up just happened — and
|
|
18355
|
+
* it is carried so the body is the same `LoginResponse` shape a direct
|
|
18356
|
+
* sign-in answers. `keyId` and `challengeHash` are for the Next.js proxy: it
|
|
18357
|
+
* seals a session only when both match the pending cookie it baked at the
|
|
18358
|
+
* 202, which is what stops a cookie from one flow sealing a session for
|
|
18359
|
+
* another's key.
|
|
18360
|
+
*/
|
|
18361
|
+
{
|
|
18362
|
+
name: "MfaVerifyResponse",
|
|
18363
|
+
fields: [
|
|
18364
|
+
required("mfaRequired", "boolean"),
|
|
18365
|
+
required("keyId", "string"),
|
|
18366
|
+
required("challengeHash", "string"),
|
|
18367
|
+
optional("userId", "string"),
|
|
18368
|
+
optional("publicId", "string"),
|
|
17776
18369
|
optional("email", "string"),
|
|
17777
18370
|
optional("phone", "string"),
|
|
17778
|
-
|
|
18371
|
+
optional("passwordChangeRequired", "boolean"),
|
|
17779
18372
|
optional("sessionBinding", "KeyBinding"),
|
|
17780
18373
|
optional("keyExpiresAtMillis", "integer")
|
|
17781
18374
|
]
|
|
17782
18375
|
},
|
|
18376
|
+
/**
|
|
18377
|
+
* What the account has enrolled. No secret, no otpauth URI, no recovery code
|
|
18378
|
+
* — only the counts and names an account screen renders.
|
|
18379
|
+
*/
|
|
18380
|
+
{
|
|
18381
|
+
name: "MfaStatusResponse",
|
|
18382
|
+
fields: [
|
|
18383
|
+
required("enrolled", "boolean"),
|
|
18384
|
+
required("methods", "array<MfaMethod>"),
|
|
18385
|
+
required("recoveryCodesRemaining", "integer")
|
|
18386
|
+
]
|
|
18387
|
+
},
|
|
17783
18388
|
{
|
|
17784
18389
|
name: "OauthNativeRequest",
|
|
17785
18390
|
fields: [
|
|
@@ -17792,12 +18397,20 @@ var CONTRACT_TYPES = [
|
|
|
17792
18397
|
required("algorithm", "KeyAlgorithm")
|
|
17793
18398
|
]
|
|
17794
18399
|
},
|
|
18400
|
+
/**
|
|
18401
|
+
* Flattened on the same terms as `LoginResponse`: a native social sign-in on
|
|
18402
|
+
* an enrolled account and a device it has not seen answers 202 with a
|
|
18403
|
+
* challenge rather than a key, so the discriminant is required and the three
|
|
18404
|
+
* login fields are the false branch.
|
|
18405
|
+
*/
|
|
17795
18406
|
{
|
|
17796
18407
|
name: "OauthNativeResponse",
|
|
17797
18408
|
fields: [
|
|
17798
|
-
required("
|
|
17799
|
-
|
|
17800
|
-
|
|
18409
|
+
required("mfaRequired", "boolean"),
|
|
18410
|
+
optional("challenge", "MfaChallenge"),
|
|
18411
|
+
optional("userId", "string"),
|
|
18412
|
+
optional("keyId", "string"),
|
|
18413
|
+
optional("isNewUser", "boolean")
|
|
17801
18414
|
]
|
|
17802
18415
|
},
|
|
17803
18416
|
{
|
|
@@ -17915,6 +18528,7 @@ var CONTRACT_TYPES = [
|
|
|
17915
18528
|
fields: [
|
|
17916
18529
|
required("status", "DeviceAuthPollStatus"),
|
|
17917
18530
|
optional("intervalMillis", "integer"),
|
|
18531
|
+
optional("mfaRequired", "boolean"),
|
|
17918
18532
|
optional("userId", "string"),
|
|
17919
18533
|
optional("publicId", "string"),
|
|
17920
18534
|
optional("email", "string"),
|
|
@@ -17963,7 +18577,12 @@ var CONTRACT_ENUMS = [
|
|
|
17963
18577
|
{ name: "KeyAlgorithm", values: [...KEY_ALGORITHM] },
|
|
17964
18578
|
{ name: "KeyPlatform", values: [...KEY_PLATFORM] },
|
|
17965
18579
|
{ name: "KeyBinding", values: [...SESSION_BINDINGS] },
|
|
17966
|
-
{ name: "DeviceAuthPollStatus", values: ["pending", "approved"] }
|
|
18580
|
+
{ name: "DeviceAuthPollStatus", values: ["pending", "approved"] },
|
|
18581
|
+
// The two things an account screen can show as enrolled. `recovery` is a
|
|
18582
|
+
// verification method but never a method the account *has* — a recovery code
|
|
18583
|
+
// is what is left when the authenticator is not to hand, so `mfa/status`
|
|
18584
|
+
// reports it as a count and not as a factor.
|
|
18585
|
+
{ name: "MfaMethod", values: ["totp", "passkey"] }
|
|
17967
18586
|
];
|
|
17968
18587
|
var BUNDLE_FILENAME = "spfn-mobile-contract.json";
|
|
17969
18588
|
var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
|
|
@@ -19043,8 +19662,8 @@ function bearerOf(header) {
|
|
|
19043
19662
|
function chain(handlers) {
|
|
19044
19663
|
return async (c, next) => {
|
|
19045
19664
|
let answer;
|
|
19046
|
-
const runFrom = async (
|
|
19047
|
-
const produced =
|
|
19665
|
+
const runFrom = async (index21) => {
|
|
19666
|
+
const produced = index21 < handlers.length ? await handlers[index21](c, () => runFrom(index21 + 1)) : await next();
|
|
19048
19667
|
if (produced instanceof Response) {
|
|
19049
19668
|
answer = produced;
|
|
19050
19669
|
}
|
|
@@ -19653,8 +20272,13 @@ var getGoogleOAuthUrl = route10.post("/_auth/oauth/google/url").input({
|
|
|
19653
20272
|
});
|
|
19654
20273
|
var oauthFinalize = route10.post("/_auth/oauth/finalize").input({
|
|
19655
20274
|
body: Type.Object({
|
|
19656
|
-
userId: Type.String({ description: "User ID from OAuth callback" }),
|
|
19657
|
-
keyId: Type.String({ description: "Key ID from OAuth state" }),
|
|
20275
|
+
userId: Type.Optional(Type.String({ description: "User ID from OAuth callback" })),
|
|
20276
|
+
keyId: Type.Optional(Type.String({ description: "Key ID from OAuth state" })),
|
|
20277
|
+
mfaChallenge: Type.Optional(Type.String({
|
|
20278
|
+
minLength: 16,
|
|
20279
|
+
maxLength: 256,
|
|
20280
|
+
description: "Second-factor challenge from the callback query, when the callback carried one"
|
|
20281
|
+
})),
|
|
19658
20282
|
returnUrl: Type.Optional(Type.String({ description: "URL to redirect after login" }))
|
|
19659
20283
|
})
|
|
19660
20284
|
}).use([rateLimitPolicy10("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
@@ -19662,8 +20286,20 @@ var oauthFinalize = route10.post("/_auth/oauth/finalize").input({
|
|
|
19662
20286
|
if (body.returnUrl && !isSafeReturnPath(body.returnUrl)) {
|
|
19663
20287
|
throw new ValidationError17({ message: "returnUrl must be a relative path within the app" });
|
|
19664
20288
|
}
|
|
20289
|
+
if (body.mfaChallenge) {
|
|
20290
|
+
return c.accepted({
|
|
20291
|
+
success: true,
|
|
20292
|
+
mfaRequired: true,
|
|
20293
|
+
challenge: body.mfaChallenge,
|
|
20294
|
+
returnUrl: body.returnUrl || "/"
|
|
20295
|
+
});
|
|
20296
|
+
}
|
|
20297
|
+
if (!body.userId || !body.keyId) {
|
|
20298
|
+
throw new ValidationError17({ message: "userId and keyId are required unless mfaChallenge is sent" });
|
|
20299
|
+
}
|
|
19665
20300
|
return {
|
|
19666
20301
|
success: true,
|
|
20302
|
+
mfaRequired: false,
|
|
19667
20303
|
userId: body.userId,
|
|
19668
20304
|
keyId: body.keyId,
|
|
19669
20305
|
returnUrl: body.returnUrl || "/",
|
|
@@ -19784,7 +20420,8 @@ var oauthNative = route10.post("/_auth/oauth/:provider/native").input({
|
|
|
19784
20420
|
by: byIpAndIdToken({ ipLimit: 20 })
|
|
19785
20421
|
})]).skip(["auth"]).handler(async (c) => {
|
|
19786
20422
|
const { params, body } = await c.data();
|
|
19787
|
-
|
|
20423
|
+
const result = await oauthNativeService({ provider: params.provider, ...body, ...deviceProvenance(c.raw) });
|
|
20424
|
+
return result.mfaRequired ? c.accepted(result) : result;
|
|
19788
20425
|
});
|
|
19789
20426
|
async function collectUnlinkNotifyFields(raw) {
|
|
19790
20427
|
const fields = { ...raw.req.query() };
|
|
@@ -20258,6 +20895,8 @@ var mainAuthRouter = defineRouter6({
|
|
|
20258
20895
|
mfaStatus,
|
|
20259
20896
|
mfaStepUp,
|
|
20260
20897
|
mfaStepUpOptions,
|
|
20898
|
+
mfaVerify,
|
|
20899
|
+
mfaVerifyOptions,
|
|
20261
20900
|
logout,
|
|
20262
20901
|
rotateKey,
|
|
20263
20902
|
listKeys,
|
|
@@ -20804,8 +21443,9 @@ var DEFAULT_MFA_SWEEP_CRON = "0 7 * * *";
|
|
|
20804
21443
|
function createMfaSweepJob(cronExpression = DEFAULT_MFA_SWEEP_CRON) {
|
|
20805
21444
|
return job5("auth.mfa.sweep").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
20806
21445
|
const { deleted } = await sweepUnconfirmedMfaService();
|
|
20807
|
-
|
|
20808
|
-
|
|
21446
|
+
const { deleted: challenges } = await sweepMfaChallengesService();
|
|
21447
|
+
if (deleted > 0 || challenges > 0) {
|
|
21448
|
+
authLogger.service.info("[auth.mfa.sweep] sweep complete", { deleted, challenges });
|
|
20809
21449
|
}
|
|
20810
21450
|
});
|
|
20811
21451
|
}
|
|
@@ -20859,8 +21499,11 @@ export {
|
|
|
20859
21499
|
KeyRevokeAllTokensRepository,
|
|
20860
21500
|
KeysRepository,
|
|
20861
21501
|
MAX_UNGRANTED_CLIENTS_PER_IP,
|
|
21502
|
+
MFA_CHALLENGE_ATTEMPT_LIMIT,
|
|
21503
|
+
MFA_CHALLENGE_CHANNELS,
|
|
20862
21504
|
MFA_CONFIRM_ATTEMPT_LIMIT,
|
|
20863
21505
|
MFA_VERIFICATION_METHODS,
|
|
21506
|
+
MfaChallengesRepository,
|
|
20864
21507
|
MfaEnrolmentRepository,
|
|
20865
21508
|
MfaRecoveryCodesRepository,
|
|
20866
21509
|
MfaTotpRepository,
|
|
@@ -20925,6 +21568,7 @@ export {
|
|
|
20925
21568
|
assertPasskeyConfig,
|
|
20926
21569
|
assertRecentAuthentication,
|
|
20927
21570
|
assertStepUp,
|
|
21571
|
+
attemptSecondFactor,
|
|
20928
21572
|
authDeletionCancelledEvent,
|
|
20929
21573
|
authDeletionCompletedEvent,
|
|
20930
21574
|
authDeletionRequestedEvent,
|
|
@@ -21092,6 +21736,8 @@ export {
|
|
|
21092
21736
|
matchOAuthCsrfCookies,
|
|
21093
21737
|
matchesCsrfToken,
|
|
21094
21738
|
matchesRegisteredRedirectUri,
|
|
21739
|
+
mfaChallenges,
|
|
21740
|
+
mfaChallengesRepository,
|
|
21095
21741
|
mfaEnrolledForUser,
|
|
21096
21742
|
mfaEnrolmentRepository,
|
|
21097
21743
|
mfaRecoveryCodes,
|
|
@@ -21122,6 +21768,7 @@ export {
|
|
|
21122
21768
|
oauthUnlinkNotifyService,
|
|
21123
21769
|
oauthUnlinkedEvent,
|
|
21124
21770
|
oneTimeTokenAuth,
|
|
21771
|
+
openStepUpChallengeService,
|
|
21125
21772
|
opsOrUser,
|
|
21126
21773
|
opsTokenAuth,
|
|
21127
21774
|
opsTokens,
|
|
@@ -21149,6 +21796,7 @@ export {
|
|
|
21149
21796
|
registerOAuthProvider,
|
|
21150
21797
|
registerPublicKeyService,
|
|
21151
21798
|
registerService,
|
|
21799
|
+
registeredBinding,
|
|
21152
21800
|
removePermissionFromRole,
|
|
21153
21801
|
renamePasskeyService,
|
|
21154
21802
|
requestAccountDeletionService,
|
|
@@ -21163,6 +21811,7 @@ export {
|
|
|
21163
21811
|
resendInvitation,
|
|
21164
21812
|
resolveAuthenticatedUser,
|
|
21165
21813
|
resolveIssuerSource,
|
|
21814
|
+
resumeStepUpChallengeService,
|
|
21166
21815
|
revokeAllKeysService,
|
|
21167
21816
|
revokeAllOAuth2GrantsForUser,
|
|
21168
21817
|
revokeKeyService,
|
|
@@ -21191,6 +21840,7 @@ export {
|
|
|
21191
21840
|
signupLinkTokensRepository,
|
|
21192
21841
|
socialAccountsRepository,
|
|
21193
21842
|
startDeviceAuthService,
|
|
21843
|
+
startMfaChallengeAssertionService,
|
|
21194
21844
|
startPasskeyEnrollmentService,
|
|
21195
21845
|
startPasskeyLoginService,
|
|
21196
21846
|
startSessionBindingDisableService,
|
|
@@ -21199,6 +21849,7 @@ export {
|
|
|
21199
21849
|
startTotpEnrolmentService,
|
|
21200
21850
|
stepUpService,
|
|
21201
21851
|
sweepDuePurges,
|
|
21852
|
+
sweepMfaChallengesService,
|
|
21202
21853
|
sweepUnconfirmedMfaService,
|
|
21203
21854
|
toEpochSeconds,
|
|
21204
21855
|
unsealSession,
|
|
@@ -21226,6 +21877,7 @@ export {
|
|
|
21226
21877
|
verifyCodeService,
|
|
21227
21878
|
verifyIdToken,
|
|
21228
21879
|
verifyKeyFingerprint,
|
|
21880
|
+
verifyMfaChallengeService,
|
|
21229
21881
|
verifyOAuthState,
|
|
21230
21882
|
verifyOneTimeTokenService,
|
|
21231
21883
|
verifyOpsTokenService,
|