@spfn/auth 0.3.0-beta.26 → 0.3.0-beta.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -9
- package/dist/client-proof.d.ts +8 -1
- package/dist/client-proof.js +58 -2
- package/dist/client-proof.js.map +1 -1
- package/dist/config.d.ts +6 -6
- package/dist/errors.d.ts +104 -2
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +504 -446
- package/dist/index.js +114 -40
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-CdEgxOB1.d.ts → machine-principals-B0bjs-0K.d.ts} +914 -285
- package/dist/server.d.ts +430 -175
- package/dist/server.js +1710 -655
- package/dist/server.js.map +1 -1
- package/migrations/20260926054221_friendly_hitman/migration.sql +28 -0
- package/migrations/20260926054221_friendly_hitman/snapshot.json +6691 -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(index22) {
|
|
901
|
+
return CreateType({ [Kind]: "Argument", index: index22 });
|
|
902
902
|
}
|
|
903
903
|
var init_argument = __esm({
|
|
904
904
|
"../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/argument/argument.mjs"() {
|
|
@@ -1136,28 +1136,28 @@ var init_union2 = __esm({
|
|
|
1136
1136
|
function Unescape(pattern) {
|
|
1137
1137
|
return pattern.replace(/\\\$/g, "$").replace(/\\\*/g, "*").replace(/\\\^/g, "^").replace(/\\\|/g, "|").replace(/\\\(/g, "(").replace(/\\\)/g, ")");
|
|
1138
1138
|
}
|
|
1139
|
-
function IsNonEscaped(pattern,
|
|
1140
|
-
return pattern[
|
|
1139
|
+
function IsNonEscaped(pattern, index22, char) {
|
|
1140
|
+
return pattern[index22] === char && pattern.charCodeAt(index22 - 1) !== 92;
|
|
1141
1141
|
}
|
|
1142
|
-
function IsOpenParen(pattern,
|
|
1143
|
-
return IsNonEscaped(pattern,
|
|
1142
|
+
function IsOpenParen(pattern, index22) {
|
|
1143
|
+
return IsNonEscaped(pattern, index22, "(");
|
|
1144
1144
|
}
|
|
1145
|
-
function IsCloseParen(pattern,
|
|
1146
|
-
return IsNonEscaped(pattern,
|
|
1145
|
+
function IsCloseParen(pattern, index22) {
|
|
1146
|
+
return IsNonEscaped(pattern, index22, ")");
|
|
1147
1147
|
}
|
|
1148
|
-
function IsSeparator(pattern,
|
|
1149
|
-
return IsNonEscaped(pattern,
|
|
1148
|
+
function IsSeparator(pattern, index22) {
|
|
1149
|
+
return IsNonEscaped(pattern, index22, "|");
|
|
1150
1150
|
}
|
|
1151
1151
|
function IsGroup(pattern) {
|
|
1152
1152
|
if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1)))
|
|
1153
1153
|
return false;
|
|
1154
1154
|
let count2 = 0;
|
|
1155
|
-
for (let
|
|
1156
|
-
if (IsOpenParen(pattern,
|
|
1155
|
+
for (let index22 = 0; index22 < pattern.length; index22++) {
|
|
1156
|
+
if (IsOpenParen(pattern, index22))
|
|
1157
1157
|
count2 += 1;
|
|
1158
|
-
if (IsCloseParen(pattern,
|
|
1158
|
+
if (IsCloseParen(pattern, index22))
|
|
1159
1159
|
count2 -= 1;
|
|
1160
|
-
if (count2 === 0 &&
|
|
1160
|
+
if (count2 === 0 && index22 !== pattern.length - 1)
|
|
1161
1161
|
return false;
|
|
1162
1162
|
}
|
|
1163
1163
|
return true;
|
|
@@ -1167,19 +1167,19 @@ function InGroup(pattern) {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
function IsPrecedenceOr(pattern) {
|
|
1169
1169
|
let count2 = 0;
|
|
1170
|
-
for (let
|
|
1171
|
-
if (IsOpenParen(pattern,
|
|
1170
|
+
for (let index22 = 0; index22 < pattern.length; index22++) {
|
|
1171
|
+
if (IsOpenParen(pattern, index22))
|
|
1172
1172
|
count2 += 1;
|
|
1173
|
-
if (IsCloseParen(pattern,
|
|
1173
|
+
if (IsCloseParen(pattern, index22))
|
|
1174
1174
|
count2 -= 1;
|
|
1175
|
-
if (IsSeparator(pattern,
|
|
1175
|
+
if (IsSeparator(pattern, index22) && count2 === 0)
|
|
1176
1176
|
return true;
|
|
1177
1177
|
}
|
|
1178
1178
|
return false;
|
|
1179
1179
|
}
|
|
1180
1180
|
function IsPrecedenceAnd(pattern) {
|
|
1181
|
-
for (let
|
|
1182
|
-
if (IsOpenParen(pattern,
|
|
1181
|
+
for (let index22 = 0; index22 < pattern.length; index22++) {
|
|
1182
|
+
if (IsOpenParen(pattern, index22))
|
|
1183
1183
|
return true;
|
|
1184
1184
|
}
|
|
1185
1185
|
return false;
|
|
@@ -1187,16 +1187,16 @@ function IsPrecedenceAnd(pattern) {
|
|
|
1187
1187
|
function Or(pattern) {
|
|
1188
1188
|
let [count2, start] = [0, 0];
|
|
1189
1189
|
const expressions = [];
|
|
1190
|
-
for (let
|
|
1191
|
-
if (IsOpenParen(pattern,
|
|
1190
|
+
for (let index22 = 0; index22 < pattern.length; index22++) {
|
|
1191
|
+
if (IsOpenParen(pattern, index22))
|
|
1192
1192
|
count2 += 1;
|
|
1193
|
-
if (IsCloseParen(pattern,
|
|
1193
|
+
if (IsCloseParen(pattern, index22))
|
|
1194
1194
|
count2 -= 1;
|
|
1195
|
-
if (IsSeparator(pattern,
|
|
1196
|
-
const range2 = pattern.slice(start,
|
|
1195
|
+
if (IsSeparator(pattern, index22) && count2 === 0) {
|
|
1196
|
+
const range2 = pattern.slice(start, index22);
|
|
1197
1197
|
if (range2.length > 0)
|
|
1198
1198
|
expressions.push(TemplateLiteralParse(range2));
|
|
1199
|
-
start =
|
|
1199
|
+
start = index22 + 1;
|
|
1200
1200
|
}
|
|
1201
1201
|
}
|
|
1202
1202
|
const range = pattern.slice(start);
|
|
@@ -1209,40 +1209,40 @@ function Or(pattern) {
|
|
|
1209
1209
|
return { type: "or", expr: expressions };
|
|
1210
1210
|
}
|
|
1211
1211
|
function And(pattern) {
|
|
1212
|
-
function Group(value,
|
|
1213
|
-
if (!IsOpenParen(value,
|
|
1212
|
+
function Group(value, index22) {
|
|
1213
|
+
if (!IsOpenParen(value, index22))
|
|
1214
1214
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
|
|
1215
1215
|
let count2 = 0;
|
|
1216
|
-
for (let scan =
|
|
1216
|
+
for (let scan = index22; scan < value.length; scan++) {
|
|
1217
1217
|
if (IsOpenParen(value, scan))
|
|
1218
1218
|
count2 += 1;
|
|
1219
1219
|
if (IsCloseParen(value, scan))
|
|
1220
1220
|
count2 -= 1;
|
|
1221
1221
|
if (count2 === 0)
|
|
1222
|
-
return [
|
|
1222
|
+
return [index22, 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, index22) {
|
|
1227
|
+
for (let scan = index22; scan < pattern2.length; scan++) {
|
|
1228
1228
|
if (IsOpenParen(pattern2, scan))
|
|
1229
|
-
return [
|
|
1229
|
+
return [index22, scan];
|
|
1230
1230
|
}
|
|
1231
|
-
return [
|
|
1231
|
+
return [index22, pattern2.length];
|
|
1232
1232
|
}
|
|
1233
1233
|
const expressions = [];
|
|
1234
|
-
for (let
|
|
1235
|
-
if (IsOpenParen(pattern,
|
|
1236
|
-
const [start, end] = Group(pattern,
|
|
1234
|
+
for (let index22 = 0; index22 < pattern.length; index22++) {
|
|
1235
|
+
if (IsOpenParen(pattern, index22)) {
|
|
1236
|
+
const [start, end] = Group(pattern, index22);
|
|
1237
1237
|
const range = pattern.slice(start, end + 1);
|
|
1238
1238
|
expressions.push(TemplateLiteralParse(range));
|
|
1239
|
-
|
|
1239
|
+
index22 = end;
|
|
1240
1240
|
} else {
|
|
1241
|
-
const [start, end] = Range(pattern,
|
|
1241
|
+
const [start, end] = Range(pattern, index22);
|
|
1242
1242
|
const range = pattern.slice(start, end);
|
|
1243
1243
|
if (range.length > 0)
|
|
1244
1244
|
expressions.push(TemplateLiteralParse(range));
|
|
1245
|
-
|
|
1245
|
+
index22 = end - 1;
|
|
1246
1246
|
}
|
|
1247
1247
|
}
|
|
1248
1248
|
return expressions.length === 0 ? { type: "const", const: "" } : expressions.length === 1 ? expressions[0] : { type: "and", expr: expressions };
|
|
@@ -2577,13 +2577,13 @@ function FromBoolean(left, right) {
|
|
|
2577
2577
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsBoolean(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2578
2578
|
}
|
|
2579
2579
|
function FromConstructor(left, right) {
|
|
2580
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2580
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsConstructor(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index22) => IntoBooleanResult(Visit3(right.parameters[index22], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2581
2581
|
}
|
|
2582
2582
|
function FromDate(left, right) {
|
|
2583
2583
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsDate(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
2584
2584
|
}
|
|
2585
2585
|
function FromFunction(left, right) {
|
|
2586
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema,
|
|
2586
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : !type_exports.IsFunction(right) ? ExtendsResult.False : left.parameters.length > right.parameters.length ? ExtendsResult.False : !left.parameters.every((schema, index22) => IntoBooleanResult(Visit3(right.parameters[index22], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
|
|
2587
2587
|
}
|
|
2588
2588
|
function FromIntegerRight(left, right) {
|
|
2589
2589
|
return type_exports.IsLiteral(left) && value_exports.IsNumber(left.const) ? ExtendsResult.True : type_exports.IsNumber(left) || type_exports.IsInteger(left) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -2743,7 +2743,7 @@ function FromTupleRight(left, right) {
|
|
|
2743
2743
|
return type_exports.IsNever(left) ? ExtendsResult.True : type_exports.IsUnknown(left) ? ExtendsResult.False : type_exports.IsAny(left) ? ExtendsResult.Union : ExtendsResult.False;
|
|
2744
2744
|
}
|
|
2745
2745
|
function FromTuple3(left, right) {
|
|
2746
|
-
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema,
|
|
2746
|
+
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) && IsObjectArrayLike(right) ? ExtendsResult.True : type_exports.IsArray(right) && IsArrayOfTuple(left, right) ? ExtendsResult.True : !type_exports.IsTuple(right) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) || !value_exports.IsUndefined(left.items) && value_exports.IsUndefined(right.items) ? ExtendsResult.False : value_exports.IsUndefined(left.items) && !value_exports.IsUndefined(right.items) ? ExtendsResult.True : left.items.every((schema, index22) => Visit3(schema, right.items[index22]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
|
|
2747
2747
|
}
|
|
2748
2748
|
function FromUint8Array(left, right) {
|
|
2749
2749
|
return IsStructuralRight(right) ? StructuralRight(left, right) : type_exports.IsObject(right) ? FromObjectRight(left, right) : type_exports.IsRecord(right) ? FromRecordRight(left, right) : type_exports.IsUint8Array(right) ? ExtendsResult.True : ExtendsResult.False;
|
|
@@ -4474,7 +4474,7 @@ var init_types = __esm({
|
|
|
4474
4474
|
|
|
4475
4475
|
// src/server/routes/schema.ts
|
|
4476
4476
|
import { EMAIL_PATTERN, PHONE_PATTERN } from "@spfn/auth";
|
|
4477
|
-
var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4477
|
+
var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PublicKeySchema, KeyIdSchema, FingerprintSchema, UserCodeSchema, LinkIdSchema, MatchChoiceSchema, DeviceAuthPollResponseSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4478
4478
|
var init_schema3 = __esm({
|
|
4479
4479
|
"src/server/routes/schema.ts"() {
|
|
4480
4480
|
"use strict";
|
|
@@ -4513,6 +4513,16 @@ var init_schema3 = __esm({
|
|
|
4513
4513
|
maxLength: 16,
|
|
4514
4514
|
description: "Device user code as displayed, e.g. WXYZ-2345. Dashes, spaces and case are ignored."
|
|
4515
4515
|
});
|
|
4516
|
+
LinkIdSchema = Type.String({
|
|
4517
|
+
minLength: 1,
|
|
4518
|
+
maxLength: 64,
|
|
4519
|
+
description: "Device link handle returned by /_auth/device/link/issue"
|
|
4520
|
+
});
|
|
4521
|
+
MatchChoiceSchema = Type.Integer({
|
|
4522
|
+
minimum: 10,
|
|
4523
|
+
maximum: 99,
|
|
4524
|
+
description: "The number the issuer picked \u2014 the one the new device shows"
|
|
4525
|
+
});
|
|
4516
4526
|
DeviceAuthPollResponseSchema = Type.Union([
|
|
4517
4527
|
Type.Object({
|
|
4518
4528
|
status: Type.Literal("pending"),
|
|
@@ -5615,9 +5625,86 @@ var init_device_authorizations = __esm({
|
|
|
5615
5625
|
}
|
|
5616
5626
|
});
|
|
5617
5627
|
|
|
5628
|
+
// src/server/entities/device-links.ts
|
|
5629
|
+
import { integer as integer9, text as text17, uniqueIndex as uniqueIndex9, index as index14 } from "drizzle-orm/pg-core";
|
|
5630
|
+
import { id as id16, timestamps as timestamps14, enumText as enumText10, utcTimestamp as utcTimestamp15, foreignKey as foreignKey12 } from "@spfn/core/db";
|
|
5631
|
+
var DEVICE_LINK_STATUSES, deviceLinks;
|
|
5632
|
+
var init_device_links = __esm({
|
|
5633
|
+
"src/server/entities/device-links.ts"() {
|
|
5634
|
+
"use strict";
|
|
5635
|
+
init_types();
|
|
5636
|
+
init_users();
|
|
5637
|
+
init_schema4();
|
|
5638
|
+
DEVICE_LINK_STATUSES = ["issued", "redeemed", "approved", "denied", "consumed", "expired"];
|
|
5639
|
+
deviceLinks = authSchema.table(
|
|
5640
|
+
"device_links",
|
|
5641
|
+
{
|
|
5642
|
+
id: id16(),
|
|
5643
|
+
// The issuer's handle on the record, returned by issue and named by
|
|
5644
|
+
// status, confirm, deny and cancel
|
|
5645
|
+
// Random rather than the row id so a handle says nothing about how many
|
|
5646
|
+
// links exist; it authorizes nothing without the issuing key beside it
|
|
5647
|
+
linkId: text17("link_id").notNull().unique(),
|
|
5648
|
+
// The code the issuer shows and the new device sends back
|
|
5649
|
+
// Stored normalized — uppercase, no dash — as device_authorizations does
|
|
5650
|
+
// Plaintext on purpose: redeeming it registers nothing until the issuer
|
|
5651
|
+
// picks the match number the redeeming device shows
|
|
5652
|
+
userCode: text17("user_code").notNull(),
|
|
5653
|
+
// The account the new device joins, written at issue time from the
|
|
5654
|
+
// issuer's session. Never taken from a request body
|
|
5655
|
+
issuerUserId: foreignKey12("issuer_user", () => users.id, { onDelete: "cascade" }),
|
|
5656
|
+
// The key that signed the issue request
|
|
5657
|
+
// Only requests signed by this key can see or answer the link, and the
|
|
5658
|
+
// link is judged expired once this key is revoked or runs out
|
|
5659
|
+
issuerKeyId: text17("issuer_key_id").notNull(),
|
|
5660
|
+
// SHA-256 of the device code, hex — set at redeem, null before
|
|
5661
|
+
// The code itself is returned to the redeeming device once and never stored
|
|
5662
|
+
deviceCodeHash: text17("device_code_hash").unique(),
|
|
5663
|
+
// Key material the redeeming device generated, same shapes as user_public_keys
|
|
5664
|
+
// null until redeem; parked, not registered, until a poll moves it over
|
|
5665
|
+
publicKey: text17("public_key"),
|
|
5666
|
+
keyId: text17("key_id"),
|
|
5667
|
+
fingerprint: text17("fingerprint"),
|
|
5668
|
+
algorithm: enumText10("algorithm", KEY_ALGORITHM),
|
|
5669
|
+
// Labels the redeeming device supplied, shown to the issuer
|
|
5670
|
+
// Display only — the match number is what the decision rests on
|
|
5671
|
+
deviceName: text17("device_name"),
|
|
5672
|
+
platform: enumText10("platform", KEY_PLATFORM),
|
|
5673
|
+
// The number the redeeming device shows, 10–99, drawn at redeem
|
|
5674
|
+
// Never sent to the issuer on its own: it is one of `choices`
|
|
5675
|
+
matchNumber: integer9("match_number"),
|
|
5676
|
+
// The three numbers the issuer picks from — the match and two distinct
|
|
5677
|
+
// decoys, shuffled once at redeem so every status answer shows the same
|
|
5678
|
+
// order
|
|
5679
|
+
choices: integer9("choices").array(),
|
|
5680
|
+
status: enumText10("status", DEVICE_LINK_STATUSES).notNull().default("issued"),
|
|
5681
|
+
// Expiry — 5 minutes from issue by default
|
|
5682
|
+
// Judged on read and in every transition; no job clears the row
|
|
5683
|
+
expiresAt: utcTimestamp15("expires_at").notNull(),
|
|
5684
|
+
// Set when a device redeemed the code
|
|
5685
|
+
redeemedAt: utcTimestamp15("redeemed_at"),
|
|
5686
|
+
// Set when the issuer picked the right number
|
|
5687
|
+
approvedAt: utcTimestamp15("approved_at"),
|
|
5688
|
+
// Set when the poll that registered the key won the race
|
|
5689
|
+
consumedAt: utcTimestamp15("consumed_at"),
|
|
5690
|
+
...timestamps14()
|
|
5691
|
+
},
|
|
5692
|
+
(table) => [
|
|
5693
|
+
// Lookup path for redeem
|
|
5694
|
+
// Unique so a typed code can never address two records
|
|
5695
|
+
uniqueIndex9("device_link_user_code_idx").on(table.userCode),
|
|
5696
|
+
// Issue expires the issuing key's live link before it creates the next one
|
|
5697
|
+
index14("device_link_issuer_key_idx").on(table.issuerKeyId),
|
|
5698
|
+
// A global revocation expires every live link of the account
|
|
5699
|
+
index14("device_link_issuer_user_idx").on(table.issuerUserId)
|
|
5700
|
+
]
|
|
5701
|
+
);
|
|
5702
|
+
}
|
|
5703
|
+
});
|
|
5704
|
+
|
|
5618
5705
|
// src/server/entities/user-invitations.ts
|
|
5619
|
-
import { text as
|
|
5620
|
-
import { id as
|
|
5706
|
+
import { text as text18, index as index15 } from "drizzle-orm/pg-core";
|
|
5707
|
+
import { id as id17, timestamps as timestamps15, enumText as enumText11, utcTimestamp as utcTimestamp16, typedJsonb as typedJsonb2, foreignKey as foreignKey13 } from "@spfn/core/db";
|
|
5621
5708
|
var userInvitations;
|
|
5622
5709
|
var init_user_invitations = __esm({
|
|
5623
5710
|
"src/server/entities/user-invitations.ts"() {
|
|
@@ -5630,39 +5717,39 @@ var init_user_invitations = __esm({
|
|
|
5630
5717
|
"user_invitations",
|
|
5631
5718
|
{
|
|
5632
5719
|
// Primary key
|
|
5633
|
-
id:
|
|
5720
|
+
id: id17(),
|
|
5634
5721
|
// Target email address for the invitation
|
|
5635
5722
|
// Will become the user's email upon acceptance
|
|
5636
|
-
email:
|
|
5723
|
+
email: text18("email").notNull(),
|
|
5637
5724
|
// Unique invitation token (UUID v4)
|
|
5638
5725
|
// Used in invitation URL: /auth/invite/{token}
|
|
5639
5726
|
// Single-use token that expires after acceptance
|
|
5640
|
-
token:
|
|
5727
|
+
token: text18("token").notNull().unique(),
|
|
5641
5728
|
// Role to be assigned when invitation is accepted
|
|
5642
5729
|
// Foreign key to roles table
|
|
5643
|
-
roleId:
|
|
5730
|
+
roleId: foreignKey13("role", () => roles.id),
|
|
5644
5731
|
// User who created this invitation
|
|
5645
5732
|
// Foreign key to users table
|
|
5646
5733
|
// Used for: audit trail, permission checks
|
|
5647
|
-
invitedBy:
|
|
5734
|
+
invitedBy: foreignKey13("invited_by", () => users.id, { onDelete: "cascade" }),
|
|
5648
5735
|
// Invitation status
|
|
5649
5736
|
// - pending: Invitation sent, awaiting acceptance
|
|
5650
5737
|
// - accepted: User accepted and account created
|
|
5651
5738
|
// - expired: Invitation expired (automatic)
|
|
5652
5739
|
// - cancelled: Invitation cancelled by admin
|
|
5653
|
-
status:
|
|
5740
|
+
status: enumText11("status", INVITATION_STATUSES).default("pending").notNull(),
|
|
5654
5741
|
// Expiration timestamp (default: 7 days from creation)
|
|
5655
5742
|
// Invitation cannot be accepted after this time
|
|
5656
5743
|
// Background job should update status to 'expired'
|
|
5657
|
-
expiresAt:
|
|
5744
|
+
expiresAt: utcTimestamp16("expires_at").notNull(),
|
|
5658
5745
|
// Timestamp when invitation was accepted
|
|
5659
5746
|
// null = not yet accepted
|
|
5660
5747
|
// Used for: audit trail, analytics
|
|
5661
|
-
acceptedAt:
|
|
5748
|
+
acceptedAt: utcTimestamp16("accepted_at"),
|
|
5662
5749
|
// Timestamp when invitation was cancelled
|
|
5663
5750
|
// null = not cancelled
|
|
5664
5751
|
// Used for: audit trail
|
|
5665
|
-
cancelledAt:
|
|
5752
|
+
cancelledAt: utcTimestamp16("cancelled_at"),
|
|
5666
5753
|
// Additional metadata (JSONB)
|
|
5667
5754
|
// Use cases:
|
|
5668
5755
|
// - Custom welcome message
|
|
@@ -5671,26 +5758,26 @@ var init_user_invitations = __esm({
|
|
|
5671
5758
|
// - Custom fields for app-specific data
|
|
5672
5759
|
// Example: { message: "Welcome!", department: "Engineering" }
|
|
5673
5760
|
metadata: typedJsonb2("metadata"),
|
|
5674
|
-
...
|
|
5761
|
+
...timestamps15()
|
|
5675
5762
|
},
|
|
5676
5763
|
(table) => [
|
|
5677
5764
|
// Indexes for query optimization
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5765
|
+
index15("invitations_token_idx").on(table.token),
|
|
5766
|
+
index15("invitations_email_idx").on(table.email),
|
|
5767
|
+
index15("invitations_status_idx").on(table.status),
|
|
5768
|
+
index15("invitations_invited_by_idx").on(table.invitedBy),
|
|
5769
|
+
index15("invitations_expires_at_idx").on(table.expiresAt),
|
|
5683
5770
|
// For cleanup jobs
|
|
5684
|
-
|
|
5771
|
+
index15("invitations_role_id_idx").on(table.roleId)
|
|
5685
5772
|
]
|
|
5686
5773
|
);
|
|
5687
5774
|
}
|
|
5688
5775
|
});
|
|
5689
5776
|
|
|
5690
5777
|
// src/server/entities/account-deletion-requests.ts
|
|
5691
|
-
import { text as
|
|
5778
|
+
import { text as text19, index as index16, uniqueIndex as uniqueIndex10 } from "drizzle-orm/pg-core";
|
|
5692
5779
|
import { sql as sql2 } from "drizzle-orm";
|
|
5693
|
-
import { id as
|
|
5780
|
+
import { id as id18, timestamps as timestamps16, enumText as enumText12, utcTimestamp as utcTimestamp17, optionalForeignKey as optionalForeignKey2 } from "@spfn/core/db";
|
|
5694
5781
|
var accountDeletionRequests;
|
|
5695
5782
|
var init_account_deletion_requests = __esm({
|
|
5696
5783
|
"src/server/entities/account-deletion-requests.ts"() {
|
|
@@ -5701,40 +5788,40 @@ var init_account_deletion_requests = __esm({
|
|
|
5701
5788
|
accountDeletionRequests = authSchema.table(
|
|
5702
5789
|
"account_deletion_requests",
|
|
5703
5790
|
{
|
|
5704
|
-
id:
|
|
5791
|
+
id: id18(),
|
|
5705
5792
|
// Foreign key to users table. `set null` (optionalForeignKey default) so this
|
|
5706
5793
|
// row survives a hard-delete purge of the user it refers to.
|
|
5707
5794
|
userId: optionalForeignKey2("user", () => users.id),
|
|
5708
5795
|
// Snapshot of the user's public UUID at request time — stays readable even
|
|
5709
5796
|
// after userId is nulled out or the account is anonymized.
|
|
5710
|
-
userPublicId:
|
|
5797
|
+
userPublicId: text19("user_public_id").notNull(),
|
|
5711
5798
|
// When the deletion was requested
|
|
5712
|
-
requestedAt:
|
|
5799
|
+
requestedAt: utcTimestamp17("requested_at").notNull().defaultNow(),
|
|
5713
5800
|
// When the purge job is allowed to run (requestedAt + grace period; equals
|
|
5714
5801
|
// requestedAt itself for immediate/zero-grace deletions)
|
|
5715
|
-
purgeScheduledAt:
|
|
5802
|
+
purgeScheduledAt: utcTimestamp17("purge_scheduled_at").notNull(),
|
|
5716
5803
|
// Request lifecycle status
|
|
5717
5804
|
// - pending: awaiting purgeScheduledAt (or immediate purge)
|
|
5718
5805
|
// - cancelled: recovered before purge
|
|
5719
5806
|
// - completed: purge ran
|
|
5720
|
-
status:
|
|
5807
|
+
status: enumText12("status", ACCOUNT_DELETION_REQUEST_STATUSES).default("pending").notNull(),
|
|
5721
5808
|
// Who initiated the request
|
|
5722
|
-
requestedBy:
|
|
5809
|
+
requestedBy: enumText12("requested_by", ACCOUNT_DELETION_REQUESTED_BY).default("self").notNull(),
|
|
5723
5810
|
// Optional free-text reason (self-service UI, admin note, DSR reference, ...)
|
|
5724
|
-
reason:
|
|
5725
|
-
cancelledAt:
|
|
5726
|
-
completedAt:
|
|
5811
|
+
reason: text19("reason"),
|
|
5812
|
+
cancelledAt: utcTimestamp17("cancelled_at"),
|
|
5813
|
+
completedAt: utcTimestamp17("completed_at"),
|
|
5727
5814
|
// Purge strategy actually executed (set on completion; null while pending)
|
|
5728
|
-
purgeStrategy:
|
|
5729
|
-
...
|
|
5815
|
+
purgeStrategy: enumText12("purge_strategy", PURGE_STRATEGIES),
|
|
5816
|
+
...timestamps16()
|
|
5730
5817
|
},
|
|
5731
5818
|
(table) => [
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5819
|
+
index16("account_deletion_requests_user_id_idx").on(table.userId),
|
|
5820
|
+
index16("account_deletion_requests_status_idx").on(table.status),
|
|
5821
|
+
index16("account_deletion_requests_purge_scheduled_at_idx").on(table.purgeScheduledAt),
|
|
5822
|
+
index16("account_deletion_requests_user_public_id_idx").on(table.userPublicId),
|
|
5736
5823
|
// Partial unique index: at most one pending request per user at a time.
|
|
5737
|
-
|
|
5824
|
+
uniqueIndex10("account_deletion_requests_user_pending_unique_idx").on(table.userId).where(sql2`${table.status} = 'pending'`)
|
|
5738
5825
|
]
|
|
5739
5826
|
);
|
|
5740
5827
|
}
|
|
@@ -5886,8 +5973,8 @@ var init_rbac = __esm({
|
|
|
5886
5973
|
});
|
|
5887
5974
|
|
|
5888
5975
|
// src/server/entities/permissions.ts
|
|
5889
|
-
import { text as
|
|
5890
|
-
import { id as
|
|
5976
|
+
import { text as text20, boolean as boolean5, index as index17 } from "drizzle-orm/pg-core";
|
|
5977
|
+
import { id as id19, timestamps as timestamps17, enumText as enumText13, typedJsonb as typedJsonb3 } from "@spfn/core/db";
|
|
5891
5978
|
var permissions;
|
|
5892
5979
|
var init_permissions = __esm({
|
|
5893
5980
|
"src/server/entities/permissions.ts"() {
|
|
@@ -5898,7 +5985,7 @@ var init_permissions = __esm({
|
|
|
5898
5985
|
"permissions",
|
|
5899
5986
|
{
|
|
5900
5987
|
// Primary key
|
|
5901
|
-
id:
|
|
5988
|
+
id: id19(),
|
|
5902
5989
|
// Permission identifier
|
|
5903
5990
|
// Format: resource:action or namespace:resource:action
|
|
5904
5991
|
// Examples:
|
|
@@ -5906,20 +5993,20 @@ var init_permissions = __esm({
|
|
|
5906
5993
|
// - Namespaced: 'auth:user:delete', 'cms:post:publish'
|
|
5907
5994
|
// Must be unique across all permissions
|
|
5908
5995
|
// Used in: permission checks, role assignments, API guards
|
|
5909
|
-
name:
|
|
5996
|
+
name: text20("name").notNull().unique(),
|
|
5910
5997
|
// Display name for UI
|
|
5911
5998
|
// Human-readable name shown in admin panels
|
|
5912
5999
|
// Example: "Delete Users", "Publish Posts"
|
|
5913
|
-
displayName:
|
|
6000
|
+
displayName: text20("display_name").notNull(),
|
|
5914
6001
|
// Permission description
|
|
5915
6002
|
// Detailed explanation of what this permission allows
|
|
5916
6003
|
// Example: "Allows deletion of user accounts from the system"
|
|
5917
|
-
description:
|
|
6004
|
+
description: text20("description"),
|
|
5918
6005
|
// Category for grouping
|
|
5919
6006
|
// Used for: organizing permissions in UI, filtering
|
|
5920
6007
|
// Built-in categories: auth, user, rbac, system
|
|
5921
6008
|
// Custom categories: any app-specific category
|
|
5922
|
-
category:
|
|
6009
|
+
category: enumText13("category", PERMISSION_CATEGORIES),
|
|
5923
6010
|
// Built-in permission flag
|
|
5924
6011
|
// true: Core package permissions (auth:*, user:*, rbac:*)
|
|
5925
6012
|
// - Cannot be deleted or modified
|
|
@@ -5950,22 +6037,22 @@ var init_permissions = __esm({
|
|
|
5950
6037
|
// - Audit: { createdBy: 123, source: 'migration', version: '1.0.0' }
|
|
5951
6038
|
// Example: { icon: 'trash', color: 'red', requiresMfa: true }
|
|
5952
6039
|
metadata: typedJsonb3("metadata"),
|
|
5953
|
-
...
|
|
6040
|
+
...timestamps17()
|
|
5954
6041
|
},
|
|
5955
6042
|
(table) => [
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
6043
|
+
index17("permissions_name_idx").on(table.name),
|
|
6044
|
+
index17("permissions_category_idx").on(table.category),
|
|
6045
|
+
index17("permissions_is_system_idx").on(table.isSystem),
|
|
6046
|
+
index17("permissions_is_active_idx").on(table.isActive),
|
|
6047
|
+
index17("permissions_is_builtin_idx").on(table.isBuiltin)
|
|
5961
6048
|
]
|
|
5962
6049
|
);
|
|
5963
6050
|
}
|
|
5964
6051
|
});
|
|
5965
6052
|
|
|
5966
6053
|
// src/server/entities/role-permissions.ts
|
|
5967
|
-
import { index as
|
|
5968
|
-
import { id as
|
|
6054
|
+
import { index as index18, unique } from "drizzle-orm/pg-core";
|
|
6055
|
+
import { id as id20, timestamps as timestamps18, foreignKey as foreignKey14 } from "@spfn/core/db";
|
|
5969
6056
|
var rolePermissions;
|
|
5970
6057
|
var init_role_permissions = __esm({
|
|
5971
6058
|
"src/server/entities/role-permissions.ts"() {
|
|
@@ -5977,25 +6064,25 @@ var init_role_permissions = __esm({
|
|
|
5977
6064
|
"role_permissions",
|
|
5978
6065
|
{
|
|
5979
6066
|
// Primary key
|
|
5980
|
-
id:
|
|
6067
|
+
id: id20(),
|
|
5981
6068
|
// Role reference
|
|
5982
6069
|
// Foreign key to roles table
|
|
5983
6070
|
// Cascade delete: when role is deleted, all role-permission mappings are removed
|
|
5984
6071
|
// Used for: defining which permissions each role has
|
|
5985
6072
|
// Example: Admin role → [user:create, user:delete, user:update]
|
|
5986
|
-
roleId:
|
|
6073
|
+
roleId: foreignKey14("role", () => roles.id, { onDelete: "cascade" }),
|
|
5987
6074
|
// Permission reference
|
|
5988
6075
|
// Foreign key to permissions table
|
|
5989
6076
|
// Cascade delete: when permission is deleted, all role-permission mappings are removed
|
|
5990
6077
|
// Used for: granting permissions to roles
|
|
5991
6078
|
// Example: user:delete permission → [Admin, Superadmin]
|
|
5992
|
-
permissionId:
|
|
5993
|
-
...
|
|
6079
|
+
permissionId: foreignKey14("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
6080
|
+
...timestamps18()
|
|
5994
6081
|
},
|
|
5995
6082
|
(table) => [
|
|
5996
6083
|
// Indexes for query performance
|
|
5997
|
-
|
|
5998
|
-
|
|
6084
|
+
index18("role_permissions_role_id_idx").on(table.roleId),
|
|
6085
|
+
index18("role_permissions_permission_id_idx").on(table.permissionId),
|
|
5999
6086
|
// Unique constraint: one role-permission pair only
|
|
6000
6087
|
unique("role_permissions_unique").on(table.roleId, table.permissionId)
|
|
6001
6088
|
]
|
|
@@ -6004,8 +6091,8 @@ var init_role_permissions = __esm({
|
|
|
6004
6091
|
});
|
|
6005
6092
|
|
|
6006
6093
|
// src/server/entities/user-permissions.ts
|
|
6007
|
-
import { boolean as boolean6, text as
|
|
6008
|
-
import { id as
|
|
6094
|
+
import { boolean as boolean6, text as text21, index as index19, unique as unique2 } from "drizzle-orm/pg-core";
|
|
6095
|
+
import { id as id21, timestamps as timestamps19, utcTimestamp as utcTimestamp18, foreignKey as foreignKey15 } from "@spfn/core/db";
|
|
6009
6096
|
var userPermissions;
|
|
6010
6097
|
var init_user_permissions = __esm({
|
|
6011
6098
|
"src/server/entities/user-permissions.ts"() {
|
|
@@ -6017,15 +6104,15 @@ var init_user_permissions = __esm({
|
|
|
6017
6104
|
"user_permissions",
|
|
6018
6105
|
{
|
|
6019
6106
|
// Primary key
|
|
6020
|
-
id:
|
|
6107
|
+
id: id21(),
|
|
6021
6108
|
// User reference
|
|
6022
6109
|
// Foreign key to users table
|
|
6023
6110
|
// Cascade delete: when user is deleted, all overrides are removed
|
|
6024
|
-
userId:
|
|
6111
|
+
userId: foreignKey15("user", () => users.id, { onDelete: "cascade" }),
|
|
6025
6112
|
// Permission reference
|
|
6026
6113
|
// Foreign key to permissions table
|
|
6027
6114
|
// Cascade delete: when permission is deleted, all overrides are removed
|
|
6028
|
-
permissionId:
|
|
6115
|
+
permissionId: foreignKey15("permission", () => permissions.id, { onDelete: "cascade" }),
|
|
6029
6116
|
// Grant or revoke flag
|
|
6030
6117
|
// true: GRANT this permission to the user (additive override)
|
|
6031
6118
|
// - Grants permission even if role doesn't have it
|
|
@@ -6040,19 +6127,19 @@ var init_user_permissions = __esm({
|
|
|
6040
6127
|
// Reason for grant/revocation
|
|
6041
6128
|
// Used for: audit trail, compliance documentation
|
|
6042
6129
|
// Example: "Temporary access for project X", "Security incident - restricted"
|
|
6043
|
-
reason:
|
|
6130
|
+
reason: text21("reason"),
|
|
6044
6131
|
// Expiration timestamp (optional)
|
|
6045
6132
|
// null: Permanent override (remains until manually removed)
|
|
6046
6133
|
// timestamp: Permission expires at this time (auto-revoked by background job)
|
|
6047
6134
|
// Use case: Time-limited elevated access, temporary restrictions
|
|
6048
|
-
expiresAt:
|
|
6049
|
-
...
|
|
6135
|
+
expiresAt: utcTimestamp18("expires_at"),
|
|
6136
|
+
...timestamps19()
|
|
6050
6137
|
},
|
|
6051
6138
|
(table) => [
|
|
6052
6139
|
// Indexes for query performance
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6140
|
+
index19("user_permissions_user_id_idx").on(table.userId),
|
|
6141
|
+
index19("user_permissions_permission_id_idx").on(table.permissionId),
|
|
6142
|
+
index19("user_permissions_expires_at_idx").on(table.expiresAt),
|
|
6056
6143
|
// Unique constraint: one user-permission pair only
|
|
6057
6144
|
unique2("user_permissions_unique").on(table.userId, table.permissionId)
|
|
6058
6145
|
]
|
|
@@ -6061,8 +6148,8 @@ var init_user_permissions = __esm({
|
|
|
6061
6148
|
});
|
|
6062
6149
|
|
|
6063
6150
|
// src/server/entities/oauth2-clients.ts
|
|
6064
|
-
import { text as
|
|
6065
|
-
import { id as
|
|
6151
|
+
import { text as text22 } from "drizzle-orm/pg-core";
|
|
6152
|
+
import { id as id22, timestamps as timestamps20, utcTimestamp as utcTimestamp19 } from "@spfn/core/db";
|
|
6066
6153
|
var oauth2Clients;
|
|
6067
6154
|
var init_oauth2_clients = __esm({
|
|
6068
6155
|
"src/server/entities/oauth2-clients.ts"() {
|
|
@@ -6071,35 +6158,35 @@ var init_oauth2_clients = __esm({
|
|
|
6071
6158
|
oauth2Clients = authSchema.table(
|
|
6072
6159
|
"oauth2_clients",
|
|
6073
6160
|
{
|
|
6074
|
-
id:
|
|
6161
|
+
id: id22(),
|
|
6075
6162
|
// The `client_id` the client sends on every later request. Random, opaque,
|
|
6076
6163
|
// and the unique constraint doubles as the lookup index.
|
|
6077
|
-
clientId:
|
|
6164
|
+
clientId: text22("client_id").notNull().unique(),
|
|
6078
6165
|
// Self-declared label, shown on the consent screen and nowhere else.
|
|
6079
6166
|
// A client that lies about it gains one wrong line on that screen.
|
|
6080
|
-
clientName:
|
|
6167
|
+
clientName: text22("client_name").notNull(),
|
|
6081
6168
|
// Registered redirect URIs, stored exactly as the client wrote them.
|
|
6082
6169
|
// The registration answer echoes them back verbatim, so normalising here
|
|
6083
6170
|
// would answer a client with a URI it did not register. Matching
|
|
6084
6171
|
// normalises both sides instead — see lib/oauth2/redirect-uri.ts.
|
|
6085
|
-
redirectUris:
|
|
6172
|
+
redirectUris: text22("redirect_uris").array().notNull(),
|
|
6086
6173
|
// Client IP the registration arrived from, so the per-IP cap on clients
|
|
6087
6174
|
// nobody has approved yet can be counted. Nullable: an IP is not always
|
|
6088
6175
|
// knowable behind a proxy that forwards none, and a registration is not
|
|
6089
6176
|
// worth refusing over that.
|
|
6090
|
-
createdIp:
|
|
6177
|
+
createdIp: text22("created_ip"),
|
|
6091
6178
|
// Last token issuance or refresh under this client, updated
|
|
6092
6179
|
// fire-and-forget. Operator-facing only; nothing is authorized by it.
|
|
6093
|
-
lastUsedAt:
|
|
6094
|
-
...
|
|
6180
|
+
lastUsedAt: utcTimestamp19("last_used_at"),
|
|
6181
|
+
...timestamps20()
|
|
6095
6182
|
}
|
|
6096
6183
|
);
|
|
6097
6184
|
}
|
|
6098
6185
|
});
|
|
6099
6186
|
|
|
6100
6187
|
// src/server/entities/oauth2-grants.ts
|
|
6101
|
-
import { uniqueIndex as
|
|
6102
|
-
import { id as
|
|
6188
|
+
import { uniqueIndex as uniqueIndex11, index as index20, text as text23 } from "drizzle-orm/pg-core";
|
|
6189
|
+
import { id as id23, timestamps as timestamps21, utcTimestamp as utcTimestamp20, foreignKey as foreignKey16 } from "@spfn/core/db";
|
|
6103
6190
|
var oauth2Grants;
|
|
6104
6191
|
var init_oauth2_grants = __esm({
|
|
6105
6192
|
"src/server/entities/oauth2-grants.ts"() {
|
|
@@ -6110,39 +6197,39 @@ var init_oauth2_grants = __esm({
|
|
|
6110
6197
|
oauth2Grants = authSchema.table(
|
|
6111
6198
|
"oauth2_grants",
|
|
6112
6199
|
{
|
|
6113
|
-
id:
|
|
6200
|
+
id: id23(),
|
|
6114
6201
|
// `oauth2_client_id` and not `client_id`: the opaque string a client
|
|
6115
6202
|
// sends is called `client_id` everywhere in the protocol, and a bigint
|
|
6116
6203
|
// foreign key under that name in this schema would read as that value.
|
|
6117
|
-
client:
|
|
6204
|
+
client: foreignKey16("oauth2_client", () => oauth2Clients.id, { onDelete: "cascade" }),
|
|
6118
6205
|
// Whose consent this is. Read from the approving session at authorize
|
|
6119
6206
|
// time, never from a request body — that is the whole authorization.
|
|
6120
|
-
user:
|
|
6207
|
+
user: foreignKey16("user", () => users.id, { onDelete: "cascade" }),
|
|
6121
6208
|
// The RFC 8707 target this consent is for, normalised (see lib/oauth2).
|
|
6122
6209
|
// An access token is only good against the resource its grant names.
|
|
6123
|
-
resource:
|
|
6210
|
+
resource: text23("resource").notNull(),
|
|
6124
6211
|
// Scope names the user approved. A refresh may ask for a subset of these
|
|
6125
6212
|
// and never for more.
|
|
6126
|
-
scopes:
|
|
6213
|
+
scopes: text23("scopes").array().notNull(),
|
|
6127
6214
|
// null = live; a timestamp cuts off every code and token beneath it
|
|
6128
|
-
revokedAt:
|
|
6129
|
-
...
|
|
6215
|
+
revokedAt: utcTimestamp20("revoked_at"),
|
|
6216
|
+
...timestamps21()
|
|
6130
6217
|
},
|
|
6131
6218
|
(table) => [
|
|
6132
6219
|
// One consent per (client, user, resource) — the re-consent path updates
|
|
6133
6220
|
// this row rather than inserting beside it.
|
|
6134
|
-
|
|
6221
|
+
uniqueIndex11("oauth2_grant_client_user_resource_idx").on(table.client, table.user, table.resource),
|
|
6135
6222
|
// The global-revocation path and the user's own grant list both address
|
|
6136
6223
|
// rows by user alone.
|
|
6137
|
-
|
|
6224
|
+
index20("oauth2_grant_user_idx").on(table.user)
|
|
6138
6225
|
]
|
|
6139
6226
|
);
|
|
6140
6227
|
}
|
|
6141
6228
|
});
|
|
6142
6229
|
|
|
6143
6230
|
// src/server/entities/oauth2-authorization-codes.ts
|
|
6144
|
-
import { text as
|
|
6145
|
-
import { id as
|
|
6231
|
+
import { text as text24 } from "drizzle-orm/pg-core";
|
|
6232
|
+
import { id as id24, timestamps as timestamps22, utcTimestamp as utcTimestamp21, foreignKey as foreignKey17 } from "@spfn/core/db";
|
|
6146
6233
|
var oauth2AuthorizationCodes;
|
|
6147
6234
|
var init_oauth2_authorization_codes = __esm({
|
|
6148
6235
|
"src/server/entities/oauth2-authorization-codes.ts"() {
|
|
@@ -6152,33 +6239,33 @@ var init_oauth2_authorization_codes = __esm({
|
|
|
6152
6239
|
oauth2AuthorizationCodes = authSchema.table(
|
|
6153
6240
|
"oauth2_authorization_codes",
|
|
6154
6241
|
{
|
|
6155
|
-
id:
|
|
6242
|
+
id: id24(),
|
|
6156
6243
|
// SHA-256 hex of the code. The code itself (32 random bytes, base64url)
|
|
6157
6244
|
// is in the redirect and nowhere else.
|
|
6158
|
-
codeHash:
|
|
6159
|
-
grant:
|
|
6245
|
+
codeHash: text24("code_hash").notNull().unique(),
|
|
6246
|
+
grant: foreignKey17("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
6160
6247
|
// The exact redirect_uri string the authorize request carried. The token
|
|
6161
6248
|
// request must repeat it character for character (RFC 6749 §4.1.3).
|
|
6162
|
-
redirectUri:
|
|
6249
|
+
redirectUri: text24("redirect_uri").notNull(),
|
|
6163
6250
|
// The PKCE S256 challenge. `code_verifier` at the token endpoint is
|
|
6164
6251
|
// hashed and compared against this; `plain` is not accepted anywhere,
|
|
6165
6252
|
// so no method column is needed.
|
|
6166
|
-
codeChallenge:
|
|
6253
|
+
codeChallenge: text24("code_challenge").notNull(),
|
|
6167
6254
|
// 60 seconds from issuance. Judged by the database in the statement that
|
|
6168
6255
|
// spends the row, not only in a read before it.
|
|
6169
|
-
expiresAt:
|
|
6256
|
+
expiresAt: utcTimestamp21("expires_at").notNull(),
|
|
6170
6257
|
// null = unspent. Non-null means spent, and a second presentation of the
|
|
6171
6258
|
// same code revokes the grant.
|
|
6172
|
-
usedAt:
|
|
6173
|
-
...
|
|
6259
|
+
usedAt: utcTimestamp21("used_at"),
|
|
6260
|
+
...timestamps22()
|
|
6174
6261
|
}
|
|
6175
6262
|
);
|
|
6176
6263
|
}
|
|
6177
6264
|
});
|
|
6178
6265
|
|
|
6179
6266
|
// src/server/entities/oauth2-tokens.ts
|
|
6180
|
-
import { index as
|
|
6181
|
-
import { id as
|
|
6267
|
+
import { index as index21, text as text25 } from "drizzle-orm/pg-core";
|
|
6268
|
+
import { id as id25, timestamps as timestamps23, enumText as enumText14, utcTimestamp as utcTimestamp22, foreignKey as foreignKey18 } from "@spfn/core/db";
|
|
6182
6269
|
var OAUTH2_TOKEN_KINDS, oauth2Tokens;
|
|
6183
6270
|
var init_oauth2_tokens = __esm({
|
|
6184
6271
|
"src/server/entities/oauth2-tokens.ts"() {
|
|
@@ -6189,30 +6276,30 @@ var init_oauth2_tokens = __esm({
|
|
|
6189
6276
|
oauth2Tokens = authSchema.table(
|
|
6190
6277
|
"oauth2_tokens",
|
|
6191
6278
|
{
|
|
6192
|
-
id:
|
|
6279
|
+
id: id25(),
|
|
6193
6280
|
// SHA-256 hex of the token value. Lookup key; the unique constraint
|
|
6194
6281
|
// doubles as the index.
|
|
6195
|
-
tokenHash:
|
|
6196
|
-
kind:
|
|
6197
|
-
grant:
|
|
6282
|
+
tokenHash: text25("token_hash").notNull().unique(),
|
|
6283
|
+
kind: enumText14("kind", OAUTH2_TOKEN_KINDS).notNull(),
|
|
6284
|
+
grant: foreignKey18("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
6198
6285
|
// What this particular token carries, which may be narrower than its
|
|
6199
6286
|
// grant's scopes: a refresh request is allowed to ask for a subset.
|
|
6200
6287
|
// Never wider — that is `invalid_scope`.
|
|
6201
|
-
scopes:
|
|
6202
|
-
expiresAt:
|
|
6288
|
+
scopes: text25("scopes").array().notNull(),
|
|
6289
|
+
expiresAt: utcTimestamp22("expires_at").notNull(),
|
|
6203
6290
|
// null = live. Set by revoke, by a grant revocation, and by the two
|
|
6204
6291
|
// replay detections (code reuse, refresh reuse).
|
|
6205
|
-
revokedAt:
|
|
6292
|
+
revokedAt: utcTimestamp22("revoked_at"),
|
|
6206
6293
|
// Refresh only: set when a rotation issued the successor. A row with
|
|
6207
6294
|
// this set is spent, and presenting it revokes the grant.
|
|
6208
|
-
replacedAt:
|
|
6295
|
+
replacedAt: utcTimestamp22("replaced_at"),
|
|
6209
6296
|
// Last successful verification, updated fire-and-forget like ops tokens
|
|
6210
|
-
lastUsedAt:
|
|
6211
|
-
...
|
|
6297
|
+
lastUsedAt: utcTimestamp22("last_used_at"),
|
|
6298
|
+
...timestamps23()
|
|
6212
6299
|
},
|
|
6213
6300
|
(table) => [
|
|
6214
6301
|
// Revocation addresses every token under one grant.
|
|
6215
|
-
|
|
6302
|
+
index21("oauth2_token_grant_idx").on(table.grant)
|
|
6216
6303
|
]
|
|
6217
6304
|
);
|
|
6218
6305
|
}
|
|
@@ -6220,7 +6307,7 @@ var init_oauth2_tokens = __esm({
|
|
|
6220
6307
|
|
|
6221
6308
|
// src/server/entities/auth-metadata.ts
|
|
6222
6309
|
import { sql as sql3 } from "drizzle-orm";
|
|
6223
|
-
import { text as
|
|
6310
|
+
import { text as text26, timestamp } from "drizzle-orm/pg-core";
|
|
6224
6311
|
var authMetadata;
|
|
6225
6312
|
var init_auth_metadata = __esm({
|
|
6226
6313
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -6230,9 +6317,9 @@ var init_auth_metadata = __esm({
|
|
|
6230
6317
|
"auth_metadata",
|
|
6231
6318
|
{
|
|
6232
6319
|
// Metadata key (primary key)
|
|
6233
|
-
key:
|
|
6320
|
+
key: text26("key").primaryKey(),
|
|
6234
6321
|
// Metadata value
|
|
6235
|
-
value:
|
|
6322
|
+
value: text26("value").notNull(),
|
|
6236
6323
|
// Last updated timestamp — stamped by the database on insert and on update
|
|
6237
6324
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
|
|
6238
6325
|
}
|
|
@@ -6241,8 +6328,8 @@ var init_auth_metadata = __esm({
|
|
|
6241
6328
|
});
|
|
6242
6329
|
|
|
6243
6330
|
// src/server/entities/ops-tokens.ts
|
|
6244
|
-
import { text as
|
|
6245
|
-
import { id as
|
|
6331
|
+
import { text as text27 } from "drizzle-orm/pg-core";
|
|
6332
|
+
import { id as id26, timestamps as timestamps24, utcTimestamp as utcTimestamp23 } from "@spfn/core/db";
|
|
6246
6333
|
var opsTokens;
|
|
6247
6334
|
var init_ops_tokens = __esm({
|
|
6248
6335
|
"src/server/entities/ops-tokens.ts"() {
|
|
@@ -6251,22 +6338,22 @@ var init_ops_tokens = __esm({
|
|
|
6251
6338
|
opsTokens = authSchema.table(
|
|
6252
6339
|
"ops_tokens",
|
|
6253
6340
|
{
|
|
6254
|
-
id:
|
|
6341
|
+
id: id26(),
|
|
6255
6342
|
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
6256
|
-
name:
|
|
6343
|
+
name: text27("name").notNull(),
|
|
6257
6344
|
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
6258
6345
|
// here, and the unique constraint doubles as the lookup index.
|
|
6259
|
-
tokenHash:
|
|
6346
|
+
tokenHash: text27("token_hash").notNull().unique(),
|
|
6260
6347
|
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
6261
6348
|
// '*' grants every scope.
|
|
6262
|
-
scopes:
|
|
6349
|
+
scopes: text27("scopes").array().notNull(),
|
|
6263
6350
|
// null = the token does not expire
|
|
6264
|
-
expiresAt:
|
|
6351
|
+
expiresAt: utcTimestamp23("expires_at"),
|
|
6265
6352
|
// null = active; a timestamp revokes the token permanently
|
|
6266
|
-
revokedAt:
|
|
6353
|
+
revokedAt: utcTimestamp23("revoked_at"),
|
|
6267
6354
|
// Last successful verification, updated fire-and-forget
|
|
6268
|
-
lastUsedAt:
|
|
6269
|
-
...
|
|
6355
|
+
lastUsedAt: utcTimestamp23("last_used_at"),
|
|
6356
|
+
...timestamps24()
|
|
6270
6357
|
}
|
|
6271
6358
|
);
|
|
6272
6359
|
}
|
|
@@ -6292,6 +6379,7 @@ var init_entities = __esm({
|
|
|
6292
6379
|
init_mfa_verifications();
|
|
6293
6380
|
init_mfa_challenges();
|
|
6294
6381
|
init_device_authorizations();
|
|
6382
|
+
init_device_links();
|
|
6295
6383
|
init_user_invitations();
|
|
6296
6384
|
init_account_deletion_requests();
|
|
6297
6385
|
init_roles();
|
|
@@ -6328,16 +6416,16 @@ var init_users_repository = __esm({
|
|
|
6328
6416
|
*
|
|
6329
6417
|
* Write primary 사용
|
|
6330
6418
|
*/
|
|
6331
|
-
async currentKeyEpoch(
|
|
6332
|
-
const result = await this.db.select({ keyEpoch: users.keyEpoch }).from(users).where(eq(users.id,
|
|
6419
|
+
async currentKeyEpoch(id27) {
|
|
6420
|
+
const result = await this.db.select({ keyEpoch: users.keyEpoch }).from(users).where(eq(users.id, id27)).limit(1);
|
|
6333
6421
|
return result[0]?.keyEpoch ?? 0;
|
|
6334
6422
|
}
|
|
6335
6423
|
/**
|
|
6336
6424
|
* ID로 사용자 조회
|
|
6337
6425
|
* Read replica 사용
|
|
6338
6426
|
*/
|
|
6339
|
-
async findById(
|
|
6340
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
6427
|
+
async findById(id27) {
|
|
6428
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id27)).limit(1);
|
|
6341
6429
|
return result[0] ?? null;
|
|
6342
6430
|
}
|
|
6343
6431
|
/**
|
|
@@ -6347,8 +6435,8 @@ var init_users_repository = __esm({
|
|
|
6347
6435
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
6348
6436
|
* 계속 사용할 것.
|
|
6349
6437
|
*/
|
|
6350
|
-
async findByIdOnPrimary(
|
|
6351
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
6438
|
+
async findByIdOnPrimary(id27) {
|
|
6439
|
+
const result = await this.db.select().from(users).where(eq(users.id, id27)).limit(1);
|
|
6352
6440
|
return result[0] ?? null;
|
|
6353
6441
|
}
|
|
6354
6442
|
/**
|
|
@@ -6364,8 +6452,8 @@ var init_users_repository = __esm({
|
|
|
6364
6452
|
* Only meaningful inside a transaction — the lock is released at commit.
|
|
6365
6453
|
* Write primary.
|
|
6366
6454
|
*/
|
|
6367
|
-
async lockById(
|
|
6368
|
-
await this.db.select({ id: users.id }).from(users).where(eq(users.id,
|
|
6455
|
+
async lockById(id27) {
|
|
6456
|
+
await this.db.select({ id: users.id }).from(users).where(eq(users.id, id27)).for("update");
|
|
6369
6457
|
}
|
|
6370
6458
|
/**
|
|
6371
6459
|
* 이메일로 사용자 조회
|
|
@@ -6435,13 +6523,13 @@ var init_users_repository = __esm({
|
|
|
6435
6523
|
*
|
|
6436
6524
|
* roleId가 null인 유저는 role: null 반환
|
|
6437
6525
|
*/
|
|
6438
|
-
async findByIdWithRole(
|
|
6526
|
+
async findByIdWithRole(id27) {
|
|
6439
6527
|
const result = await this.readDb.select({
|
|
6440
6528
|
user: users,
|
|
6441
6529
|
roleName: roles.name,
|
|
6442
6530
|
roleDisplayName: roles.displayName,
|
|
6443
6531
|
rolePriority: roles.priority
|
|
6444
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
6532
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id27)).limit(1);
|
|
6445
6533
|
const row = result[0];
|
|
6446
6534
|
if (!row) {
|
|
6447
6535
|
return null;
|
|
@@ -6516,9 +6604,9 @@ var init_users_repository = __esm({
|
|
|
6516
6604
|
* 사용자 정보 업데이트
|
|
6517
6605
|
* Write primary 사용
|
|
6518
6606
|
*/
|
|
6519
|
-
async updateById(
|
|
6607
|
+
async updateById(id27, data) {
|
|
6520
6608
|
const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
|
|
6521
|
-
const result = await this.db.update(users).set(patch).where(eq(users.id,
|
|
6609
|
+
const result = await this.db.update(users).set(patch).where(eq(users.id, id27)).returning();
|
|
6522
6610
|
return result[0] ?? null;
|
|
6523
6611
|
}
|
|
6524
6612
|
/**
|
|
@@ -6530,10 +6618,10 @@ var init_users_repository = __esm({
|
|
|
6530
6618
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
6531
6619
|
* Write primary 사용
|
|
6532
6620
|
*/
|
|
6533
|
-
async reactivateFromPendingDeletion(
|
|
6621
|
+
async reactivateFromPendingDeletion(id27) {
|
|
6534
6622
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
6535
6623
|
and(
|
|
6536
|
-
eq(users.id,
|
|
6624
|
+
eq(users.id, id27),
|
|
6537
6625
|
eq(users.status, "pending_deletion")
|
|
6538
6626
|
)
|
|
6539
6627
|
).returning();
|
|
@@ -6543,32 +6631,32 @@ var init_users_repository = __esm({
|
|
|
6543
6631
|
* 비밀번호 업데이트
|
|
6544
6632
|
* Write primary 사용
|
|
6545
6633
|
*/
|
|
6546
|
-
async updatePassword(
|
|
6634
|
+
async updatePassword(id27, passwordHash, clearPasswordChangeRequired = true) {
|
|
6547
6635
|
const updateData = {
|
|
6548
6636
|
passwordHash
|
|
6549
6637
|
};
|
|
6550
6638
|
if (clearPasswordChangeRequired) {
|
|
6551
6639
|
updateData.passwordChangeRequired = false;
|
|
6552
6640
|
}
|
|
6553
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
6641
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id27)).returning();
|
|
6554
6642
|
return result[0] ?? null;
|
|
6555
6643
|
}
|
|
6556
6644
|
/**
|
|
6557
6645
|
* 마지막 로그인 시간 업데이트
|
|
6558
6646
|
* Write primary 사용
|
|
6559
6647
|
*/
|
|
6560
|
-
async updateLastLogin(
|
|
6648
|
+
async updateLastLogin(id27) {
|
|
6561
6649
|
const result = await this.db.update(users).set({
|
|
6562
6650
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
6563
|
-
}).where(eq(users.id,
|
|
6651
|
+
}).where(eq(users.id, id27)).returning();
|
|
6564
6652
|
return result[0] ?? null;
|
|
6565
6653
|
}
|
|
6566
6654
|
/**
|
|
6567
6655
|
* 사용자 삭제
|
|
6568
6656
|
* Write primary 사용
|
|
6569
6657
|
*/
|
|
6570
|
-
async deleteById(
|
|
6571
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
6658
|
+
async deleteById(id27) {
|
|
6659
|
+
const result = await this.db.delete(users).where(eq(users.id, id27)).returning();
|
|
6572
6660
|
return result[0] ?? null;
|
|
6573
6661
|
}
|
|
6574
6662
|
/**
|
|
@@ -6756,10 +6844,10 @@ function parseDuration(duration) {
|
|
|
6756
6844
|
throw new Error(`Unknown duration unit: ${unit}`);
|
|
6757
6845
|
}
|
|
6758
6846
|
}
|
|
6759
|
-
function configureAuth(
|
|
6847
|
+
function configureAuth(config5) {
|
|
6760
6848
|
globalConfig = {
|
|
6761
6849
|
...globalConfig,
|
|
6762
|
-
...
|
|
6850
|
+
...config5
|
|
6763
6851
|
};
|
|
6764
6852
|
}
|
|
6765
6853
|
function getAuthConfig() {
|
|
@@ -7170,6 +7258,18 @@ var init_keys_repository = __esm({
|
|
|
7170
7258
|
async revokeAllActiveByUserIdExcept(userId, keepKeyId, reason) {
|
|
7171
7259
|
return await this.revokeActive(userId, reason, keepKeyId);
|
|
7172
7260
|
}
|
|
7261
|
+
/**
|
|
7262
|
+
* Lock the user's active key rows until the transaction ends, for a revocation
|
|
7263
|
+
* that has to touch other rows before it revokes the keys.
|
|
7264
|
+
*
|
|
7265
|
+
* Device-link statements lock the issuing key row before the link row. A
|
|
7266
|
+
* transaction that expires links and then revokes keys takes them the other
|
|
7267
|
+
* way round, and deadlocks against an issue, redeem, approve or consume in
|
|
7268
|
+
* flight on the same key. Calling this first restores the key-before-link order.
|
|
7269
|
+
*/
|
|
7270
|
+
async lockActiveByUserId(userId) {
|
|
7271
|
+
await this.db.select({ id: userPublicKeys.id }).from(userPublicKeys).where(and2(eq2(userPublicKeys.userId, userId), eq2(userPublicKeys.isActive, true))).for("no key update");
|
|
7272
|
+
}
|
|
7173
7273
|
/**
|
|
7174
7274
|
* The one statement behind both global revocations.
|
|
7175
7275
|
*
|
|
@@ -7366,7 +7466,7 @@ var init_keys_repository = __esm({
|
|
|
7366
7466
|
* replica-lagged address compared in application code both misses real
|
|
7367
7467
|
* switches and invents ones that did not happen.
|
|
7368
7468
|
*/
|
|
7369
|
-
async updateLastUsedById(
|
|
7469
|
+
async updateLastUsedById(id27, identity, ip) {
|
|
7370
7470
|
const now = /* @__PURE__ */ new Date();
|
|
7371
7471
|
const nowParam = sql5`${now.toISOString()}::timestamptz`;
|
|
7372
7472
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
@@ -7407,7 +7507,7 @@ var init_keys_repository = __esm({
|
|
|
7407
7507
|
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
7408
7508
|
} : {}
|
|
7409
7509
|
}).where(and2(
|
|
7410
|
-
eq2(userPublicKeys.id,
|
|
7510
|
+
eq2(userPublicKeys.id, id27),
|
|
7411
7511
|
or(lastUsedIsStale, identityChanged, sql5`(${ipChanged} AND ${notStampedThisWindow})`)
|
|
7412
7512
|
));
|
|
7413
7513
|
}
|
|
@@ -7458,8 +7558,8 @@ var init_verification_codes_repository = __esm({
|
|
|
7458
7558
|
* ID로 인증 코드 조회
|
|
7459
7559
|
* Read replica 사용
|
|
7460
7560
|
*/
|
|
7461
|
-
async findById(
|
|
7462
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
7561
|
+
async findById(id27) {
|
|
7562
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id27)).limit(1);
|
|
7463
7563
|
return result[0] ?? null;
|
|
7464
7564
|
}
|
|
7465
7565
|
/**
|
|
@@ -7473,22 +7573,22 @@ var init_verification_codes_repository = __esm({
|
|
|
7473
7573
|
* 인증 코드 사용 처리
|
|
7474
7574
|
* Write primary 사용
|
|
7475
7575
|
*/
|
|
7476
|
-
async markAsUsed(
|
|
7576
|
+
async markAsUsed(id27) {
|
|
7477
7577
|
const result = await this.db.update(verificationCodes).set({
|
|
7478
7578
|
usedAt: /* @__PURE__ */ new Date()
|
|
7479
|
-
}).where(eq3(verificationCodes.id,
|
|
7579
|
+
}).where(eq3(verificationCodes.id, id27)).returning();
|
|
7480
7580
|
return result[0] ?? null;
|
|
7481
7581
|
}
|
|
7482
7582
|
/**
|
|
7483
7583
|
* 시도 횟수 증가
|
|
7484
7584
|
* Write primary 사용
|
|
7485
7585
|
*/
|
|
7486
|
-
async incrementAttempts(
|
|
7487
|
-
const code = await this.findById(
|
|
7586
|
+
async incrementAttempts(id27) {
|
|
7587
|
+
const code = await this.findById(id27);
|
|
7488
7588
|
if (!code) return null;
|
|
7489
7589
|
const result = await this.db.update(verificationCodes).set({
|
|
7490
7590
|
attempts: code.attempts + 1
|
|
7491
|
-
}).where(eq3(verificationCodes.id,
|
|
7591
|
+
}).where(eq3(verificationCodes.id, id27)).returning();
|
|
7492
7592
|
return result[0] ?? null;
|
|
7493
7593
|
}
|
|
7494
7594
|
/**
|
|
@@ -7572,10 +7672,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7572
7672
|
*
|
|
7573
7673
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
7574
7674
|
*/
|
|
7575
|
-
async issue(
|
|
7675
|
+
async issue(id27, tokenHash) {
|
|
7576
7676
|
const result = await this.db.update(signupLinkTokens).set({ tokenHash }).where(
|
|
7577
7677
|
and4(
|
|
7578
|
-
eq4(signupLinkTokens.id,
|
|
7678
|
+
eq4(signupLinkTokens.id, id27),
|
|
7579
7679
|
isNull3(signupLinkTokens.consumedAt),
|
|
7580
7680
|
isNull3(signupLinkTokens.supersededAt),
|
|
7581
7681
|
isNull3(signupLinkTokens.completedAt),
|
|
@@ -7611,14 +7711,14 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7611
7711
|
*
|
|
7612
7712
|
* @returns the updated row, or null if another request claimed it first
|
|
7613
7713
|
*/
|
|
7614
|
-
async claimLink(
|
|
7714
|
+
async claimLink(id27, setupSecretHash, setupExpiresAt) {
|
|
7615
7715
|
const result = await this.db.update(signupLinkTokens).set({
|
|
7616
7716
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
7617
7717
|
setupSecretHash,
|
|
7618
7718
|
setupExpiresAt
|
|
7619
7719
|
}).where(
|
|
7620
7720
|
and4(
|
|
7621
|
-
eq4(signupLinkTokens.id,
|
|
7721
|
+
eq4(signupLinkTokens.id, id27),
|
|
7622
7722
|
isNull3(signupLinkTokens.consumedAt),
|
|
7623
7723
|
isNull3(signupLinkTokens.supersededAt)
|
|
7624
7724
|
)
|
|
@@ -7631,10 +7731,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
7631
7731
|
*
|
|
7632
7732
|
* @returns the updated row, or null if another request completed it first
|
|
7633
7733
|
*/
|
|
7634
|
-
async claimSetupSession(
|
|
7734
|
+
async claimSetupSession(id27) {
|
|
7635
7735
|
const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
7636
7736
|
and4(
|
|
7637
|
-
eq4(signupLinkTokens.id,
|
|
7737
|
+
eq4(signupLinkTokens.id, id27),
|
|
7638
7738
|
isNull3(signupLinkTokens.completedAt),
|
|
7639
7739
|
isNull3(signupLinkTokens.supersededAt)
|
|
7640
7740
|
)
|
|
@@ -7716,10 +7816,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7716
7816
|
*
|
|
7717
7817
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
7718
7818
|
*/
|
|
7719
|
-
async issue(
|
|
7819
|
+
async issue(id27, tokenHash) {
|
|
7720
7820
|
const result = await this.db.update(passwordResetTokens).set({ tokenHash }).where(
|
|
7721
7821
|
and5(
|
|
7722
|
-
eq5(passwordResetTokens.id,
|
|
7822
|
+
eq5(passwordResetTokens.id, id27),
|
|
7723
7823
|
isNull4(passwordResetTokens.consumedAt),
|
|
7724
7824
|
isNull4(passwordResetTokens.supersededAt),
|
|
7725
7825
|
isNull4(passwordResetTokens.completedAt),
|
|
@@ -7767,14 +7867,14 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7767
7867
|
*
|
|
7768
7868
|
* @returns the updated row, or null if another request claimed it first
|
|
7769
7869
|
*/
|
|
7770
|
-
async consume(
|
|
7870
|
+
async consume(id27, setupSecretHash, setupExpiresAt) {
|
|
7771
7871
|
const result = await this.db.update(passwordResetTokens).set({
|
|
7772
7872
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
7773
7873
|
setupSecretHash,
|
|
7774
7874
|
setupExpiresAt
|
|
7775
7875
|
}).where(
|
|
7776
7876
|
and5(
|
|
7777
|
-
eq5(passwordResetTokens.id,
|
|
7877
|
+
eq5(passwordResetTokens.id, id27),
|
|
7778
7878
|
isNull4(passwordResetTokens.consumedAt),
|
|
7779
7879
|
isNull4(passwordResetTokens.supersededAt)
|
|
7780
7880
|
)
|
|
@@ -7787,10 +7887,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
7787
7887
|
*
|
|
7788
7888
|
* @returns the updated row, or null if another request completed it first
|
|
7789
7889
|
*/
|
|
7790
|
-
async complete(
|
|
7890
|
+
async complete(id27) {
|
|
7791
7891
|
const result = await this.db.update(passwordResetTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
7792
7892
|
and5(
|
|
7793
|
-
eq5(passwordResetTokens.id,
|
|
7893
|
+
eq5(passwordResetTokens.id, id27),
|
|
7794
7894
|
isNull4(passwordResetTokens.completedAt),
|
|
7795
7895
|
isNull4(passwordResetTokens.supersededAt)
|
|
7796
7896
|
)
|
|
@@ -7988,9 +8088,9 @@ var init_passkeys_repository = __esm({
|
|
|
7988
8088
|
* Owner-scoped, so an id belonging to someone else answers null rather than
|
|
7989
8089
|
* a row — a management route can only ever say "not yours".
|
|
7990
8090
|
*/
|
|
7991
|
-
async findLiveByIdAndUserId(
|
|
8091
|
+
async findLiveByIdAndUserId(id27, userId) {
|
|
7992
8092
|
const result = await this.db.select().from(passkeys).where(and7(
|
|
7993
|
-
eq7(passkeys.id,
|
|
8093
|
+
eq7(passkeys.id, id27),
|
|
7994
8094
|
eq7(passkeys.userId, userId),
|
|
7995
8095
|
isNull6(passkeys.revokedAt)
|
|
7996
8096
|
)).limit(1);
|
|
@@ -8000,17 +8100,17 @@ var init_passkeys_repository = __esm({
|
|
|
8000
8100
|
* Record a successful assertion.
|
|
8001
8101
|
* Write primary.
|
|
8002
8102
|
*/
|
|
8003
|
-
async recordUse(
|
|
8004
|
-
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id,
|
|
8103
|
+
async recordUse(id27, counter) {
|
|
8104
|
+
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(passkeys.id, id27));
|
|
8005
8105
|
}
|
|
8006
8106
|
/**
|
|
8007
8107
|
* Rename, but only a credential this user still owns and has not revoked.
|
|
8008
8108
|
*
|
|
8009
8109
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
8010
8110
|
*/
|
|
8011
|
-
async renameByIdAndUserId(
|
|
8111
|
+
async renameByIdAndUserId(id27, userId, label) {
|
|
8012
8112
|
const result = await this.db.update(passkeys).set({ label }).where(and7(
|
|
8013
|
-
eq7(passkeys.id,
|
|
8113
|
+
eq7(passkeys.id, id27),
|
|
8014
8114
|
eq7(passkeys.userId, userId),
|
|
8015
8115
|
isNull6(passkeys.revokedAt)
|
|
8016
8116
|
)).returning();
|
|
@@ -8025,9 +8125,9 @@ var init_passkeys_repository = __esm({
|
|
|
8025
8125
|
*
|
|
8026
8126
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
8027
8127
|
*/
|
|
8028
|
-
async revokeByIdAndUserId(
|
|
8128
|
+
async revokeByIdAndUserId(id27, userId, reason) {
|
|
8029
8129
|
const result = await this.db.update(passkeys).set({ revokedAt: /* @__PURE__ */ new Date(), revokedReason: reason }).where(and7(
|
|
8030
|
-
eq7(passkeys.id,
|
|
8130
|
+
eq7(passkeys.id, id27),
|
|
8031
8131
|
eq7(passkeys.userId, userId),
|
|
8032
8132
|
isNull6(passkeys.revokedAt)
|
|
8033
8133
|
)).returning();
|
|
@@ -8042,9 +8142,9 @@ var init_passkeys_repository = __esm({
|
|
|
8042
8142
|
*
|
|
8043
8143
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
8044
8144
|
*/
|
|
8045
|
-
async markSecondFactorByIdAndUserId(
|
|
8145
|
+
async markSecondFactorByIdAndUserId(id27, userId, secondFactor) {
|
|
8046
8146
|
const result = await this.db.update(passkeys).set({ secondFactor }).where(and7(
|
|
8047
|
-
eq7(passkeys.id,
|
|
8147
|
+
eq7(passkeys.id, id27),
|
|
8048
8148
|
eq7(passkeys.userId, userId),
|
|
8049
8149
|
isNull6(passkeys.revokedAt)
|
|
8050
8150
|
)).returning();
|
|
@@ -8265,8 +8365,8 @@ var init_mfa_recovery_codes_repository = __esm({
|
|
|
8265
8365
|
*
|
|
8266
8366
|
* @returns true when this call spent the row
|
|
8267
8367
|
*/
|
|
8268
|
-
async consume(
|
|
8269
|
-
const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id,
|
|
8368
|
+
async consume(id27) {
|
|
8369
|
+
const result = await this.db.update(mfaRecoveryCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(and10(eq10(mfaRecoveryCodes.id, id27), isNull9(mfaRecoveryCodes.usedAt))).returning();
|
|
8270
8370
|
return result.length > 0;
|
|
8271
8371
|
}
|
|
8272
8372
|
/** Drop every generation the account has. Write primary. */
|
|
@@ -8368,9 +8468,9 @@ var init_mfa_challenges_repository = __esm({
|
|
|
8368
8468
|
* Every condition the caller checked is restated here, so the row that is
|
|
8369
8469
|
* marked is a row that was still spendable at the moment it was marked.
|
|
8370
8470
|
*/
|
|
8371
|
-
async markVerified(
|
|
8471
|
+
async markVerified(id27, keyEpoch) {
|
|
8372
8472
|
const result = await this.db.update(mfaChallenges).set({ verifiedAt: /* @__PURE__ */ new Date() }).where(and12(
|
|
8373
|
-
eq12(mfaChallenges.id,
|
|
8473
|
+
eq12(mfaChallenges.id, id27),
|
|
8374
8474
|
eq12(mfaChallenges.keyEpoch, keyEpoch),
|
|
8375
8475
|
isNull10(mfaChallenges.verifiedAt),
|
|
8376
8476
|
gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
|
|
@@ -8384,8 +8484,8 @@ var init_mfa_challenges_repository = __esm({
|
|
|
8384
8484
|
* is the whole reason lookup is by hash: there is no id for a caller to name,
|
|
8385
8485
|
* so no counter but their own can be moved.
|
|
8386
8486
|
*/
|
|
8387
|
-
async countFailure(
|
|
8388
|
-
const result = await this.db.update(mfaChallenges).set({ attempts: sql8`${mfaChallenges.attempts} + 1` }).where(eq12(mfaChallenges.id,
|
|
8487
|
+
async countFailure(id27) {
|
|
8488
|
+
const result = await this.db.update(mfaChallenges).set({ attempts: sql8`${mfaChallenges.attempts} + 1` }).where(eq12(mfaChallenges.id, id27)).returning({ attempts: mfaChallenges.attempts });
|
|
8389
8489
|
return (result[0]?.attempts ?? 0) >= MFA_CHALLENGE_ATTEMPT_LIMIT;
|
|
8390
8490
|
}
|
|
8391
8491
|
/**
|
|
@@ -8407,9 +8507,9 @@ var init_mfa_challenges_repository = __esm({
|
|
|
8407
8507
|
* another ten minutes; only the secret is new, because the old one was never
|
|
8408
8508
|
* stored and cannot be handed out twice.
|
|
8409
8509
|
*/
|
|
8410
|
-
async resecret(
|
|
8510
|
+
async resecret(id27, challengeHash) {
|
|
8411
8511
|
const result = await this.db.update(mfaChallenges).set({ challengeHash }).where(and12(
|
|
8412
|
-
eq12(mfaChallenges.id,
|
|
8512
|
+
eq12(mfaChallenges.id, id27),
|
|
8413
8513
|
isNull10(mfaChallenges.verifiedAt),
|
|
8414
8514
|
gt6(mfaChallenges.expiresAt, /* @__PURE__ */ new Date())
|
|
8415
8515
|
)).returning({ id: mfaChallenges.id });
|
|
@@ -8505,6 +8605,107 @@ var init_mfa_enrolment_repository = __esm({
|
|
|
8505
8605
|
}
|
|
8506
8606
|
});
|
|
8507
8607
|
|
|
8608
|
+
// src/server/lib/answer-waiters.ts
|
|
8609
|
+
import { onAfterCommit } from "@spfn/core/db";
|
|
8610
|
+
function createAnswerWaiters() {
|
|
8611
|
+
const parkedById = /* @__PURE__ */ new Map();
|
|
8612
|
+
const waitingById = /* @__PURE__ */ new Map();
|
|
8613
|
+
function wake(id27) {
|
|
8614
|
+
const parked = parkedById.get(id27);
|
|
8615
|
+
if (!parked) {
|
|
8616
|
+
return;
|
|
8617
|
+
}
|
|
8618
|
+
parkedById.delete(id27);
|
|
8619
|
+
for (const resolve of parked) {
|
|
8620
|
+
resolve();
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
function unpark(id27, parked, resolver) {
|
|
8624
|
+
parked.delete(resolver);
|
|
8625
|
+
if (parked.size === 0 && parkedById.get(id27) === parked) {
|
|
8626
|
+
parkedById.delete(id27);
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
function announce(ids) {
|
|
8630
|
+
if (ids.length === 0) {
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
onAfterCommit(() => {
|
|
8634
|
+
for (const id27 of ids) {
|
|
8635
|
+
wake(id27);
|
|
8636
|
+
}
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
function wait(id27, timeoutMs, signal) {
|
|
8640
|
+
if (signal?.aborted) {
|
|
8641
|
+
return Promise.resolve();
|
|
8642
|
+
}
|
|
8643
|
+
return new Promise((resolve) => {
|
|
8644
|
+
const parked = parkedById.get(id27) ?? /* @__PURE__ */ new Set();
|
|
8645
|
+
const done = () => {
|
|
8646
|
+
clearTimeout(timer);
|
|
8647
|
+
signal?.removeEventListener("abort", done);
|
|
8648
|
+
unpark(id27, parked, done);
|
|
8649
|
+
resolve();
|
|
8650
|
+
};
|
|
8651
|
+
const timer = setTimeout(done, timeoutMs);
|
|
8652
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
8653
|
+
parked.add(done);
|
|
8654
|
+
parkedById.set(id27, parked);
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
function waiting(id27) {
|
|
8658
|
+
return waitingById.get(id27) ?? 0;
|
|
8659
|
+
}
|
|
8660
|
+
async function hold(id27, run) {
|
|
8661
|
+
waitingById.set(id27, waiting(id27) + 1);
|
|
8662
|
+
try {
|
|
8663
|
+
return await run();
|
|
8664
|
+
} finally {
|
|
8665
|
+
const left = waiting(id27) - 1;
|
|
8666
|
+
if (left > 0) {
|
|
8667
|
+
waitingById.set(id27, left);
|
|
8668
|
+
} else {
|
|
8669
|
+
waitingById.delete(id27);
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
return {
|
|
8674
|
+
announce,
|
|
8675
|
+
wait,
|
|
8676
|
+
waiting,
|
|
8677
|
+
hold,
|
|
8678
|
+
parkedCount: () => parkedById.size + waitingById.size
|
|
8679
|
+
};
|
|
8680
|
+
}
|
|
8681
|
+
var init_answer_waiters = __esm({
|
|
8682
|
+
"src/server/lib/answer-waiters.ts"() {
|
|
8683
|
+
"use strict";
|
|
8684
|
+
}
|
|
8685
|
+
});
|
|
8686
|
+
|
|
8687
|
+
// src/server/lib/device-auth-waiters.ts
|
|
8688
|
+
function announceDeviceAuthAnswered(ids) {
|
|
8689
|
+
waiters.announce(ids);
|
|
8690
|
+
}
|
|
8691
|
+
function waitForDeviceAuthAnswer(id27, timeoutMs, signal) {
|
|
8692
|
+
return waiters.wait(id27, timeoutMs, signal);
|
|
8693
|
+
}
|
|
8694
|
+
function waitingOnDeviceAuth(id27) {
|
|
8695
|
+
return waiters.waiting(id27);
|
|
8696
|
+
}
|
|
8697
|
+
function holdDeviceAuthWait(id27, wait) {
|
|
8698
|
+
return waiters.hold(id27, wait);
|
|
8699
|
+
}
|
|
8700
|
+
var waiters;
|
|
8701
|
+
var init_device_auth_waiters = __esm({
|
|
8702
|
+
"src/server/lib/device-auth-waiters.ts"() {
|
|
8703
|
+
"use strict";
|
|
8704
|
+
init_answer_waiters();
|
|
8705
|
+
waiters = createAnswerWaiters();
|
|
8706
|
+
}
|
|
8707
|
+
});
|
|
8708
|
+
|
|
8508
8709
|
// src/server/repositories/device-authorizations.repository.ts
|
|
8509
8710
|
import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
|
|
8510
8711
|
import { eq as eq14, and as and14, gt as gt7, inArray as inArray2, sql as sql9 } from "drizzle-orm";
|
|
@@ -8513,6 +8714,7 @@ var init_device_authorizations_repository = __esm({
|
|
|
8513
8714
|
"src/server/repositories/device-authorizations.repository.ts"() {
|
|
8514
8715
|
"use strict";
|
|
8515
8716
|
init_device_authorizations();
|
|
8717
|
+
init_device_auth_waiters();
|
|
8516
8718
|
notExpired = () => gt7(deviceAuthorizations.expiresAt, sql9`now()`);
|
|
8517
8719
|
DeviceAuthorizationsRepository = class extends BaseRepository14 {
|
|
8518
8720
|
/**
|
|
@@ -8555,6 +8757,19 @@ var init_device_authorizations_repository = __esm({
|
|
|
8555
8757
|
const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
8556
8758
|
return result[0] ?? null;
|
|
8557
8759
|
}
|
|
8760
|
+
/**
|
|
8761
|
+
* `findByDeviceCodeHash`, read from the primary even outside a transaction.
|
|
8762
|
+
*
|
|
8763
|
+
* For the long poll's wait, which runs before any transaction opens: it is
|
|
8764
|
+
* woken right after an answer commits, and a replica that has not caught up
|
|
8765
|
+
* yet would show it the record still pending and send it back to sleep.
|
|
8766
|
+
*
|
|
8767
|
+
* Write primary.
|
|
8768
|
+
*/
|
|
8769
|
+
async findByDeviceCodeHashOnPrimary(deviceCodeHash) {
|
|
8770
|
+
const result = await this.db.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
|
|
8771
|
+
return result[0] ?? null;
|
|
8772
|
+
}
|
|
8558
8773
|
/**
|
|
8559
8774
|
* Bind the approving user and move the record to `approved`, but only from
|
|
8560
8775
|
* `pending`.
|
|
@@ -8564,15 +8779,15 @@ var init_device_authorizations_repository = __esm({
|
|
|
8564
8779
|
*
|
|
8565
8780
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8566
8781
|
*/
|
|
8567
|
-
async approve(
|
|
8782
|
+
async approve(id27, userId) {
|
|
8568
8783
|
const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
|
|
8569
8784
|
and14(
|
|
8570
|
-
eq14(deviceAuthorizations.id,
|
|
8785
|
+
eq14(deviceAuthorizations.id, id27),
|
|
8571
8786
|
eq14(deviceAuthorizations.status, "pending"),
|
|
8572
8787
|
notExpired()
|
|
8573
8788
|
)
|
|
8574
8789
|
).returning();
|
|
8575
|
-
return result[0] ?? null;
|
|
8790
|
+
return this.answered(result[0] ?? null);
|
|
8576
8791
|
}
|
|
8577
8792
|
/**
|
|
8578
8793
|
* Move the record to `denied`, but only from `pending`.
|
|
@@ -8581,15 +8796,15 @@ var init_device_authorizations_repository = __esm({
|
|
|
8581
8796
|
*
|
|
8582
8797
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8583
8798
|
*/
|
|
8584
|
-
async deny(
|
|
8799
|
+
async deny(id27) {
|
|
8585
8800
|
const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8586
8801
|
and14(
|
|
8587
|
-
eq14(deviceAuthorizations.id,
|
|
8802
|
+
eq14(deviceAuthorizations.id, id27),
|
|
8588
8803
|
eq14(deviceAuthorizations.status, "pending"),
|
|
8589
8804
|
notExpired()
|
|
8590
8805
|
)
|
|
8591
8806
|
).returning();
|
|
8592
|
-
return result[0] ?? null;
|
|
8807
|
+
return this.answered(result[0] ?? null);
|
|
8593
8808
|
}
|
|
8594
8809
|
/**
|
|
8595
8810
|
* Refuse every authorization a user still has in flight.
|
|
@@ -8617,12 +8832,14 @@ var init_device_authorizations_repository = __esm({
|
|
|
8617
8832
|
* @returns the rows this call refused
|
|
8618
8833
|
*/
|
|
8619
8834
|
async denyAllActiveByUserId(userId) {
|
|
8620
|
-
|
|
8835
|
+
const denied = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8621
8836
|
and14(
|
|
8622
8837
|
eq14(deviceAuthorizations.userId, userId),
|
|
8623
8838
|
inArray2(deviceAuthorizations.status, ["pending", "approved"])
|
|
8624
8839
|
)
|
|
8625
8840
|
).returning();
|
|
8841
|
+
announceDeviceAuthAnswered(denied.map((record) => record.id));
|
|
8842
|
+
return denied;
|
|
8626
8843
|
}
|
|
8627
8844
|
/**
|
|
8628
8845
|
* Spend an approved record, but only from `approved`, and address it by the
|
|
@@ -8645,32 +8862,255 @@ var init_device_authorizations_repository = __esm({
|
|
|
8645
8862
|
).returning();
|
|
8646
8863
|
return result[0] ?? null;
|
|
8647
8864
|
}
|
|
8865
|
+
/** Wake the polls parked on a record this call moved, and hand the row back. */
|
|
8866
|
+
answered(record) {
|
|
8867
|
+
if (record) {
|
|
8868
|
+
announceDeviceAuthAnswered([record.id]);
|
|
8869
|
+
}
|
|
8870
|
+
return record;
|
|
8871
|
+
}
|
|
8648
8872
|
};
|
|
8649
8873
|
deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
|
|
8650
8874
|
}
|
|
8651
8875
|
});
|
|
8652
8876
|
|
|
8653
|
-
// src/server/
|
|
8877
|
+
// src/server/lib/device-link-waiters.ts
|
|
8878
|
+
function announceDeviceLinkMoved(ids) {
|
|
8879
|
+
waiters2.announce(ids);
|
|
8880
|
+
}
|
|
8881
|
+
function waitForDeviceLinkMove(id27, timeoutMs, signal) {
|
|
8882
|
+
return waiters2.wait(id27, timeoutMs, signal);
|
|
8883
|
+
}
|
|
8884
|
+
function waitingOnDeviceLink(id27) {
|
|
8885
|
+
return waiters2.waiting(id27);
|
|
8886
|
+
}
|
|
8887
|
+
function holdDeviceLinkWait(id27, wait) {
|
|
8888
|
+
return waiters2.hold(id27, wait);
|
|
8889
|
+
}
|
|
8890
|
+
var waiters2;
|
|
8891
|
+
var init_device_link_waiters = __esm({
|
|
8892
|
+
"src/server/lib/device-link-waiters.ts"() {
|
|
8893
|
+
"use strict";
|
|
8894
|
+
init_answer_waiters();
|
|
8895
|
+
waiters2 = createAnswerWaiters();
|
|
8896
|
+
}
|
|
8897
|
+
});
|
|
8898
|
+
|
|
8899
|
+
// src/server/repositories/device-links.repository.ts
|
|
8654
8900
|
import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
|
|
8655
|
-
import { eq as eq15,
|
|
8901
|
+
import { eq as eq15, and as and15, gt as gt8, or as or4, isNull as isNull12, inArray as inArray3, exists, getTableColumns, sql as sql10 } from "drizzle-orm";
|
|
8902
|
+
var LIVE_STATUSES, notExpired2, DeviceLinksRepository, deviceLinksRepository;
|
|
8903
|
+
var init_device_links_repository = __esm({
|
|
8904
|
+
"src/server/repositories/device-links.repository.ts"() {
|
|
8905
|
+
"use strict";
|
|
8906
|
+
init_entities();
|
|
8907
|
+
init_device_link_waiters();
|
|
8908
|
+
LIVE_STATUSES = ["issued", "redeemed", "approved"];
|
|
8909
|
+
notExpired2 = () => gt8(deviceLinks.expiresAt, sql10`now()`);
|
|
8910
|
+
DeviceLinksRepository = class extends BaseRepository15 {
|
|
8911
|
+
/**
|
|
8912
|
+
* Insert an issued link, unless its user code is already taken.
|
|
8913
|
+
*
|
|
8914
|
+
* `onConflictDoNothing` for the reason `DeviceAuthorizationsRepository.create`
|
|
8915
|
+
* gives: a raised unique violation would abort the issue route's transaction
|
|
8916
|
+
* and leave no way to retry with a fresh code.
|
|
8917
|
+
*
|
|
8918
|
+
* @returns the inserted row, or null if the code collided
|
|
8919
|
+
*/
|
|
8920
|
+
async create(data) {
|
|
8921
|
+
const result = await this.db.insert(deviceLinks).values(data).onConflictDoNothing().returning();
|
|
8922
|
+
return result[0] ?? null;
|
|
8923
|
+
}
|
|
8924
|
+
/**
|
|
8925
|
+
* Find a link by the issuer's handle, in any state.
|
|
8926
|
+
*
|
|
8927
|
+
* Unfiltered, as `DeviceAuthorizationsRepository.findByUserCode` is: which
|
|
8928
|
+
* refusal a caller is owed is the service's decision.
|
|
8929
|
+
*/
|
|
8930
|
+
async findByLinkId(linkId) {
|
|
8931
|
+
return this.findOne(eq15(deviceLinks.linkId, linkId));
|
|
8932
|
+
}
|
|
8933
|
+
/** Find a link by its normalized user code, in any state. */
|
|
8934
|
+
async findByUserCode(userCode) {
|
|
8935
|
+
return this.findOne(eq15(deviceLinks.userCode, userCode));
|
|
8936
|
+
}
|
|
8937
|
+
/** Find a link by the hash of the device code its redeemer holds, in any state. */
|
|
8938
|
+
async findByDeviceCodeHash(deviceCodeHash) {
|
|
8939
|
+
return this.findOne(eq15(deviceLinks.deviceCodeHash, deviceCodeHash));
|
|
8940
|
+
}
|
|
8941
|
+
/**
|
|
8942
|
+
* Park a device's key on an issued link and move it to `redeemed`.
|
|
8943
|
+
*
|
|
8944
|
+
* From `issued` only, so of two devices redeeming one code exactly one wins;
|
|
8945
|
+
* the other matches nothing and is answered as if the code were unknown.
|
|
8946
|
+
*
|
|
8947
|
+
* @returns the updated row, or null if it was no longer issued, expired, or its issuer signed out
|
|
8948
|
+
*/
|
|
8949
|
+
async redeem(id27, redemption) {
|
|
8950
|
+
return this.transition(
|
|
8951
|
+
{ ...redemption, status: "redeemed", redeemedAt: /* @__PURE__ */ new Date() },
|
|
8952
|
+
and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "issued"), notExpired2(), this.issuerKeyLive(true))
|
|
8953
|
+
);
|
|
8954
|
+
}
|
|
8955
|
+
/**
|
|
8956
|
+
* The issuer picked the right number: move the link to `approved`, from
|
|
8957
|
+
* `redeemed` only.
|
|
8958
|
+
*
|
|
8959
|
+
* The key is not registered here, for the reason device-code approval gives:
|
|
8960
|
+
* the redeeming device may never come back for it.
|
|
8961
|
+
*
|
|
8962
|
+
* @returns the updated row, or null if it was no longer redeemed, expired, or its issuer signed out
|
|
8963
|
+
*/
|
|
8964
|
+
async approve(id27) {
|
|
8965
|
+
return this.transition(
|
|
8966
|
+
{ status: "approved", approvedAt: /* @__PURE__ */ new Date() },
|
|
8967
|
+
and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "redeemed"), notExpired2(), this.issuerKeyLive(true))
|
|
8968
|
+
);
|
|
8969
|
+
}
|
|
8970
|
+
/**
|
|
8971
|
+
* Refuse a redeemed link — the issuer said no, or picked the wrong number.
|
|
8972
|
+
*
|
|
8973
|
+
* No issuing-key condition: refusing is always safe, and a signed-out issuer's
|
|
8974
|
+
* link is refused anyway.
|
|
8975
|
+
*
|
|
8976
|
+
* @returns the updated row, or null if it was no longer redeemed, or expired
|
|
8977
|
+
*/
|
|
8978
|
+
async deny(id27) {
|
|
8979
|
+
return this.transition(
|
|
8980
|
+
{ status: "denied" },
|
|
8981
|
+
and15(eq15(deviceLinks.id, id27), eq15(deviceLinks.status, "redeemed"), notExpired2())
|
|
8982
|
+
);
|
|
8983
|
+
}
|
|
8984
|
+
/**
|
|
8985
|
+
* The issuer closed the link before anyone was let in: `issued` or `redeemed`
|
|
8986
|
+
* to `expired`.
|
|
8987
|
+
*
|
|
8988
|
+
* @returns the updated row, or null if it had moved past redeemed, or expired
|
|
8989
|
+
*/
|
|
8990
|
+
async cancel(id27) {
|
|
8991
|
+
return this.transition(
|
|
8992
|
+
{ status: "expired" },
|
|
8993
|
+
and15(eq15(deviceLinks.id, id27), inArray3(deviceLinks.status, ["issued", "redeemed"]), notExpired2())
|
|
8994
|
+
);
|
|
8995
|
+
}
|
|
8996
|
+
/**
|
|
8997
|
+
* Spend an approved link, addressed by the device code hash the caller
|
|
8998
|
+
* presented — the one-shot `DeviceAuthorizationsRepository.consumeApproved`
|
|
8999
|
+
* is, with the issuing key judged in the same statement.
|
|
9000
|
+
*
|
|
9001
|
+
* @returns the spent row, or null if it was not approved (any more), expired, or its issuer signed out
|
|
9002
|
+
*/
|
|
9003
|
+
async consumeApproved(deviceCodeHash) {
|
|
9004
|
+
return this.transition(
|
|
9005
|
+
{ status: "consumed", consumedAt: /* @__PURE__ */ new Date() },
|
|
9006
|
+
and15(
|
|
9007
|
+
eq15(deviceLinks.deviceCodeHash, deviceCodeHash),
|
|
9008
|
+
eq15(deviceLinks.status, "approved"),
|
|
9009
|
+
notExpired2(),
|
|
9010
|
+
this.issuerKeyLive(true)
|
|
9011
|
+
)
|
|
9012
|
+
);
|
|
9013
|
+
}
|
|
9014
|
+
/**
|
|
9015
|
+
* Lock the issuing key's row until the transaction ends, so two issues from
|
|
9016
|
+
* one key run one after the other: each expires the other's link or waits for
|
|
9017
|
+
* it to be inserted, and never both read "nothing live" and insert.
|
|
9018
|
+
*
|
|
9019
|
+
* `no key update` is the lock a plain UPDATE of the row takes: it conflicts
|
|
9020
|
+
* with itself, with the share lock `issuerKeyLive(true)` takes and with a
|
|
9021
|
+
* revocation's UPDATE, and it leaves foreign-key checks on the row alone.
|
|
9022
|
+
* Taken first, before any link row, so every path that locks both — redeem,
|
|
9023
|
+
* approve, consume and the revocations — locks the key row before the link.
|
|
9024
|
+
*/
|
|
9025
|
+
async lockIssuerKey(issuerKeyId) {
|
|
9026
|
+
await this.db.select({ one: sql10`1` }).from(userPublicKeys).where(eq15(userPublicKeys.keyId, issuerKeyId)).for("no key update");
|
|
9027
|
+
}
|
|
9028
|
+
/**
|
|
9029
|
+
* Expire the live link a key issued, so a fresh issue leaves one link per key.
|
|
9030
|
+
*
|
|
9031
|
+
* Race-free only after `lockIssuerKey` in the same transaction.
|
|
9032
|
+
*
|
|
9033
|
+
* @returns the rows this call expired
|
|
9034
|
+
*/
|
|
9035
|
+
async expireLiveByIssuerKey(issuerKeyId) {
|
|
9036
|
+
return this.expireLive(eq15(deviceLinks.issuerKeyId, issuerKeyId));
|
|
9037
|
+
}
|
|
9038
|
+
/**
|
|
9039
|
+
* Expire every link an account still has in play — the device-link half of a
|
|
9040
|
+
* global revocation, beside `DeviceAuthorizationsRepository.denyAllActiveByUserId`.
|
|
9041
|
+
*
|
|
9042
|
+
* A revoke-all that spares the calling device spares its key too, so the
|
|
9043
|
+
* issuing-key condition alone would leave that device's link able to let a
|
|
9044
|
+
* new device in seconds after the owner signed everything else out.
|
|
9045
|
+
*
|
|
9046
|
+
* @returns the rows this call expired
|
|
9047
|
+
*/
|
|
9048
|
+
async expireAllLiveByUserId(userId) {
|
|
9049
|
+
return this.expireLive(eq15(deviceLinks.issuerUserId, userId));
|
|
9050
|
+
}
|
|
9051
|
+
async expireLive(owner) {
|
|
9052
|
+
const expired = await this.db.update(deviceLinks).set({ status: "expired" }).where(and15(owner, inArray3(deviceLinks.status, LIVE_STATUSES))).returning();
|
|
9053
|
+
announceDeviceLinkMoved(expired.map((link) => link.id));
|
|
9054
|
+
return expired;
|
|
9055
|
+
}
|
|
9056
|
+
/** Apply one conditional UPDATE, wake the waiters of the row it moved, and hand the row back. */
|
|
9057
|
+
async transition(values, where) {
|
|
9058
|
+
const result = await this.db.update(deviceLinks).set(values).where(where).returning();
|
|
9059
|
+
const moved = result[0] ?? null;
|
|
9060
|
+
if (moved) {
|
|
9061
|
+
announceDeviceLinkMoved([moved.id]);
|
|
9062
|
+
}
|
|
9063
|
+
return moved;
|
|
9064
|
+
}
|
|
9065
|
+
/** One link with its issuing key's standing. */
|
|
9066
|
+
async findOne(where) {
|
|
9067
|
+
const result = await this.db.select({ ...getTableColumns(deviceLinks), issuerKeyLive: this.issuerKeyLive(false) }).from(deviceLinks).where(where).limit(1);
|
|
9068
|
+
return result[0] ?? null;
|
|
9069
|
+
}
|
|
9070
|
+
/**
|
|
9071
|
+
* Whether the link's issuing key is still registered to its issuer, active
|
|
9072
|
+
* and unexpired — the test `authenticate` applies to the same row.
|
|
9073
|
+
*
|
|
9074
|
+
* @param lock take a share lock on the key row, so a transition waits for a
|
|
9075
|
+
* revocation in flight instead of judging the version before it
|
|
9076
|
+
*/
|
|
9077
|
+
issuerKeyLive(lock) {
|
|
9078
|
+
const key = this.db.select({ one: sql10`1` }).from(userPublicKeys).where(
|
|
9079
|
+
and15(
|
|
9080
|
+
eq15(userPublicKeys.keyId, deviceLinks.issuerKeyId),
|
|
9081
|
+
eq15(userPublicKeys.userId, deviceLinks.issuerUserId),
|
|
9082
|
+
eq15(userPublicKeys.isActive, true),
|
|
9083
|
+
or4(isNull12(userPublicKeys.expiresAt), gt8(userPublicKeys.expiresAt, sql10`now()`))
|
|
9084
|
+
)
|
|
9085
|
+
);
|
|
9086
|
+
return sql10`${exists(lock ? key.for("share") : key)}`;
|
|
9087
|
+
}
|
|
9088
|
+
};
|
|
9089
|
+
deviceLinksRepository = new DeviceLinksRepository();
|
|
9090
|
+
}
|
|
9091
|
+
});
|
|
9092
|
+
|
|
9093
|
+
// src/server/repositories/roles.repository.ts
|
|
9094
|
+
import { BaseRepository as BaseRepository16 } from "@spfn/core/db";
|
|
9095
|
+
import { eq as eq16, asc } from "drizzle-orm";
|
|
8656
9096
|
var RolesRepository, rolesRepository;
|
|
8657
9097
|
var init_roles_repository = __esm({
|
|
8658
9098
|
"src/server/repositories/roles.repository.ts"() {
|
|
8659
9099
|
"use strict";
|
|
8660
9100
|
init_roles();
|
|
8661
|
-
RolesRepository = class extends
|
|
9101
|
+
RolesRepository = class extends BaseRepository16 {
|
|
8662
9102
|
/**
|
|
8663
9103
|
* ID로 역할 조회
|
|
8664
9104
|
*/
|
|
8665
|
-
async findById(
|
|
8666
|
-
const result = await this.readDb.select().from(roles).where(
|
|
9105
|
+
async findById(id27) {
|
|
9106
|
+
const result = await this.readDb.select().from(roles).where(eq16(roles.id, id27)).limit(1);
|
|
8667
9107
|
return result[0] ?? null;
|
|
8668
9108
|
}
|
|
8669
9109
|
/**
|
|
8670
9110
|
* Name으로 역할 조회
|
|
8671
9111
|
*/
|
|
8672
9112
|
async findByName(name) {
|
|
8673
|
-
const result = await this.readDb.select().from(roles).where(
|
|
9113
|
+
const result = await this.readDb.select().from(roles).where(eq16(roles.name, name)).limit(1);
|
|
8674
9114
|
return result[0] ?? null;
|
|
8675
9115
|
}
|
|
8676
9116
|
/**
|
|
@@ -8683,7 +9123,7 @@ var init_roles_repository = __esm({
|
|
|
8683
9123
|
* 활성 역할만 조회
|
|
8684
9124
|
*/
|
|
8685
9125
|
async findActive() {
|
|
8686
|
-
return this.readDb.select().from(roles).where(
|
|
9126
|
+
return this.readDb.select().from(roles).where(eq16(roles.isActive, true)).orderBy(asc(roles.priority));
|
|
8687
9127
|
}
|
|
8688
9128
|
/**
|
|
8689
9129
|
* 역할 생성
|
|
@@ -8694,15 +9134,15 @@ var init_roles_repository = __esm({
|
|
|
8694
9134
|
/**
|
|
8695
9135
|
* 역할 업데이트
|
|
8696
9136
|
*/
|
|
8697
|
-
async updateById(
|
|
8698
|
-
const result = await this.db.update(roles).set(data).where(
|
|
9137
|
+
async updateById(id27, data) {
|
|
9138
|
+
const result = await this.db.update(roles).set(data).where(eq16(roles.id, id27)).returning();
|
|
8699
9139
|
return result[0] ?? null;
|
|
8700
9140
|
}
|
|
8701
9141
|
/**
|
|
8702
9142
|
* 역할 삭제
|
|
8703
9143
|
*/
|
|
8704
|
-
async deleteById(
|
|
8705
|
-
const result = await this.db.delete(roles).where(
|
|
9144
|
+
async deleteById(id27) {
|
|
9145
|
+
const result = await this.db.delete(roles).where(eq16(roles.id, id27)).returning();
|
|
8706
9146
|
return result[0] ?? null;
|
|
8707
9147
|
}
|
|
8708
9148
|
};
|
|
@@ -8711,26 +9151,26 @@ var init_roles_repository = __esm({
|
|
|
8711
9151
|
});
|
|
8712
9152
|
|
|
8713
9153
|
// src/server/repositories/permissions.repository.ts
|
|
8714
|
-
import { BaseRepository as
|
|
8715
|
-
import { asc as asc2, eq as
|
|
9154
|
+
import { BaseRepository as BaseRepository17 } from "@spfn/core/db";
|
|
9155
|
+
import { asc as asc2, eq as eq17, inArray as inArray4 } from "drizzle-orm";
|
|
8716
9156
|
var PermissionsRepository, permissionsRepository;
|
|
8717
9157
|
var init_permissions_repository = __esm({
|
|
8718
9158
|
"src/server/repositories/permissions.repository.ts"() {
|
|
8719
9159
|
"use strict";
|
|
8720
9160
|
init_permissions();
|
|
8721
|
-
PermissionsRepository = class extends
|
|
9161
|
+
PermissionsRepository = class extends BaseRepository17 {
|
|
8722
9162
|
/**
|
|
8723
9163
|
* ID로 권한 조회
|
|
8724
9164
|
*/
|
|
8725
|
-
async findById(
|
|
8726
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
9165
|
+
async findById(id27) {
|
|
9166
|
+
const result = await this.readDb.select().from(permissions).where(eq17(permissions.id, id27)).limit(1);
|
|
8727
9167
|
return result[0] ?? null;
|
|
8728
9168
|
}
|
|
8729
9169
|
/**
|
|
8730
9170
|
* Name으로 권한 조회
|
|
8731
9171
|
*/
|
|
8732
9172
|
async findByName(name) {
|
|
8733
|
-
const result = await this.readDb.select().from(permissions).where(
|
|
9173
|
+
const result = await this.readDb.select().from(permissions).where(eq17(permissions.name, name)).limit(1);
|
|
8734
9174
|
return result[0] ?? null;
|
|
8735
9175
|
}
|
|
8736
9176
|
/**
|
|
@@ -8738,7 +9178,7 @@ var init_permissions_repository = __esm({
|
|
|
8738
9178
|
*/
|
|
8739
9179
|
async findByNames(names) {
|
|
8740
9180
|
if (names.length === 0) return [];
|
|
8741
|
-
return this.readDb.select().from(permissions).where(
|
|
9181
|
+
return this.readDb.select().from(permissions).where(inArray4(permissions.name, names));
|
|
8742
9182
|
}
|
|
8743
9183
|
/**
|
|
8744
9184
|
* 모든 권한 조회
|
|
@@ -8750,13 +9190,13 @@ var init_permissions_repository = __esm({
|
|
|
8750
9190
|
* 활성 권한만 조회
|
|
8751
9191
|
*/
|
|
8752
9192
|
async findActive() {
|
|
8753
|
-
return this.readDb.select().from(permissions).where(
|
|
9193
|
+
return this.readDb.select().from(permissions).where(eq17(permissions.isActive, true)).orderBy(asc2(permissions.name));
|
|
8754
9194
|
}
|
|
8755
9195
|
/**
|
|
8756
9196
|
* 카테고리별 권한 조회
|
|
8757
9197
|
*/
|
|
8758
9198
|
async findByCategory(category) {
|
|
8759
|
-
return this.readDb.select().from(permissions).where(
|
|
9199
|
+
return this.readDb.select().from(permissions).where(eq17(permissions.category, category)).orderBy(asc2(permissions.name));
|
|
8760
9200
|
}
|
|
8761
9201
|
/**
|
|
8762
9202
|
* 권한 생성
|
|
@@ -8774,15 +9214,15 @@ var init_permissions_repository = __esm({
|
|
|
8774
9214
|
/**
|
|
8775
9215
|
* 권한 업데이트
|
|
8776
9216
|
*/
|
|
8777
|
-
async updateById(
|
|
8778
|
-
const result = await this.db.update(permissions).set(data).where(
|
|
9217
|
+
async updateById(id27, data) {
|
|
9218
|
+
const result = await this.db.update(permissions).set(data).where(eq17(permissions.id, id27)).returning();
|
|
8779
9219
|
return result[0] ?? null;
|
|
8780
9220
|
}
|
|
8781
9221
|
/**
|
|
8782
9222
|
* 권한 삭제
|
|
8783
9223
|
*/
|
|
8784
|
-
async deleteById(
|
|
8785
|
-
const result = await this.db.delete(permissions).where(
|
|
9224
|
+
async deleteById(id27) {
|
|
9225
|
+
const result = await this.db.delete(permissions).where(eq17(permissions.id, id27)).returning();
|
|
8786
9226
|
return result[0] ?? null;
|
|
8787
9227
|
}
|
|
8788
9228
|
};
|
|
@@ -8791,25 +9231,25 @@ var init_permissions_repository = __esm({
|
|
|
8791
9231
|
});
|
|
8792
9232
|
|
|
8793
9233
|
// src/server/repositories/role-permissions.repository.ts
|
|
8794
|
-
import { BaseRepository as
|
|
8795
|
-
import { and as
|
|
9234
|
+
import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
|
|
9235
|
+
import { and as and16, eq as eq18 } from "drizzle-orm";
|
|
8796
9236
|
var RolePermissionsRepository, rolePermissionsRepository;
|
|
8797
9237
|
var init_role_permissions_repository = __esm({
|
|
8798
9238
|
"src/server/repositories/role-permissions.repository.ts"() {
|
|
8799
9239
|
"use strict";
|
|
8800
9240
|
init_role_permissions();
|
|
8801
|
-
RolePermissionsRepository = class extends
|
|
9241
|
+
RolePermissionsRepository = class extends BaseRepository18 {
|
|
8802
9242
|
/**
|
|
8803
9243
|
* 역할 ID로 모든 권한 조회
|
|
8804
9244
|
*/
|
|
8805
9245
|
async findByRoleId(roleId) {
|
|
8806
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
9246
|
+
return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.roleId, roleId));
|
|
8807
9247
|
}
|
|
8808
9248
|
/**
|
|
8809
9249
|
* 권한 ID로 모든 역할 조회
|
|
8810
9250
|
*/
|
|
8811
9251
|
async findByPermissionId(permissionId) {
|
|
8812
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
9252
|
+
return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.permissionId, permissionId));
|
|
8813
9253
|
}
|
|
8814
9254
|
/**
|
|
8815
9255
|
* 역할-권한 매핑 생성
|
|
@@ -8829,9 +9269,9 @@ var init_role_permissions_repository = __esm({
|
|
|
8829
9269
|
*/
|
|
8830
9270
|
async deleteByRoleIdAndPermissionId(roleId, permissionId) {
|
|
8831
9271
|
const result = await this.db.delete(rolePermissions).where(
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
9272
|
+
and16(
|
|
9273
|
+
eq18(rolePermissions.roleId, roleId),
|
|
9274
|
+
eq18(rolePermissions.permissionId, permissionId)
|
|
8835
9275
|
)
|
|
8836
9276
|
).returning();
|
|
8837
9277
|
return result[0] ?? null;
|
|
@@ -8840,7 +9280,7 @@ var init_role_permissions_repository = __esm({
|
|
|
8840
9280
|
* 역할의 모든 권한 매핑 삭제
|
|
8841
9281
|
*/
|
|
8842
9282
|
async deleteByRoleId(roleId) {
|
|
8843
|
-
const result = await this.db.delete(rolePermissions).where(
|
|
9283
|
+
const result = await this.db.delete(rolePermissions).where(eq18(rolePermissions.roleId, roleId)).returning();
|
|
8844
9284
|
return result.length;
|
|
8845
9285
|
}
|
|
8846
9286
|
/**
|
|
@@ -8861,19 +9301,19 @@ var init_role_permissions_repository = __esm({
|
|
|
8861
9301
|
});
|
|
8862
9302
|
|
|
8863
9303
|
// src/server/repositories/user-permissions.repository.ts
|
|
8864
|
-
import { BaseRepository as
|
|
8865
|
-
import { eq as
|
|
9304
|
+
import { BaseRepository as BaseRepository19 } from "@spfn/core/db";
|
|
9305
|
+
import { eq as eq19, and as and17, or as or5, isNull as isNull13, isNotNull as isNotNull3, lt as lt6, gt as gt9 } from "drizzle-orm";
|
|
8866
9306
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
8867
9307
|
var init_user_permissions_repository = __esm({
|
|
8868
9308
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
8869
9309
|
"use strict";
|
|
8870
9310
|
init_user_permissions();
|
|
8871
|
-
UserPermissionsRepository = class extends
|
|
9311
|
+
UserPermissionsRepository = class extends BaseRepository19 {
|
|
8872
9312
|
/**
|
|
8873
9313
|
* 사용자 ID로 모든 권한 오버라이드 조회
|
|
8874
9314
|
*/
|
|
8875
9315
|
async findByUserId(userId) {
|
|
8876
|
-
return this.readDb.select().from(userPermissions).where(
|
|
9316
|
+
return this.readDb.select().from(userPermissions).where(eq19(userPermissions.userId, userId));
|
|
8877
9317
|
}
|
|
8878
9318
|
/**
|
|
8879
9319
|
* 사용자 ID로 유효한 권한 오버라이드만 조회
|
|
@@ -8882,11 +9322,11 @@ var init_user_permissions_repository = __esm({
|
|
|
8882
9322
|
async findValidByUserId(userId) {
|
|
8883
9323
|
const now = /* @__PURE__ */ new Date();
|
|
8884
9324
|
return this.readDb.select().from(userPermissions).where(
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
9325
|
+
and17(
|
|
9326
|
+
eq19(userPermissions.userId, userId),
|
|
9327
|
+
or5(
|
|
9328
|
+
isNull13(userPermissions.expiresAt),
|
|
9329
|
+
gt9(userPermissions.expiresAt, now)
|
|
8890
9330
|
)
|
|
8891
9331
|
)
|
|
8892
9332
|
);
|
|
@@ -8896,9 +9336,9 @@ var init_user_permissions_repository = __esm({
|
|
|
8896
9336
|
*/
|
|
8897
9337
|
async findByUserIdAndPermissionId(userId, permissionId) {
|
|
8898
9338
|
const result = await this.readDb.select().from(userPermissions).where(
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
9339
|
+
and17(
|
|
9340
|
+
eq19(userPermissions.userId, userId),
|
|
9341
|
+
eq19(userPermissions.permissionId, permissionId)
|
|
8902
9342
|
)
|
|
8903
9343
|
).limit(1);
|
|
8904
9344
|
return result[0] ?? null;
|
|
@@ -8912,8 +9352,8 @@ var init_user_permissions_repository = __esm({
|
|
|
8912
9352
|
/**
|
|
8913
9353
|
* 사용자 권한 오버라이드 업데이트
|
|
8914
9354
|
*/
|
|
8915
|
-
async updateById(
|
|
8916
|
-
const result = await this.db.update(userPermissions).set(data).where(
|
|
9355
|
+
async updateById(id27, data) {
|
|
9356
|
+
const result = await this.db.update(userPermissions).set(data).where(eq19(userPermissions.id, id27)).returning();
|
|
8917
9357
|
return result[0] ?? null;
|
|
8918
9358
|
}
|
|
8919
9359
|
/**
|
|
@@ -8921,9 +9361,9 @@ var init_user_permissions_repository = __esm({
|
|
|
8921
9361
|
*/
|
|
8922
9362
|
async deleteByUserIdAndPermissionId(userId, permissionId) {
|
|
8923
9363
|
const result = await this.db.delete(userPermissions).where(
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
9364
|
+
and17(
|
|
9365
|
+
eq19(userPermissions.userId, userId),
|
|
9366
|
+
eq19(userPermissions.permissionId, permissionId)
|
|
8927
9367
|
)
|
|
8928
9368
|
).returning();
|
|
8929
9369
|
return result[0] ?? null;
|
|
@@ -8932,7 +9372,7 @@ var init_user_permissions_repository = __esm({
|
|
|
8932
9372
|
* 사용자의 모든 권한 오버라이드 삭제
|
|
8933
9373
|
*/
|
|
8934
9374
|
async deleteByUserId(userId) {
|
|
8935
|
-
const result = await this.db.delete(userPermissions).where(
|
|
9375
|
+
const result = await this.db.delete(userPermissions).where(eq19(userPermissions.userId, userId)).returning();
|
|
8936
9376
|
return result.length;
|
|
8937
9377
|
}
|
|
8938
9378
|
/**
|
|
@@ -8941,7 +9381,7 @@ var init_user_permissions_repository = __esm({
|
|
|
8941
9381
|
async deleteExpired() {
|
|
8942
9382
|
const now = /* @__PURE__ */ new Date();
|
|
8943
9383
|
const result = await this.db.delete(userPermissions).where(
|
|
8944
|
-
|
|
9384
|
+
and17(
|
|
8945
9385
|
isNotNull3(userPermissions.expiresAt),
|
|
8946
9386
|
lt6(userPermissions.expiresAt, now)
|
|
8947
9387
|
)
|
|
@@ -8954,33 +9394,33 @@ var init_user_permissions_repository = __esm({
|
|
|
8954
9394
|
});
|
|
8955
9395
|
|
|
8956
9396
|
// src/server/repositories/user-profiles.repository.ts
|
|
8957
|
-
import { BaseRepository as
|
|
8958
|
-
import { eq as
|
|
9397
|
+
import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
|
|
9398
|
+
import { eq as eq20 } from "drizzle-orm";
|
|
8959
9399
|
var UserProfilesRepository, userProfilesRepository;
|
|
8960
9400
|
var init_user_profiles_repository = __esm({
|
|
8961
9401
|
"src/server/repositories/user-profiles.repository.ts"() {
|
|
8962
9402
|
"use strict";
|
|
8963
9403
|
init_user_profiles();
|
|
8964
|
-
UserProfilesRepository = class extends
|
|
9404
|
+
UserProfilesRepository = class extends BaseRepository20 {
|
|
8965
9405
|
/**
|
|
8966
9406
|
* ID로 프로필 조회
|
|
8967
9407
|
*/
|
|
8968
|
-
async findById(
|
|
8969
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
9408
|
+
async findById(id27) {
|
|
9409
|
+
const result = await this.readDb.select().from(userProfiles).where(eq20(userProfiles.id, id27)).limit(1);
|
|
8970
9410
|
return result[0] ?? null;
|
|
8971
9411
|
}
|
|
8972
9412
|
/**
|
|
8973
9413
|
* User ID로 locale만 조회 (경량)
|
|
8974
9414
|
*/
|
|
8975
9415
|
async findLocaleByUserId(userId) {
|
|
8976
|
-
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(
|
|
9416
|
+
const result = await this.readDb.select({ locale: userProfiles.locale }).from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1);
|
|
8977
9417
|
return result[0]?.locale || "en";
|
|
8978
9418
|
}
|
|
8979
9419
|
/**
|
|
8980
9420
|
* User ID로 프로필 조회
|
|
8981
9421
|
*/
|
|
8982
9422
|
async findByUserId(userId) {
|
|
8983
|
-
const result = await this.readDb.select().from(userProfiles).where(
|
|
9423
|
+
const result = await this.readDb.select().from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1);
|
|
8984
9424
|
return result[0] ?? null;
|
|
8985
9425
|
}
|
|
8986
9426
|
/**
|
|
@@ -8992,29 +9432,29 @@ var init_user_profiles_repository = __esm({
|
|
|
8992
9432
|
/**
|
|
8993
9433
|
* 프로필 업데이트 (by ID)
|
|
8994
9434
|
*/
|
|
8995
|
-
async updateById(
|
|
8996
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
9435
|
+
async updateById(id27, data) {
|
|
9436
|
+
const result = await this.db.update(userProfiles).set(data).where(eq20(userProfiles.id, id27)).returning();
|
|
8997
9437
|
return result[0] ?? null;
|
|
8998
9438
|
}
|
|
8999
9439
|
/**
|
|
9000
9440
|
* 프로필 업데이트 (by User ID)
|
|
9001
9441
|
*/
|
|
9002
9442
|
async updateByUserId(userId, data) {
|
|
9003
|
-
const result = await this.db.update(userProfiles).set(data).where(
|
|
9443
|
+
const result = await this.db.update(userProfiles).set(data).where(eq20(userProfiles.userId, userId)).returning();
|
|
9004
9444
|
return result[0] ?? null;
|
|
9005
9445
|
}
|
|
9006
9446
|
/**
|
|
9007
9447
|
* 프로필 삭제 (by ID)
|
|
9008
9448
|
*/
|
|
9009
|
-
async deleteById(
|
|
9010
|
-
const result = await this.db.delete(userProfiles).where(
|
|
9449
|
+
async deleteById(id27) {
|
|
9450
|
+
const result = await this.db.delete(userProfiles).where(eq20(userProfiles.id, id27)).returning();
|
|
9011
9451
|
return result[0] ?? null;
|
|
9012
9452
|
}
|
|
9013
9453
|
/**
|
|
9014
9454
|
* 프로필 삭제 (by User ID)
|
|
9015
9455
|
*/
|
|
9016
9456
|
async deleteByUserId(userId) {
|
|
9017
|
-
const result = await this.db.delete(userProfiles).where(
|
|
9457
|
+
const result = await this.db.delete(userProfiles).where(eq20(userProfiles.userId, userId)).returning();
|
|
9018
9458
|
return result[0] ?? null;
|
|
9019
9459
|
}
|
|
9020
9460
|
/**
|
|
@@ -9056,7 +9496,7 @@ var init_user_profiles_repository = __esm({
|
|
|
9056
9496
|
metadata: userProfiles.metadata,
|
|
9057
9497
|
createdAt: userProfiles.createdAt,
|
|
9058
9498
|
updatedAt: userProfiles.updatedAt
|
|
9059
|
-
}).from(userProfiles).where(
|
|
9499
|
+
}).from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
9060
9500
|
if (!profile) {
|
|
9061
9501
|
return null;
|
|
9062
9502
|
}
|
|
@@ -9084,8 +9524,8 @@ var init_user_profiles_repository = __esm({
|
|
|
9084
9524
|
});
|
|
9085
9525
|
|
|
9086
9526
|
// src/server/repositories/invitations.repository.ts
|
|
9087
|
-
import { eq as
|
|
9088
|
-
import { BaseRepository as
|
|
9527
|
+
import { eq as eq21, and as and18, lt as lt7, desc as desc3, sql as sql11 } from "drizzle-orm";
|
|
9528
|
+
import { BaseRepository as BaseRepository21 } from "@spfn/core/db";
|
|
9089
9529
|
var InvitationsRepository, invitationsRepository;
|
|
9090
9530
|
var init_invitations_repository = __esm({
|
|
9091
9531
|
"src/server/repositories/invitations.repository.ts"() {
|
|
@@ -9094,19 +9534,19 @@ var init_invitations_repository = __esm({
|
|
|
9094
9534
|
init_roles();
|
|
9095
9535
|
init_user_invitations();
|
|
9096
9536
|
init_email();
|
|
9097
|
-
InvitationsRepository = class extends
|
|
9537
|
+
InvitationsRepository = class extends BaseRepository21 {
|
|
9098
9538
|
/**
|
|
9099
9539
|
* ID로 초대 조회
|
|
9100
9540
|
*/
|
|
9101
|
-
async findById(
|
|
9102
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
9541
|
+
async findById(id27) {
|
|
9542
|
+
const result = await this.readDb.select().from(userInvitations).where(eq21(userInvitations.id, id27)).limit(1);
|
|
9103
9543
|
return result[0] ?? null;
|
|
9104
9544
|
}
|
|
9105
9545
|
/**
|
|
9106
9546
|
* Token으로 초대 조회
|
|
9107
9547
|
*/
|
|
9108
9548
|
async findByToken(token) {
|
|
9109
|
-
const result = await this.readDb.select().from(userInvitations).where(
|
|
9549
|
+
const result = await this.readDb.select().from(userInvitations).where(eq21(userInvitations.token, token)).limit(1);
|
|
9110
9550
|
return result[0] ?? null;
|
|
9111
9551
|
}
|
|
9112
9552
|
/**
|
|
@@ -9114,9 +9554,9 @@ var init_invitations_repository = __esm({
|
|
|
9114
9554
|
*/
|
|
9115
9555
|
async findPendingByEmail(email) {
|
|
9116
9556
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9557
|
+
and18(
|
|
9558
|
+
eq21(userInvitations.email, normalizeEmail(email)),
|
|
9559
|
+
eq21(userInvitations.status, "pending")
|
|
9120
9560
|
)
|
|
9121
9561
|
).limit(1);
|
|
9122
9562
|
return result[0] ?? null;
|
|
@@ -9125,13 +9565,13 @@ var init_invitations_repository = __esm({
|
|
|
9125
9565
|
* 초대자 ID로 모든 초대 조회
|
|
9126
9566
|
*/
|
|
9127
9567
|
async findByInvitedBy(invitedBy) {
|
|
9128
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9568
|
+
return this.readDb.select().from(userInvitations).where(eq21(userInvitations.invitedBy, invitedBy));
|
|
9129
9569
|
}
|
|
9130
9570
|
/**
|
|
9131
9571
|
* 상태별 초대 조회
|
|
9132
9572
|
*/
|
|
9133
9573
|
async findByStatus(status) {
|
|
9134
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9574
|
+
return this.readDb.select().from(userInvitations).where(eq21(userInvitations.status, status));
|
|
9135
9575
|
}
|
|
9136
9576
|
/**
|
|
9137
9577
|
* 초대 생성
|
|
@@ -9142,7 +9582,7 @@ var init_invitations_repository = __esm({
|
|
|
9142
9582
|
/**
|
|
9143
9583
|
* 초대 상태 업데이트
|
|
9144
9584
|
*/
|
|
9145
|
-
async updateStatus(
|
|
9585
|
+
async updateStatus(id27, status, timestamp2) {
|
|
9146
9586
|
const updates = {
|
|
9147
9587
|
status
|
|
9148
9588
|
};
|
|
@@ -9153,14 +9593,14 @@ var init_invitations_repository = __esm({
|
|
|
9153
9593
|
updates.cancelledAt = timestamp2;
|
|
9154
9594
|
}
|
|
9155
9595
|
}
|
|
9156
|
-
const result = await this.db.update(userInvitations).set(updates).where(
|
|
9596
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq21(userInvitations.id, id27)).returning();
|
|
9157
9597
|
return result[0] ?? null;
|
|
9158
9598
|
}
|
|
9159
9599
|
/**
|
|
9160
9600
|
* 초대 삭제
|
|
9161
9601
|
*/
|
|
9162
|
-
async deleteById(
|
|
9163
|
-
const result = await this.db.delete(userInvitations).where(
|
|
9602
|
+
async deleteById(id27) {
|
|
9603
|
+
const result = await this.db.delete(userInvitations).where(eq21(userInvitations.id, id27)).returning();
|
|
9164
9604
|
return result[0] ?? null;
|
|
9165
9605
|
}
|
|
9166
9606
|
/**
|
|
@@ -9169,8 +9609,8 @@ var init_invitations_repository = __esm({
|
|
|
9169
9609
|
async updateExpiredInvitations() {
|
|
9170
9610
|
const now = /* @__PURE__ */ new Date();
|
|
9171
9611
|
const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
|
|
9172
|
-
|
|
9173
|
-
|
|
9612
|
+
and18(
|
|
9613
|
+
eq21(userInvitations.status, "pending"),
|
|
9174
9614
|
lt7(userInvitations.expiresAt, now)
|
|
9175
9615
|
)
|
|
9176
9616
|
).returning();
|
|
@@ -9202,7 +9642,7 @@ var init_invitations_repository = __esm({
|
|
|
9202
9642
|
id: users.id,
|
|
9203
9643
|
email: users.email
|
|
9204
9644
|
}
|
|
9205
|
-
}).from(userInvitations).innerJoin(roles,
|
|
9645
|
+
}).from(userInvitations).innerJoin(roles, eq21(userInvitations.roleId, roles.id)).innerJoin(users, eq21(userInvitations.invitedBy, users.id)).where(eq21(userInvitations.token, token)).limit(1);
|
|
9206
9646
|
return result[0] ?? null;
|
|
9207
9647
|
}
|
|
9208
9648
|
/**
|
|
@@ -9213,13 +9653,13 @@ var init_invitations_repository = __esm({
|
|
|
9213
9653
|
const offset = (page - 1) * limit;
|
|
9214
9654
|
const conditions = [];
|
|
9215
9655
|
if (status) {
|
|
9216
|
-
conditions.push(
|
|
9656
|
+
conditions.push(eq21(userInvitations.status, status));
|
|
9217
9657
|
}
|
|
9218
9658
|
if (invitedBy) {
|
|
9219
|
-
conditions.push(
|
|
9659
|
+
conditions.push(eq21(userInvitations.invitedBy, invitedBy));
|
|
9220
9660
|
}
|
|
9221
|
-
const whereClause = conditions.length > 0 ?
|
|
9222
|
-
const countResult = await this.readDb.select({ count:
|
|
9661
|
+
const whereClause = conditions.length > 0 ? and18(...conditions) : void 0;
|
|
9662
|
+
const countResult = await this.readDb.select({ count: sql11`count(*)` }).from(userInvitations).where(whereClause);
|
|
9223
9663
|
const total = Number(countResult[0]?.count || 0);
|
|
9224
9664
|
const results = await this.readDb.select({
|
|
9225
9665
|
id: userInvitations.id,
|
|
@@ -9243,7 +9683,7 @@ var init_invitations_repository = __esm({
|
|
|
9243
9683
|
id: users.id,
|
|
9244
9684
|
email: users.email
|
|
9245
9685
|
}
|
|
9246
|
-
}).from(userInvitations).innerJoin(roles,
|
|
9686
|
+
}).from(userInvitations).innerJoin(roles, eq21(userInvitations.roleId, roles.id)).innerJoin(users, eq21(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc3(userInvitations.createdAt)).limit(limit).offset(offset);
|
|
9247
9687
|
return {
|
|
9248
9688
|
invitations: results,
|
|
9249
9689
|
total,
|
|
@@ -9255,31 +9695,31 @@ var init_invitations_repository = __esm({
|
|
|
9255
9695
|
/**
|
|
9256
9696
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
9257
9697
|
*/
|
|
9258
|
-
async updateById(
|
|
9698
|
+
async updateById(id27, data) {
|
|
9259
9699
|
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
9260
|
-
const result = await this.db.update(userInvitations).set(patch).where(
|
|
9700
|
+
const result = await this.db.update(userInvitations).set(patch).where(eq21(userInvitations.id, id27)).returning();
|
|
9261
9701
|
return result[0] ?? null;
|
|
9262
9702
|
}
|
|
9263
9703
|
/**
|
|
9264
9704
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
9265
9705
|
*/
|
|
9266
|
-
async resend(
|
|
9706
|
+
async resend(id27, newExpiresAt) {
|
|
9267
9707
|
const result = await this.db.update(userInvitations).set({
|
|
9268
9708
|
status: "pending",
|
|
9269
9709
|
expiresAt: newExpiresAt
|
|
9270
|
-
}).where(
|
|
9710
|
+
}).where(eq21(userInvitations.id, id27)).returning();
|
|
9271
9711
|
return result[0] ?? null;
|
|
9272
9712
|
}
|
|
9273
9713
|
/**
|
|
9274
9714
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
9275
9715
|
*/
|
|
9276
|
-
async cancel(
|
|
9716
|
+
async cancel(id27, cancelledBy, reason, currentMetadata) {
|
|
9277
9717
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
9278
9718
|
const result = await this.db.update(userInvitations).set({
|
|
9279
9719
|
status: "cancelled",
|
|
9280
9720
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
9281
9721
|
metadata: newMetadata
|
|
9282
|
-
}).where(
|
|
9722
|
+
}).where(eq21(userInvitations.id, id27)).returning();
|
|
9283
9723
|
return result[0] ?? null;
|
|
9284
9724
|
}
|
|
9285
9725
|
};
|
|
@@ -10119,15 +10559,15 @@ var init_token_cipher = __esm({
|
|
|
10119
10559
|
});
|
|
10120
10560
|
|
|
10121
10561
|
// src/server/repositories/social-accounts.repository.ts
|
|
10122
|
-
import { eq as
|
|
10123
|
-
import { BaseRepository as
|
|
10562
|
+
import { eq as eq22, and as and19 } from "drizzle-orm";
|
|
10563
|
+
import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
|
|
10124
10564
|
var SocialAccountsRepository, socialAccountsRepository;
|
|
10125
10565
|
var init_social_accounts_repository = __esm({
|
|
10126
10566
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
10127
10567
|
"use strict";
|
|
10128
10568
|
init_entities();
|
|
10129
10569
|
init_token_cipher();
|
|
10130
|
-
SocialAccountsRepository = class extends
|
|
10570
|
+
SocialAccountsRepository = class extends BaseRepository22 {
|
|
10131
10571
|
/**
|
|
10132
10572
|
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
10133
10573
|
*
|
|
@@ -10155,10 +10595,10 @@ var init_social_accounts_repository = __esm({
|
|
|
10155
10595
|
if (refresh?.needsRotation) {
|
|
10156
10596
|
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
10157
10597
|
}
|
|
10158
|
-
await this.db.update(userSocialAccounts).set(heal).where(
|
|
10159
|
-
|
|
10160
|
-
access?.needsRotation && account.accessToken !== null ?
|
|
10161
|
-
refresh?.needsRotation && account.refreshToken !== null ?
|
|
10598
|
+
await this.db.update(userSocialAccounts).set(heal).where(and19(
|
|
10599
|
+
eq22(userSocialAccounts.id, account.id),
|
|
10600
|
+
access?.needsRotation && account.accessToken !== null ? eq22(userSocialAccounts.accessToken, account.accessToken) : void 0,
|
|
10601
|
+
refresh?.needsRotation && account.refreshToken !== null ? eq22(userSocialAccounts.refreshToken, account.refreshToken) : void 0
|
|
10162
10602
|
));
|
|
10163
10603
|
} catch {
|
|
10164
10604
|
}
|
|
@@ -10175,9 +10615,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10175
10615
|
*/
|
|
10176
10616
|
async findByProviderAndProviderId(provider, providerUserId) {
|
|
10177
10617
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10618
|
+
and19(
|
|
10619
|
+
eq22(userSocialAccounts.provider, provider),
|
|
10620
|
+
eq22(userSocialAccounts.providerUserId, providerUserId)
|
|
10181
10621
|
)
|
|
10182
10622
|
).limit(1);
|
|
10183
10623
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -10187,7 +10627,7 @@ var init_social_accounts_repository = __esm({
|
|
|
10187
10627
|
* Read replica 사용
|
|
10188
10628
|
*/
|
|
10189
10629
|
async findByUserId(userId) {
|
|
10190
|
-
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10630
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId));
|
|
10191
10631
|
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
10192
10632
|
}
|
|
10193
10633
|
/**
|
|
@@ -10196,9 +10636,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10196
10636
|
*/
|
|
10197
10637
|
async findByUserIdAndProvider(userId, provider) {
|
|
10198
10638
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10639
|
+
and19(
|
|
10640
|
+
eq22(userSocialAccounts.userId, userId),
|
|
10641
|
+
eq22(userSocialAccounts.provider, provider)
|
|
10202
10642
|
)
|
|
10203
10643
|
).limit(1);
|
|
10204
10644
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -10224,11 +10664,11 @@ var init_social_accounts_repository = __esm({
|
|
|
10224
10664
|
* 토큰 정보 업데이트
|
|
10225
10665
|
* Write primary 사용
|
|
10226
10666
|
*/
|
|
10227
|
-
async updateTokens(
|
|
10667
|
+
async updateTokens(id27, data) {
|
|
10228
10668
|
const accounts = await this.db.select({
|
|
10229
10669
|
provider: userSocialAccounts.provider,
|
|
10230
10670
|
providerUserId: userSocialAccounts.providerUserId
|
|
10231
|
-
}).from(userSocialAccounts).where(
|
|
10671
|
+
}).from(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).limit(1);
|
|
10232
10672
|
const account = accounts[0];
|
|
10233
10673
|
if (!account) {
|
|
10234
10674
|
return null;
|
|
@@ -10242,15 +10682,15 @@ var init_social_accounts_repository = __esm({
|
|
|
10242
10682
|
...data,
|
|
10243
10683
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
10244
10684
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
10245
|
-
}).where(
|
|
10685
|
+
}).where(eq22(userSocialAccounts.id, id27)).returning();
|
|
10246
10686
|
return this.decryptAccount(result[0] ?? null);
|
|
10247
10687
|
}
|
|
10248
10688
|
/**
|
|
10249
10689
|
* 소셜 계정 삭제
|
|
10250
10690
|
* Write primary 사용
|
|
10251
10691
|
*/
|
|
10252
|
-
async deleteById(
|
|
10253
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
10692
|
+
async deleteById(id27) {
|
|
10693
|
+
const result = await this.db.delete(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).returning();
|
|
10254
10694
|
return result[0] ?? null;
|
|
10255
10695
|
}
|
|
10256
10696
|
/**
|
|
@@ -10259,9 +10699,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10259
10699
|
*/
|
|
10260
10700
|
async deleteByUserIdAndProvider(userId, provider) {
|
|
10261
10701
|
const result = await this.db.delete(userSocialAccounts).where(
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10702
|
+
and19(
|
|
10703
|
+
eq22(userSocialAccounts.userId, userId),
|
|
10704
|
+
eq22(userSocialAccounts.provider, provider)
|
|
10265
10705
|
)
|
|
10266
10706
|
).returning();
|
|
10267
10707
|
return result[0] ?? null;
|
|
@@ -10274,7 +10714,7 @@ var init_social_accounts_repository = __esm({
|
|
|
10274
10714
|
* Write primary 사용
|
|
10275
10715
|
*/
|
|
10276
10716
|
async deleteAllByUserId(userId) {
|
|
10277
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
10717
|
+
const result = await this.db.delete(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId)).returning();
|
|
10278
10718
|
return result.length;
|
|
10279
10719
|
}
|
|
10280
10720
|
};
|
|
@@ -10283,19 +10723,19 @@ var init_social_accounts_repository = __esm({
|
|
|
10283
10723
|
});
|
|
10284
10724
|
|
|
10285
10725
|
// src/server/repositories/auth-metadata.repository.ts
|
|
10286
|
-
import { BaseRepository as
|
|
10287
|
-
import { eq as
|
|
10726
|
+
import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
|
|
10727
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
10288
10728
|
var AuthMetadataRepository, authMetadataRepository;
|
|
10289
10729
|
var init_auth_metadata_repository = __esm({
|
|
10290
10730
|
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
10291
10731
|
"use strict";
|
|
10292
10732
|
init_auth_metadata();
|
|
10293
|
-
AuthMetadataRepository = class extends
|
|
10733
|
+
AuthMetadataRepository = class extends BaseRepository23 {
|
|
10294
10734
|
/**
|
|
10295
10735
|
* 키로 값 조회
|
|
10296
10736
|
*/
|
|
10297
10737
|
async get(key) {
|
|
10298
|
-
const result = await this.readDb.select().from(authMetadata).where(
|
|
10738
|
+
const result = await this.readDb.select().from(authMetadata).where(eq23(authMetadata.key, key)).limit(1);
|
|
10299
10739
|
return result[0]?.value ?? null;
|
|
10300
10740
|
}
|
|
10301
10741
|
/**
|
|
@@ -10318,20 +10758,20 @@ var init_auth_metadata_repository = __esm({
|
|
|
10318
10758
|
});
|
|
10319
10759
|
|
|
10320
10760
|
// src/server/repositories/account-deletion-requests.repository.ts
|
|
10321
|
-
import { eq as
|
|
10322
|
-
import { BaseRepository as
|
|
10761
|
+
import { eq as eq24, and as and20, lte as lte2 } from "drizzle-orm";
|
|
10762
|
+
import { BaseRepository as BaseRepository24 } from "@spfn/core/db";
|
|
10323
10763
|
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
10324
10764
|
var init_account_deletion_requests_repository = __esm({
|
|
10325
10765
|
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
10326
10766
|
"use strict";
|
|
10327
10767
|
init_account_deletion_requests();
|
|
10328
|
-
AccountDeletionRequestsRepository = class extends
|
|
10768
|
+
AccountDeletionRequestsRepository = class extends BaseRepository24 {
|
|
10329
10769
|
/**
|
|
10330
10770
|
* ID로 요청 조회
|
|
10331
10771
|
* Read replica 사용
|
|
10332
10772
|
*/
|
|
10333
|
-
async findById(
|
|
10334
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
10773
|
+
async findById(id27) {
|
|
10774
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq24(accountDeletionRequests.id, id27)).limit(1);
|
|
10335
10775
|
return result[0] ?? null;
|
|
10336
10776
|
}
|
|
10337
10777
|
/**
|
|
@@ -10340,9 +10780,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10340
10780
|
*/
|
|
10341
10781
|
async findPendingByUserId(userId) {
|
|
10342
10782
|
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10783
|
+
and20(
|
|
10784
|
+
eq24(accountDeletionRequests.userId, userId),
|
|
10785
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10346
10786
|
)
|
|
10347
10787
|
).limit(1);
|
|
10348
10788
|
return result[0] ?? null;
|
|
@@ -10356,9 +10796,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10356
10796
|
*/
|
|
10357
10797
|
async findPendingByUserIdOnPrimary(userId) {
|
|
10358
10798
|
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10799
|
+
and20(
|
|
10800
|
+
eq24(accountDeletionRequests.userId, userId),
|
|
10801
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10362
10802
|
)
|
|
10363
10803
|
).limit(1);
|
|
10364
10804
|
return result[0] ?? null;
|
|
@@ -10369,8 +10809,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10369
10809
|
*/
|
|
10370
10810
|
async findDueForPurge(now) {
|
|
10371
10811
|
return this.readDb.select().from(accountDeletionRequests).where(
|
|
10372
|
-
|
|
10373
|
-
|
|
10812
|
+
and20(
|
|
10813
|
+
eq24(accountDeletionRequests.status, "pending"),
|
|
10374
10814
|
lte2(accountDeletionRequests.purgeScheduledAt, now)
|
|
10375
10815
|
)
|
|
10376
10816
|
);
|
|
@@ -10390,14 +10830,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10390
10830
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
10391
10831
|
* Write primary 사용
|
|
10392
10832
|
*/
|
|
10393
|
-
async markCancelled(
|
|
10833
|
+
async markCancelled(id27) {
|
|
10394
10834
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10395
10835
|
status: "cancelled",
|
|
10396
10836
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
10397
10837
|
}).where(
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10838
|
+
and20(
|
|
10839
|
+
eq24(accountDeletionRequests.id, id27),
|
|
10840
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10401
10841
|
)
|
|
10402
10842
|
).returning();
|
|
10403
10843
|
return result[0] ?? null;
|
|
@@ -10412,15 +10852,15 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10412
10852
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
10413
10853
|
* Write primary 사용
|
|
10414
10854
|
*/
|
|
10415
|
-
async markCompleted(
|
|
10855
|
+
async markCompleted(id27, purgeStrategy) {
|
|
10416
10856
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10417
10857
|
status: "completed",
|
|
10418
10858
|
completedAt: /* @__PURE__ */ new Date(),
|
|
10419
10859
|
purgeStrategy
|
|
10420
10860
|
}).where(
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10861
|
+
and20(
|
|
10862
|
+
eq24(accountDeletionRequests.id, id27),
|
|
10863
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10424
10864
|
)
|
|
10425
10865
|
).returning();
|
|
10426
10866
|
return result[0] ?? null;
|
|
@@ -10431,14 +10871,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10431
10871
|
});
|
|
10432
10872
|
|
|
10433
10873
|
// src/server/repositories/ops-tokens.repository.ts
|
|
10434
|
-
import { and as
|
|
10435
|
-
import { BaseRepository as
|
|
10874
|
+
import { and as and21, desc as desc4, eq as eq25, isNull as isNull14 } from "drizzle-orm";
|
|
10875
|
+
import { BaseRepository as BaseRepository25 } from "@spfn/core/db";
|
|
10436
10876
|
var OpsTokensRepository, opsTokensRepository;
|
|
10437
10877
|
var init_ops_tokens_repository = __esm({
|
|
10438
10878
|
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
10439
10879
|
"use strict";
|
|
10440
10880
|
init_ops_tokens();
|
|
10441
|
-
OpsTokensRepository = class extends
|
|
10881
|
+
OpsTokensRepository = class extends BaseRepository25 {
|
|
10442
10882
|
/**
|
|
10443
10883
|
* Lookup by the secret's hash — the verification path.
|
|
10444
10884
|
*
|
|
@@ -10448,7 +10888,7 @@ var init_ops_tokens_repository = __esm({
|
|
|
10448
10888
|
* and revocation is documented as taking effect immediately.
|
|
10449
10889
|
*/
|
|
10450
10890
|
async findByTokenHash(tokenHash) {
|
|
10451
|
-
const result = await this.db.select().from(opsTokens).where(
|
|
10891
|
+
const result = await this.db.select().from(opsTokens).where(eq25(opsTokens.tokenHash, tokenHash)).limit(1);
|
|
10452
10892
|
return result[0] ?? null;
|
|
10453
10893
|
}
|
|
10454
10894
|
async create(data) {
|
|
@@ -10463,13 +10903,13 @@ var init_ops_tokens_repository = __esm({
|
|
|
10463
10903
|
* token is already revoked — the first revocation's timestamp is never
|
|
10464
10904
|
* overwritten.
|
|
10465
10905
|
*/
|
|
10466
|
-
async revokeById(
|
|
10467
|
-
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10906
|
+
async revokeById(id27) {
|
|
10907
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and21(eq25(opsTokens.id, id27), isNull14(opsTokens.revokedAt))).returning();
|
|
10468
10908
|
return result[0] ?? null;
|
|
10469
10909
|
}
|
|
10470
10910
|
/** Fire-and-forget from the verification path. */
|
|
10471
|
-
async updateLastUsedById(
|
|
10472
|
-
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
10911
|
+
async updateLastUsedById(id27) {
|
|
10912
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq25(opsTokens.id, id27));
|
|
10473
10913
|
}
|
|
10474
10914
|
};
|
|
10475
10915
|
opsTokensRepository = new OpsTokensRepository();
|
|
@@ -10477,15 +10917,15 @@ var init_ops_tokens_repository = __esm({
|
|
|
10477
10917
|
});
|
|
10478
10918
|
|
|
10479
10919
|
// src/server/repositories/oauth2-clients.repository.ts
|
|
10480
|
-
import { and as
|
|
10481
|
-
import { BaseRepository as
|
|
10920
|
+
import { and as and22, eq as eq26, gt as gt10, lt as lt8, sql as sql12 } from "drizzle-orm";
|
|
10921
|
+
import { BaseRepository as BaseRepository26, runInTransaction } from "@spfn/core/db";
|
|
10482
10922
|
var OAuth2ClientsRepository, oauth2ClientsRepository;
|
|
10483
10923
|
var init_oauth2_clients_repository = __esm({
|
|
10484
10924
|
"src/server/repositories/oauth2-clients.repository.ts"() {
|
|
10485
10925
|
"use strict";
|
|
10486
10926
|
init_oauth2_clients();
|
|
10487
10927
|
init_oauth2_grants();
|
|
10488
|
-
OAuth2ClientsRepository = class extends
|
|
10928
|
+
OAuth2ClientsRepository = class extends BaseRepository26 {
|
|
10489
10929
|
async create(data) {
|
|
10490
10930
|
const result = await this.db.insert(oauth2Clients).values(data).returning();
|
|
10491
10931
|
return result[0];
|
|
@@ -10499,12 +10939,12 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10499
10939
|
* at the one moment a CLI is being connected.
|
|
10500
10940
|
*/
|
|
10501
10941
|
async findByClientId(clientId) {
|
|
10502
|
-
const result = await this.db.select().from(oauth2Clients).where(
|
|
10942
|
+
const result = await this.db.select().from(oauth2Clients).where(eq26(oauth2Clients.clientId, clientId)).limit(1);
|
|
10503
10943
|
return result[0] ?? null;
|
|
10504
10944
|
}
|
|
10505
10945
|
/** Fire-and-forget from the token-issuing path. */
|
|
10506
|
-
async updateLastUsedById(
|
|
10507
|
-
await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
10946
|
+
async updateLastUsedById(id27) {
|
|
10947
|
+
await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq26(oauth2Clients.id, id27));
|
|
10508
10948
|
}
|
|
10509
10949
|
/**
|
|
10510
10950
|
* Register a client unless this address is already at its standing cap.
|
|
@@ -10526,7 +10966,7 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10526
10966
|
*/
|
|
10527
10967
|
async createWithinStandingCap(data, limit) {
|
|
10528
10968
|
return await runInTransaction(async () => {
|
|
10529
|
-
await this.db.execute(
|
|
10969
|
+
await this.db.execute(sql12`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
|
|
10530
10970
|
const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
|
|
10531
10971
|
return standing < limit.max ? await this.create(data) : null;
|
|
10532
10972
|
});
|
|
@@ -10548,11 +10988,11 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10548
10988
|
* count exists to see.
|
|
10549
10989
|
*/
|
|
10550
10990
|
async countRecentUngrantedByIp(ip, windowMs) {
|
|
10551
|
-
const result = await this.db.select({ count:
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10991
|
+
const result = await this.db.select({ count: sql12`count(*)::int` }).from(oauth2Clients).where(
|
|
10992
|
+
and22(
|
|
10993
|
+
eq26(oauth2Clients.createdIp, ip),
|
|
10994
|
+
gt10(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
|
|
10995
|
+
sql12`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
10556
10996
|
)
|
|
10557
10997
|
);
|
|
10558
10998
|
return result[0]?.count ?? 0;
|
|
@@ -10569,9 +11009,9 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10569
11009
|
*/
|
|
10570
11010
|
async deleteStaleUngranted(before) {
|
|
10571
11011
|
const deleted = await this.db.delete(oauth2Clients).where(
|
|
10572
|
-
|
|
11012
|
+
and22(
|
|
10573
11013
|
lt8(oauth2Clients.createdAt, before),
|
|
10574
|
-
|
|
11014
|
+
sql12`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
10575
11015
|
)
|
|
10576
11016
|
).returning({ id: oauth2Clients.id });
|
|
10577
11017
|
return deleted.length;
|
|
@@ -10582,8 +11022,8 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10582
11022
|
});
|
|
10583
11023
|
|
|
10584
11024
|
// src/server/repositories/oauth2-grants.repository.ts
|
|
10585
|
-
import { and as
|
|
10586
|
-
import { BaseRepository as
|
|
11025
|
+
import { and as and23, desc as desc5, eq as eq27, inArray as inArray5, isNull as isNull15, sql as sql13 } from "drizzle-orm";
|
|
11026
|
+
import { BaseRepository as BaseRepository27 } from "@spfn/core/db";
|
|
10587
11027
|
var OAuth2GrantsRepository, oauth2GrantsRepository;
|
|
10588
11028
|
var init_oauth2_grants_repository = __esm({
|
|
10589
11029
|
"src/server/repositories/oauth2-grants.repository.ts"() {
|
|
@@ -10591,7 +11031,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10591
11031
|
init_oauth2_grants();
|
|
10592
11032
|
init_oauth2_clients();
|
|
10593
11033
|
init_oauth2_tokens();
|
|
10594
|
-
OAuth2GrantsRepository = class extends
|
|
11034
|
+
OAuth2GrantsRepository = class extends BaseRepository27 {
|
|
10595
11035
|
/**
|
|
10596
11036
|
* Record a consent, or refresh the scopes of the one already there.
|
|
10597
11037
|
*
|
|
@@ -10618,13 +11058,13 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10618
11058
|
*
|
|
10619
11059
|
* Read primary: revocation is documented as taking effect immediately.
|
|
10620
11060
|
*/
|
|
10621
|
-
async findWithClientById(
|
|
10622
|
-
const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients,
|
|
11061
|
+
async findWithClientById(id27) {
|
|
11062
|
+
const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq27(oauth2Grants.client, oauth2Clients.id)).where(eq27(oauth2Grants.id, id27)).limit(1);
|
|
10623
11063
|
return result[0] ?? null;
|
|
10624
11064
|
}
|
|
10625
11065
|
/** What the user's "connected apps" screen lists. Read replica. */
|
|
10626
11066
|
async listActiveByUserId(userId) {
|
|
10627
|
-
return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients,
|
|
11067
|
+
return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq27(oauth2Grants.client, oauth2Clients.id)).where(and23(eq27(oauth2Grants.user, userId), isNull15(oauth2Grants.revokedAt))).orderBy(desc5(oauth2Grants.createdAt));
|
|
10628
11068
|
}
|
|
10629
11069
|
/**
|
|
10630
11070
|
* Revoke one live grant belonging to one user.
|
|
@@ -10637,19 +11077,19 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10637
11077
|
* @returns the revoked row, or null if there was no live grant of that id
|
|
10638
11078
|
* for that user
|
|
10639
11079
|
*/
|
|
10640
|
-
async revokeByIdForUser(
|
|
11080
|
+
async revokeByIdForUser(id27, userId) {
|
|
10641
11081
|
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
11082
|
+
and23(
|
|
11083
|
+
eq27(oauth2Grants.id, id27),
|
|
11084
|
+
eq27(oauth2Grants.user, userId),
|
|
11085
|
+
isNull15(oauth2Grants.revokedAt)
|
|
10646
11086
|
)
|
|
10647
11087
|
).returning();
|
|
10648
11088
|
return result[0] ?? null;
|
|
10649
11089
|
}
|
|
10650
11090
|
/** Revoke one grant by id, whoever it belongs to — the replay detections. */
|
|
10651
|
-
async revokeById(
|
|
10652
|
-
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
11091
|
+
async revokeById(id27) {
|
|
11092
|
+
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and23(eq27(oauth2Grants.id, id27), isNull15(oauth2Grants.revokedAt))).returning();
|
|
10653
11093
|
return result[0] ?? null;
|
|
10654
11094
|
}
|
|
10655
11095
|
/**
|
|
@@ -10659,7 +11099,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10659
11099
|
* @returns the grant ids this call revoked
|
|
10660
11100
|
*/
|
|
10661
11101
|
async revokeAllActiveByUserId(userId) {
|
|
10662
|
-
const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
11102
|
+
const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and23(eq27(oauth2Grants.user, userId), isNull15(oauth2Grants.revokedAt))).returning({ id: oauth2Grants.id });
|
|
10663
11103
|
return revoked.map((row) => row.id);
|
|
10664
11104
|
}
|
|
10665
11105
|
/**
|
|
@@ -10675,7 +11115,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10675
11115
|
if (grantIds.length === 0) {
|
|
10676
11116
|
return 0;
|
|
10677
11117
|
}
|
|
10678
|
-
const revoked = await this.db.update(oauth2Tokens).set({ revokedAt:
|
|
11118
|
+
const revoked = await this.db.update(oauth2Tokens).set({ revokedAt: sql13`now()` }).where(and23(inArray5(oauth2Tokens.grant, grantIds), isNull15(oauth2Tokens.revokedAt))).returning({ id: oauth2Tokens.id });
|
|
10679
11119
|
return revoked.length;
|
|
10680
11120
|
}
|
|
10681
11121
|
};
|
|
@@ -10684,14 +11124,14 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10684
11124
|
});
|
|
10685
11125
|
|
|
10686
11126
|
// src/server/repositories/oauth2-authorization-codes.repository.ts
|
|
10687
|
-
import { and as
|
|
10688
|
-
import { BaseRepository as
|
|
11127
|
+
import { and as and24, eq as eq28, gt as gt11, isNull as isNull16, sql as sql14 } from "drizzle-orm";
|
|
11128
|
+
import { BaseRepository as BaseRepository28 } from "@spfn/core/db";
|
|
10689
11129
|
var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
|
|
10690
11130
|
var init_oauth2_authorization_codes_repository = __esm({
|
|
10691
11131
|
"src/server/repositories/oauth2-authorization-codes.repository.ts"() {
|
|
10692
11132
|
"use strict";
|
|
10693
11133
|
init_oauth2_authorization_codes();
|
|
10694
|
-
OAuth2AuthorizationCodesRepository = class extends
|
|
11134
|
+
OAuth2AuthorizationCodesRepository = class extends BaseRepository28 {
|
|
10695
11135
|
async create(data) {
|
|
10696
11136
|
const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
|
|
10697
11137
|
return result[0];
|
|
@@ -10703,11 +11143,11 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10703
11143
|
* spent, or past its 60 seconds
|
|
10704
11144
|
*/
|
|
10705
11145
|
async consume(codeHash) {
|
|
10706
|
-
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt:
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10710
|
-
|
|
11146
|
+
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql14`now()` }).where(
|
|
11147
|
+
and24(
|
|
11148
|
+
eq28(oauth2AuthorizationCodes.codeHash, codeHash),
|
|
11149
|
+
isNull16(oauth2AuthorizationCodes.usedAt),
|
|
11150
|
+
gt11(oauth2AuthorizationCodes.expiresAt, sql14`now()`)
|
|
10711
11151
|
)
|
|
10712
11152
|
).returning();
|
|
10713
11153
|
return result[0] ?? null;
|
|
@@ -10718,7 +11158,7 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10718
11158
|
* would be indistinguishable from a code that never existed.
|
|
10719
11159
|
*/
|
|
10720
11160
|
async findByCodeHash(codeHash) {
|
|
10721
|
-
const result = await this.db.select().from(oauth2AuthorizationCodes).where(
|
|
11161
|
+
const result = await this.db.select().from(oauth2AuthorizationCodes).where(eq28(oauth2AuthorizationCodes.codeHash, codeHash)).limit(1);
|
|
10722
11162
|
return result[0] ?? null;
|
|
10723
11163
|
}
|
|
10724
11164
|
};
|
|
@@ -10727,14 +11167,14 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10727
11167
|
});
|
|
10728
11168
|
|
|
10729
11169
|
// src/server/repositories/oauth2-tokens.repository.ts
|
|
10730
|
-
import { and as
|
|
10731
|
-
import { BaseRepository as
|
|
11170
|
+
import { and as and25, eq as eq29, gt as gt12, isNull as isNull17, sql as sql15 } from "drizzle-orm";
|
|
11171
|
+
import { BaseRepository as BaseRepository29 } from "@spfn/core/db";
|
|
10732
11172
|
var OAuth2TokensRepository, oauth2TokensRepository;
|
|
10733
11173
|
var init_oauth2_tokens_repository = __esm({
|
|
10734
11174
|
"src/server/repositories/oauth2-tokens.repository.ts"() {
|
|
10735
11175
|
"use strict";
|
|
10736
11176
|
init_oauth2_tokens();
|
|
10737
|
-
OAuth2TokensRepository = class extends
|
|
11177
|
+
OAuth2TokensRepository = class extends BaseRepository29 {
|
|
10738
11178
|
async create(data) {
|
|
10739
11179
|
const result = await this.db.insert(oauth2Tokens).values(data).returning();
|
|
10740
11180
|
return result[0];
|
|
@@ -10748,7 +11188,7 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10748
11188
|
* Unfiltered, so the service can tell revoked from expired from unknown.
|
|
10749
11189
|
*/
|
|
10750
11190
|
async findByTokenHash(tokenHash) {
|
|
10751
|
-
const result = await this.db.select().from(oauth2Tokens).where(
|
|
11191
|
+
const result = await this.db.select().from(oauth2Tokens).where(eq29(oauth2Tokens.tokenHash, tokenHash)).limit(1);
|
|
10752
11192
|
return result[0] ?? null;
|
|
10753
11193
|
}
|
|
10754
11194
|
/**
|
|
@@ -10758,13 +11198,13 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10758
11198
|
* rotated, revoked, or expired
|
|
10759
11199
|
*/
|
|
10760
11200
|
async rotate(tokenHash) {
|
|
10761
|
-
const result = await this.db.update(oauth2Tokens).set({ replacedAt:
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
11201
|
+
const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql15`now()` }).where(
|
|
11202
|
+
and25(
|
|
11203
|
+
eq29(oauth2Tokens.tokenHash, tokenHash),
|
|
11204
|
+
eq29(oauth2Tokens.kind, "refresh"),
|
|
11205
|
+
isNull17(oauth2Tokens.replacedAt),
|
|
11206
|
+
isNull17(oauth2Tokens.revokedAt),
|
|
11207
|
+
gt12(oauth2Tokens.expiresAt, sql15`now()`)
|
|
10768
11208
|
)
|
|
10769
11209
|
).returning();
|
|
10770
11210
|
return result[0] ?? null;
|
|
@@ -10777,12 +11217,12 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10777
11217
|
* about whether the value it presented ever existed.
|
|
10778
11218
|
*/
|
|
10779
11219
|
async revokeByTokenHash(tokenHash) {
|
|
10780
|
-
const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
11220
|
+
const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and25(eq29(oauth2Tokens.tokenHash, tokenHash), isNull17(oauth2Tokens.revokedAt))).returning();
|
|
10781
11221
|
return result[0] ?? null;
|
|
10782
11222
|
}
|
|
10783
11223
|
/** Fire-and-forget from the verification path, as ops tokens do. */
|
|
10784
|
-
async updateLastUsedById(
|
|
10785
|
-
await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(
|
|
11224
|
+
async updateLastUsedById(id27) {
|
|
11225
|
+
await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq29(oauth2Tokens.id, id27));
|
|
10786
11226
|
}
|
|
10787
11227
|
};
|
|
10788
11228
|
oauth2TokensRepository = new OAuth2TokensRepository();
|
|
@@ -10807,6 +11247,7 @@ var init_repositories = __esm({
|
|
|
10807
11247
|
init_mfa_challenges_repository();
|
|
10808
11248
|
init_mfa_enrolment_repository();
|
|
10809
11249
|
init_device_authorizations_repository();
|
|
11250
|
+
init_device_links_repository();
|
|
10810
11251
|
init_roles_repository();
|
|
10811
11252
|
init_permissions_repository();
|
|
10812
11253
|
init_role_permissions_repository();
|
|
@@ -10912,7 +11353,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
10912
11353
|
}
|
|
10913
11354
|
async function setRolePermissions(roleId, permissionIds) {
|
|
10914
11355
|
const roleIdNum = Number(roleId);
|
|
10915
|
-
const permissionIdNums = permissionIds.map((
|
|
11356
|
+
const permissionIdNums = permissionIds.map((id27) => Number(id27));
|
|
10916
11357
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
10917
11358
|
}
|
|
10918
11359
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -10932,7 +11373,7 @@ async function getRolePermissions(roleId) {
|
|
|
10932
11373
|
}
|
|
10933
11374
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
10934
11375
|
const perms = await Promise.all(
|
|
10935
|
-
permissionIds.map((
|
|
11376
|
+
permissionIds.map((id27) => permissionsRepository.findById(id27))
|
|
10936
11377
|
);
|
|
10937
11378
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
10938
11379
|
}
|
|
@@ -11196,8 +11637,8 @@ async function listOAuth2GrantsService(userId) {
|
|
|
11196
11637
|
lastUsedAtMillis: client.lastUsedAt?.getTime()
|
|
11197
11638
|
}));
|
|
11198
11639
|
}
|
|
11199
|
-
async function revokeOAuth2GrantService(
|
|
11200
|
-
const revoked = await oauth2GrantsRepository.revokeByIdForUser(
|
|
11640
|
+
async function revokeOAuth2GrantService(id27, userId) {
|
|
11641
|
+
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id27, userId);
|
|
11201
11642
|
if (!revoked) {
|
|
11202
11643
|
throw new OAuth2GrantNotFoundError();
|
|
11203
11644
|
}
|
|
@@ -11587,7 +12028,7 @@ import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/a
|
|
|
11587
12028
|
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
11588
12029
|
|
|
11589
12030
|
// src/server/services/device-registration.service.ts
|
|
11590
|
-
import { onAfterCommit } from "@spfn/core/db";
|
|
12031
|
+
import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
|
|
11591
12032
|
|
|
11592
12033
|
// src/server/events/index.ts
|
|
11593
12034
|
init_esm();
|
|
@@ -11623,6 +12064,7 @@ var DeviceRegistrationChannelSchema = Type.Union([
|
|
|
11623
12064
|
Type.Literal("oauth"),
|
|
11624
12065
|
Type.Literal("oauth-native"),
|
|
11625
12066
|
Type.Literal("device-code"),
|
|
12067
|
+
Type.Literal("device-link"),
|
|
11626
12068
|
Type.Literal("password-reset"),
|
|
11627
12069
|
Type.Literal("passkey"),
|
|
11628
12070
|
Type.Literal("renewal")
|
|
@@ -11739,7 +12181,7 @@ init_repositories();
|
|
|
11739
12181
|
var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
|
|
11740
12182
|
async function emitDeviceRegistered(row, channel) {
|
|
11741
12183
|
const mfaEnrolled = await mfaEnrolmentRepository.isEnrolled(row.userId);
|
|
11742
|
-
|
|
12184
|
+
onAfterCommit2(() => authDeviceRegisteredEvent.emit({
|
|
11743
12185
|
userId: String(row.userId),
|
|
11744
12186
|
keyId: row.keyId,
|
|
11745
12187
|
algorithm: row.algorithm,
|
|
@@ -11757,7 +12199,7 @@ async function emitDeviceRegistered(row, channel) {
|
|
|
11757
12199
|
// src/server/services/mfa.service.ts
|
|
11758
12200
|
init_logger();
|
|
11759
12201
|
init_config();
|
|
11760
|
-
import { onAfterCommit as
|
|
12202
|
+
import { onAfterCommit as onAfterCommit3, runInTransaction as runInTransaction2 } from "@spfn/core/db";
|
|
11761
12203
|
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
11762
12204
|
import {
|
|
11763
12205
|
MfaAlreadyEnrolledError,
|
|
@@ -11855,16 +12297,16 @@ function encodeBase32(bytes) {
|
|
|
11855
12297
|
}
|
|
11856
12298
|
return bits > 0 ? output + BASE32_ALPHABET[value << 5 - bits & 31] : output;
|
|
11857
12299
|
}
|
|
11858
|
-
function decodeBase32(
|
|
12300
|
+
function decodeBase32(text28) {
|
|
11859
12301
|
let bits = 0;
|
|
11860
12302
|
let value = 0;
|
|
11861
12303
|
const bytes = [];
|
|
11862
|
-
for (const character of
|
|
11863
|
-
const
|
|
11864
|
-
if (
|
|
12304
|
+
for (const character of text28.replace(/=+$/, "").toUpperCase()) {
|
|
12305
|
+
const index22 = BASE32_ALPHABET.indexOf(character);
|
|
12306
|
+
if (index22 < 0) {
|
|
11865
12307
|
throw new Error("Value is not RFC 4648 base32");
|
|
11866
12308
|
}
|
|
11867
|
-
value = value << 5 |
|
|
12309
|
+
value = value << 5 | index22;
|
|
11868
12310
|
bits += 5;
|
|
11869
12311
|
if (bits >= 8) {
|
|
11870
12312
|
bytes.push(value >>> bits - 8 & 255);
|
|
@@ -12154,9 +12596,9 @@ function presentedChallenge(clientDataJSON) {
|
|
|
12154
12596
|
const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
|
|
12155
12597
|
return typeof decoded?.challenge === "string" ? decoded.challenge : "";
|
|
12156
12598
|
}
|
|
12157
|
-
function parseJson(
|
|
12599
|
+
function parseJson(text28) {
|
|
12158
12600
|
try {
|
|
12159
|
-
return JSON.parse(
|
|
12601
|
+
return JSON.parse(text28);
|
|
12160
12602
|
} catch {
|
|
12161
12603
|
return null;
|
|
12162
12604
|
}
|
|
@@ -12230,7 +12672,7 @@ async function releaseDeferredAnnouncements(row, key) {
|
|
|
12230
12672
|
return;
|
|
12231
12673
|
}
|
|
12232
12674
|
const mfaEnrolled = await mfaEnrolledForUser(row.userId);
|
|
12233
|
-
|
|
12675
|
+
onAfterCommit3(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
|
|
12234
12676
|
}
|
|
12235
12677
|
async function verifyMfaChallengeService(params) {
|
|
12236
12678
|
const challengeHash = hashCredential(params.challenge);
|
|
@@ -12324,7 +12766,7 @@ async function countConfirmFailure(userId) {
|
|
|
12324
12766
|
function readSecret(secretEnc, userId) {
|
|
12325
12767
|
const { value, needsRotation } = decryptMfaSecret(secretEnc, userId);
|
|
12326
12768
|
if (needsRotation) {
|
|
12327
|
-
|
|
12769
|
+
onAfterCommit3(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
|
|
12328
12770
|
}
|
|
12329
12771
|
return value;
|
|
12330
12772
|
}
|
|
@@ -12623,6 +13065,7 @@ async function revokeAllKeysService(params) {
|
|
|
12623
13065
|
}
|
|
12624
13066
|
const revoked = !includeCurrent && currentKeyId ? await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason) : await keysRepository.revokeAllActiveByUserId(userId, reason);
|
|
12625
13067
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
13068
|
+
await deviceLinksRepository.expireAllLiveByUserId(userId);
|
|
12626
13069
|
await revokeAllOAuth2GrantsForUser(userId);
|
|
12627
13070
|
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
12628
13071
|
}
|
|
@@ -12633,7 +13076,7 @@ init_key_policy();
|
|
|
12633
13076
|
// src/server/services/account-deletion.service.ts
|
|
12634
13077
|
init_repositories();
|
|
12635
13078
|
import { ValidationError as ValidationError4, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
|
|
12636
|
-
import { runInTransaction as runInTransaction3, onAfterCommit as
|
|
13079
|
+
import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
|
|
12637
13080
|
import { sendEmail as sendEmail3 } from "@spfn/notification/server";
|
|
12638
13081
|
import {
|
|
12639
13082
|
InvalidCredentialsError,
|
|
@@ -12708,8 +13151,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
12708
13151
|
throw new VerificationTokenTargetMismatchError();
|
|
12709
13152
|
}
|
|
12710
13153
|
}
|
|
12711
|
-
async function sendDeletionEmail(to, subject,
|
|
12712
|
-
const result = await sendEmail3({ to, subject, text:
|
|
13154
|
+
async function sendDeletionEmail(to, subject, text28) {
|
|
13155
|
+
const result = await sendEmail3({ to, subject, text: text28 });
|
|
12713
13156
|
if (!result.success) {
|
|
12714
13157
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
12715
13158
|
}
|
|
@@ -12761,12 +13204,12 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
12761
13204
|
if (requestedBy === "self") {
|
|
12762
13205
|
await verifyReauthCredential(user, { password, verificationToken });
|
|
12763
13206
|
}
|
|
12764
|
-
const
|
|
13207
|
+
const config5 = getDeletionConfig();
|
|
12765
13208
|
const wantsImmediate = immediate === true;
|
|
12766
|
-
if (wantsImmediate && requestedBy === "self" && !
|
|
13209
|
+
if (wantsImmediate && requestedBy === "self" && !config5.allowSelfImmediate) {
|
|
12767
13210
|
throw new ImmediateDeletionNotAllowedError();
|
|
12768
13211
|
}
|
|
12769
|
-
const gracePeriodDays = wantsImmediate ? 0 :
|
|
13212
|
+
const gracePeriodDays = wantsImmediate ? 0 : config5.gracePeriodDays;
|
|
12770
13213
|
const requestedAt = /* @__PURE__ */ new Date();
|
|
12771
13214
|
const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
|
|
12772
13215
|
await usersRepository.updateById(user.id, { status: "pending_deletion" });
|
|
@@ -12789,14 +13232,15 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
12789
13232
|
}
|
|
12790
13233
|
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
12791
13234
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
13235
|
+
await deviceLinksRepository.expireAllLiveByUserId(user.id);
|
|
12792
13236
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
12793
|
-
|
|
13237
|
+
onAfterCommit4(() => authDeletionRequestedEvent.emit({
|
|
12794
13238
|
userId: String(user.id),
|
|
12795
13239
|
userPublicId: user.publicId,
|
|
12796
13240
|
purgeScheduledAt: purgeScheduledAt.toISOString(),
|
|
12797
13241
|
requestedBy
|
|
12798
13242
|
}));
|
|
12799
|
-
|
|
13243
|
+
onAfterCommit4(() => notifyDeletionRequested(user, purgeScheduledAt));
|
|
12800
13244
|
if (gracePeriodDays === 0) {
|
|
12801
13245
|
await purgePendingRequest(request);
|
|
12802
13246
|
}
|
|
@@ -12828,11 +13272,11 @@ async function cancelAccountDeletionService(params) {
|
|
|
12828
13272
|
throw new DeletionNotRequestedError();
|
|
12829
13273
|
}
|
|
12830
13274
|
await usersRepository.reactivateFromPendingDeletion(user.id);
|
|
12831
|
-
|
|
13275
|
+
onAfterCommit4(() => authDeletionCancelledEvent.emit({
|
|
12832
13276
|
userId: String(user.id),
|
|
12833
13277
|
userPublicId: user.publicId
|
|
12834
13278
|
}));
|
|
12835
|
-
|
|
13279
|
+
onAfterCommit4(() => notifyDeletionCancelled(user));
|
|
12836
13280
|
return { userId: String(user.id) };
|
|
12837
13281
|
}
|
|
12838
13282
|
async function anonymizeUser(user) {
|
|
@@ -12873,10 +13317,10 @@ async function purgePendingRequest(request) {
|
|
|
12873
13317
|
if (!precheckUser || precheckUser.status !== "pending_deletion") {
|
|
12874
13318
|
return { outcome: "skipped" };
|
|
12875
13319
|
}
|
|
12876
|
-
const
|
|
12877
|
-
if (
|
|
13320
|
+
const config5 = getDeletionConfig();
|
|
13321
|
+
if (config5.onBeforePurge) {
|
|
12878
13322
|
try {
|
|
12879
|
-
await
|
|
13323
|
+
await config5.onBeforePurge({
|
|
12880
13324
|
id: precheckUser.id,
|
|
12881
13325
|
publicId: precheckUser.publicId,
|
|
12882
13326
|
email: precheckUser.email,
|
|
@@ -12890,7 +13334,7 @@ async function purgePendingRequest(request) {
|
|
|
12890
13334
|
return { outcome: "skipped" };
|
|
12891
13335
|
}
|
|
12892
13336
|
}
|
|
12893
|
-
const purgeStrategy =
|
|
13337
|
+
const purgeStrategy = config5.purgeStrategy;
|
|
12894
13338
|
let purgedUser = null;
|
|
12895
13339
|
await runInTransaction3(async () => {
|
|
12896
13340
|
const user = await usersRepository.findById(userId);
|
|
@@ -12919,9 +13363,9 @@ async function purgePendingRequest(request) {
|
|
|
12919
13363
|
}
|
|
12920
13364
|
const { email, publicId } = purgedUser;
|
|
12921
13365
|
if (email) {
|
|
12922
|
-
|
|
13366
|
+
onAfterCommit4(() => notifyPurgeFinal(email));
|
|
12923
13367
|
}
|
|
12924
|
-
|
|
13368
|
+
onAfterCommit4(() => authDeletionCompletedEvent.emit({
|
|
12925
13369
|
userPublicId: publicId,
|
|
12926
13370
|
purgeStrategy
|
|
12927
13371
|
}));
|
|
@@ -13135,6 +13579,7 @@ async function changePasswordService(params) {
|
|
|
13135
13579
|
const newPasswordHash = await hashPassword(newPassword);
|
|
13136
13580
|
await usersRepository.updatePassword(userId, newPasswordHash, true);
|
|
13137
13581
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
13582
|
+
await deviceLinksRepository.expireAllLiveByUserId(userId);
|
|
13138
13583
|
await revokeAllOAuth2GrantsForUser(userId);
|
|
13139
13584
|
await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
|
|
13140
13585
|
}
|
|
@@ -13275,7 +13720,7 @@ init_repositories();
|
|
|
13275
13720
|
import { env as env11 } from "@spfn/auth/config";
|
|
13276
13721
|
import { PasswordResetLinkError, PasswordResetSessionError } from "@spfn/auth/errors";
|
|
13277
13722
|
import { ValidationError as ValidationError6 } from "@spfn/core/errors";
|
|
13278
|
-
import { onAfterCommit as
|
|
13723
|
+
import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
|
|
13279
13724
|
init_key_policy();
|
|
13280
13725
|
async function activeUserOf(userId) {
|
|
13281
13726
|
const user = await usersRepository.findByIdOnPrimary(userId);
|
|
@@ -13336,7 +13781,9 @@ async function replaceCredentials(row, user, params) {
|
|
|
13336
13781
|
// for accounts created before the register flows stamped it.
|
|
13337
13782
|
...user.emailVerifiedAt ? {} : { emailVerifiedAt: /* @__PURE__ */ new Date() }
|
|
13338
13783
|
});
|
|
13784
|
+
await keysRepository.lockActiveByUserId(user.id);
|
|
13339
13785
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
13786
|
+
await deviceLinksRepository.expireAllLiveByUserId(user.id);
|
|
13340
13787
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
13341
13788
|
await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
|
|
13342
13789
|
const registered = await registerPublicKeyService({
|
|
@@ -13376,7 +13823,7 @@ async function completePasswordResetService(params) {
|
|
|
13376
13823
|
throw new PasswordResetSessionError();
|
|
13377
13824
|
}
|
|
13378
13825
|
const registered = await replaceCredentials(row, user, params);
|
|
13379
|
-
|
|
13826
|
+
onAfterCommit5(() => authPasswordResetEvent.emit({
|
|
13380
13827
|
userId: String(user.id),
|
|
13381
13828
|
email: row.email
|
|
13382
13829
|
}));
|
|
@@ -13468,21 +13915,26 @@ import {
|
|
|
13468
13915
|
DeviceAuthDeniedError,
|
|
13469
13916
|
InvalidKeyFingerprintError as InvalidKeyFingerprintError2
|
|
13470
13917
|
} from "@spfn/auth/errors";
|
|
13471
|
-
import { onAfterCommit as
|
|
13918
|
+
import { onAfterCommit as onAfterCommit6 } from "@spfn/core/db";
|
|
13919
|
+
import { getShutdownManager } from "@spfn/core/server";
|
|
13472
13920
|
|
|
13473
13921
|
// src/server/lib/device-auth-config.ts
|
|
13474
13922
|
var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
|
|
13475
13923
|
var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
|
|
13924
|
+
var DEFAULT_DEVICE_AUTH_MAX_WAIT_MS = 20 * 1e3;
|
|
13476
13925
|
var config2 = {
|
|
13477
13926
|
ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
13478
|
-
intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
|
|
13927
|
+
intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
13928
|
+
maxWaitMs: DEFAULT_DEVICE_AUTH_MAX_WAIT_MS
|
|
13479
13929
|
};
|
|
13480
13930
|
function configureDeviceAuth(options) {
|
|
13481
13931
|
const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
|
|
13482
13932
|
const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
|
|
13933
|
+
const maxWaitMs = options?.maxWaitMs ?? DEFAULT_DEVICE_AUTH_MAX_WAIT_MS;
|
|
13483
13934
|
assertWholeMillis("ttlMs", ttlMs);
|
|
13484
13935
|
assertWholeMillis("intervalMs", intervalMs);
|
|
13485
|
-
|
|
13936
|
+
assertWholeMillis("maxWaitMs", maxWaitMs);
|
|
13937
|
+
config2 = { ttlMs, intervalMs, maxWaitMs };
|
|
13486
13938
|
}
|
|
13487
13939
|
function assertWholeMillis(name, value) {
|
|
13488
13940
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -13495,6 +13947,9 @@ function getDeviceAuthConfig() {
|
|
|
13495
13947
|
return config2;
|
|
13496
13948
|
}
|
|
13497
13949
|
|
|
13950
|
+
// src/server/services/device-auth.service.ts
|
|
13951
|
+
init_device_auth_waiters();
|
|
13952
|
+
|
|
13498
13953
|
// src/server/lib/device-code.ts
|
|
13499
13954
|
import { createHash, randomBytes, randomInt } from "crypto";
|
|
13500
13955
|
var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
@@ -13524,6 +13979,8 @@ function hashDeviceCode(deviceCode) {
|
|
|
13524
13979
|
// src/server/services/device-auth.service.ts
|
|
13525
13980
|
init_key_policy();
|
|
13526
13981
|
var USER_CODE_ATTEMPTS = 3;
|
|
13982
|
+
var WAIT_RECHECK_MS = 1e3;
|
|
13983
|
+
var MAX_WAITERS_PER_RECORD = 3;
|
|
13527
13984
|
function assertActionable(record) {
|
|
13528
13985
|
if (!record || record.status === "consumed") {
|
|
13529
13986
|
throw new DeviceAuthNotFoundError();
|
|
@@ -13623,6 +14080,37 @@ async function denyDeviceAuthService(params) {
|
|
|
13623
14080
|
);
|
|
13624
14081
|
}
|
|
13625
14082
|
}
|
|
14083
|
+
async function waitForDeviceAuthAnswerService(params) {
|
|
14084
|
+
const requested = Math.min(params.waitMillis, getDeviceAuthConfig().maxWaitMs);
|
|
14085
|
+
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
14086
|
+
const record = requested > 0 ? await readWaitable(deviceCodeHash) : null;
|
|
14087
|
+
if (!record || waitingOnDeviceAuth(record.id) >= MAX_WAITERS_PER_RECORD) {
|
|
14088
|
+
return 0;
|
|
14089
|
+
}
|
|
14090
|
+
const startedAt = Date.now();
|
|
14091
|
+
const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
|
|
14092
|
+
await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
|
|
14093
|
+
return Date.now() - startedAt;
|
|
14094
|
+
}
|
|
14095
|
+
async function waitUntil(id27, deviceCodeHash, deadline, signal) {
|
|
14096
|
+
while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
|
|
14097
|
+
const remaining = deadline - Date.now();
|
|
14098
|
+
if (remaining <= 0) {
|
|
14099
|
+
return;
|
|
14100
|
+
}
|
|
14101
|
+
await waitForDeviceAuthAnswer(id27, Math.min(remaining, WAIT_RECHECK_MS), signal);
|
|
14102
|
+
if (!await readWaitable(deviceCodeHash)) {
|
|
14103
|
+
return;
|
|
14104
|
+
}
|
|
14105
|
+
}
|
|
14106
|
+
}
|
|
14107
|
+
async function readWaitable(deviceCodeHash) {
|
|
14108
|
+
const record = await deviceAuthorizationsRepository.findByDeviceCodeHashOnPrimary(deviceCodeHash).catch(() => null);
|
|
14109
|
+
return isWaitable(record) ? record : null;
|
|
14110
|
+
}
|
|
14111
|
+
function isWaitable(record) {
|
|
14112
|
+
return record?.status === "pending" && record.expiresAt.getTime() > Date.now();
|
|
14113
|
+
}
|
|
13626
14114
|
async function pollDeviceAuthService(params) {
|
|
13627
14115
|
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
13628
14116
|
const record = assertActionable(
|
|
@@ -13632,7 +14120,10 @@ async function pollDeviceAuthService(params) {
|
|
|
13632
14120
|
throw new DeviceAuthDeniedError();
|
|
13633
14121
|
}
|
|
13634
14122
|
if (record.status === "pending") {
|
|
13635
|
-
return {
|
|
14123
|
+
return {
|
|
14124
|
+
status: "pending",
|
|
14125
|
+
intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
|
|
14126
|
+
};
|
|
13636
14127
|
}
|
|
13637
14128
|
const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
|
|
13638
14129
|
if (!consumed) {
|
|
@@ -13642,15 +14133,25 @@ async function pollDeviceAuthService(params) {
|
|
|
13642
14133
|
() => new DeviceAuthNotFoundError()
|
|
13643
14134
|
);
|
|
13644
14135
|
}
|
|
13645
|
-
|
|
13646
|
-
}
|
|
13647
|
-
async function completeDeviceLogin(record, provenance) {
|
|
13648
|
-
if (record.userId === null) {
|
|
14136
|
+
if (consumed.userId === null) {
|
|
13649
14137
|
throw new DeviceAuthNotFoundError();
|
|
13650
14138
|
}
|
|
13651
|
-
|
|
14139
|
+
return {
|
|
14140
|
+
status: "approved",
|
|
14141
|
+
...await completeDeviceLogin({
|
|
14142
|
+
userId: consumed.userId,
|
|
14143
|
+
key: consumed,
|
|
14144
|
+
channel: "device-code",
|
|
14145
|
+
provenance: params,
|
|
14146
|
+
missingAccount: () => new DeviceAuthNotFoundError()
|
|
14147
|
+
})
|
|
14148
|
+
};
|
|
14149
|
+
}
|
|
14150
|
+
async function completeDeviceLogin(params) {
|
|
14151
|
+
const { key, provenance } = params;
|
|
14152
|
+
const user = await usersRepository.findById(params.userId);
|
|
13652
14153
|
if (!user) {
|
|
13653
|
-
throw
|
|
14154
|
+
throw params.missingAccount();
|
|
13654
14155
|
}
|
|
13655
14156
|
if (user.status !== "active") {
|
|
13656
14157
|
if (user.status === "pending_deletion") {
|
|
@@ -13663,22 +14164,22 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13663
14164
|
}
|
|
13664
14165
|
const registered = await registerPublicKeyService({
|
|
13665
14166
|
userId: user.id,
|
|
13666
|
-
keyId:
|
|
13667
|
-
publicKey:
|
|
13668
|
-
fingerprint:
|
|
13669
|
-
algorithm:
|
|
13670
|
-
deviceName:
|
|
13671
|
-
platform:
|
|
13672
|
-
channel:
|
|
14167
|
+
keyId: key.keyId,
|
|
14168
|
+
publicKey: key.publicKey,
|
|
14169
|
+
fingerprint: key.fingerprint,
|
|
14170
|
+
algorithm: key.algorithm,
|
|
14171
|
+
deviceName: key.deviceName ?? void 0,
|
|
14172
|
+
platform: key.platform ?? void 0,
|
|
14173
|
+
channel: params.channel,
|
|
13673
14174
|
ip: provenance.ip,
|
|
13674
14175
|
userAgent: provenance.userAgent,
|
|
13675
14176
|
binding: decideKeyBinding(user.sessionBinding, provenance.webProxy)
|
|
13676
14177
|
});
|
|
13677
14178
|
await updateLastLoginService(user.id);
|
|
13678
14179
|
const result = {
|
|
13679
|
-
//
|
|
13680
|
-
//
|
|
13681
|
-
// channel
|
|
14180
|
+
// An approval from a device that is already signed in is itself a
|
|
14181
|
+
// second factor — the owner read the code there and said yes — so
|
|
14182
|
+
// neither channel ever steps up (#95).
|
|
13682
14183
|
mfaRequired: false,
|
|
13683
14184
|
userId: String(user.id),
|
|
13684
14185
|
publicId: user.publicId,
|
|
@@ -13688,7 +14189,7 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13688
14189
|
...loginBindingFields(registeredBinding(registered))
|
|
13689
14190
|
};
|
|
13690
14191
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13691
|
-
|
|
14192
|
+
onAfterCommit6(() => authLoginEvent.emit({
|
|
13692
14193
|
userId: String(user.id),
|
|
13693
14194
|
provider: "device",
|
|
13694
14195
|
email: result.email,
|
|
@@ -13698,10 +14199,324 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13698
14199
|
return result;
|
|
13699
14200
|
}
|
|
13700
14201
|
|
|
14202
|
+
// src/server/services/device-link.service.ts
|
|
14203
|
+
init_repositories();
|
|
14204
|
+
import { randomUUID } from "crypto";
|
|
14205
|
+
import {
|
|
14206
|
+
DeviceLinkAlreadyHandledError,
|
|
14207
|
+
DeviceLinkDeniedError,
|
|
14208
|
+
DeviceLinkExpiredError,
|
|
14209
|
+
DeviceLinkNotFoundError,
|
|
14210
|
+
DeviceLinkNotRedeemedError,
|
|
14211
|
+
DeviceLinkWrongMatchError,
|
|
14212
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError3
|
|
14213
|
+
} from "@spfn/auth/errors";
|
|
14214
|
+
import { getShutdownManager as getShutdownManager2 } from "@spfn/core/server";
|
|
14215
|
+
|
|
14216
|
+
// src/server/lib/device-link-config.ts
|
|
14217
|
+
var DEFAULT_DEVICE_LINK_TTL_MS = 5 * 60 * 1e3;
|
|
14218
|
+
var config3 = {
|
|
14219
|
+
ttlMs: DEFAULT_DEVICE_LINK_TTL_MS
|
|
14220
|
+
};
|
|
14221
|
+
function configureDeviceLink(options) {
|
|
14222
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_LINK_TTL_MS;
|
|
14223
|
+
if (!Number.isInteger(ttlMs) || ttlMs <= 0) {
|
|
14224
|
+
throw new Error(`deviceLink.ttlMs must be a positive whole number of milliseconds, received ${ttlMs}.`);
|
|
14225
|
+
}
|
|
14226
|
+
config3 = { ttlMs };
|
|
14227
|
+
}
|
|
14228
|
+
function getDeviceLinkConfig() {
|
|
14229
|
+
return config3;
|
|
14230
|
+
}
|
|
14231
|
+
|
|
14232
|
+
// src/server/lib/device-link-match.ts
|
|
14233
|
+
import { randomInt as randomInt2 } from "crypto";
|
|
14234
|
+
var MATCH_MIN = 10;
|
|
14235
|
+
var MATCH_MAX_EXCLUSIVE = 100;
|
|
14236
|
+
var DEVICE_LINK_CHOICE_COUNT = 3;
|
|
14237
|
+
function generateMatchNumber() {
|
|
14238
|
+
return randomInt2(MATCH_MIN, MATCH_MAX_EXCLUSIVE);
|
|
14239
|
+
}
|
|
14240
|
+
function generateChoices(matchNumber) {
|
|
14241
|
+
const choices = /* @__PURE__ */ new Set([matchNumber]);
|
|
14242
|
+
while (choices.size < DEVICE_LINK_CHOICE_COUNT) {
|
|
14243
|
+
choices.add(generateMatchNumber());
|
|
14244
|
+
}
|
|
14245
|
+
return shuffle([...choices]);
|
|
14246
|
+
}
|
|
14247
|
+
function shuffle(values) {
|
|
14248
|
+
for (let index22 = values.length - 1; index22 > 0; index22--) {
|
|
14249
|
+
const other = randomInt2(index22 + 1);
|
|
14250
|
+
[values[index22], values[other]] = [values[other], values[index22]];
|
|
14251
|
+
}
|
|
14252
|
+
return values;
|
|
14253
|
+
}
|
|
14254
|
+
|
|
14255
|
+
// src/server/services/device-link.service.ts
|
|
14256
|
+
init_device_link_waiters();
|
|
14257
|
+
var USER_CODE_ATTEMPTS2 = 3;
|
|
14258
|
+
var WAIT_RECHECK_MS2 = 1e3;
|
|
14259
|
+
var MAX_WAITERS_PER_RECORD2 = 3;
|
|
14260
|
+
function isDead(record) {
|
|
14261
|
+
return record.status === "expired" || record.expiresAt.getTime() <= Date.now() || !record.issuerKeyLive;
|
|
14262
|
+
}
|
|
14263
|
+
function assertReachable(record) {
|
|
14264
|
+
if (!record || record.status === "consumed") {
|
|
14265
|
+
throw new DeviceLinkNotFoundError();
|
|
14266
|
+
}
|
|
14267
|
+
if (isDead(record)) {
|
|
14268
|
+
throw new DeviceLinkExpiredError();
|
|
14269
|
+
}
|
|
14270
|
+
return record;
|
|
14271
|
+
}
|
|
14272
|
+
function assertIssuedBy(record, issuer) {
|
|
14273
|
+
if (!record || record.issuerUserId !== issuer.userId || record.issuerKeyId !== issuer.keyId) {
|
|
14274
|
+
throw new DeviceLinkNotFoundError();
|
|
14275
|
+
}
|
|
14276
|
+
if (isDead(record)) {
|
|
14277
|
+
throw new DeviceLinkExpiredError();
|
|
14278
|
+
}
|
|
14279
|
+
return record;
|
|
14280
|
+
}
|
|
14281
|
+
function refuseMissedTransition2(record, from, moved) {
|
|
14282
|
+
if (!record || record.status === "consumed") {
|
|
14283
|
+
throw new DeviceLinkNotFoundError();
|
|
14284
|
+
}
|
|
14285
|
+
if (record.status === from || isDead(record)) {
|
|
14286
|
+
throw new DeviceLinkExpiredError();
|
|
14287
|
+
}
|
|
14288
|
+
throw moved();
|
|
14289
|
+
}
|
|
14290
|
+
function assertAwaitingDecision(record) {
|
|
14291
|
+
if (record.status === "issued") {
|
|
14292
|
+
throw new DeviceLinkNotRedeemedError();
|
|
14293
|
+
}
|
|
14294
|
+
if (record.status !== "redeemed") {
|
|
14295
|
+
throw new DeviceLinkAlreadyHandledError();
|
|
14296
|
+
}
|
|
14297
|
+
}
|
|
14298
|
+
function describeLink(record) {
|
|
14299
|
+
return {
|
|
14300
|
+
status: record.status,
|
|
14301
|
+
expiresAtMillis: record.expiresAt.getTime(),
|
|
14302
|
+
...describeDevice2(record),
|
|
14303
|
+
...record.status === "redeemed" && record.choices ? { choices: record.choices } : {}
|
|
14304
|
+
};
|
|
14305
|
+
}
|
|
14306
|
+
function describeDevice2(record) {
|
|
14307
|
+
if (!record.redeemedAt || !record.fingerprint) {
|
|
14308
|
+
return {};
|
|
14309
|
+
}
|
|
14310
|
+
return {
|
|
14311
|
+
deviceName: record.deviceName ?? void 0,
|
|
14312
|
+
platform: record.platform ?? void 0,
|
|
14313
|
+
fingerprintPrefix: record.fingerprint.slice(0, KEY_FINGERPRINT_PREFIX_LENGTH),
|
|
14314
|
+
redeemedAtMillis: record.redeemedAt.getTime()
|
|
14315
|
+
};
|
|
14316
|
+
}
|
|
14317
|
+
async function issueDeviceLinkService(issuer) {
|
|
14318
|
+
await deviceLinksRepository.lockIssuerKey(issuer.keyId);
|
|
14319
|
+
await deviceLinksRepository.expireLiveByIssuerKey(issuer.keyId);
|
|
14320
|
+
const expiresAt = new Date(Date.now() + getDeviceLinkConfig().ttlMs);
|
|
14321
|
+
for (let attempt = 0; attempt < USER_CODE_ATTEMPTS2; attempt++) {
|
|
14322
|
+
const record = await deviceLinksRepository.create({
|
|
14323
|
+
linkId: randomUUID(),
|
|
14324
|
+
userCode: generateUserCode(),
|
|
14325
|
+
issuerUserId: issuer.userId,
|
|
14326
|
+
issuerKeyId: issuer.keyId,
|
|
14327
|
+
expiresAt
|
|
14328
|
+
});
|
|
14329
|
+
if (record) {
|
|
14330
|
+
return {
|
|
14331
|
+
linkId: record.linkId,
|
|
14332
|
+
userCode: formatUserCode(record.userCode),
|
|
14333
|
+
expiresAtMillis: expiresAt.getTime()
|
|
14334
|
+
};
|
|
14335
|
+
}
|
|
14336
|
+
}
|
|
14337
|
+
throw new Error(
|
|
14338
|
+
`Could not allocate a unique device link code in ${USER_CODE_ATTEMPTS2} attempts. Check the code generator and the user_code unique index.`
|
|
14339
|
+
);
|
|
14340
|
+
}
|
|
14341
|
+
async function redeemDeviceLinkService(params) {
|
|
14342
|
+
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
14343
|
+
throw new InvalidKeyFingerprintError3();
|
|
14344
|
+
}
|
|
14345
|
+
const algorithm = params.algorithm ?? DEFAULT_KEY_ALGORITHM;
|
|
14346
|
+
assertKeyMatchesAlgorithm(params.publicKey, algorithm);
|
|
14347
|
+
const userCode = normalizeUserCode(params.userCode);
|
|
14348
|
+
const record = assertReachable(await deviceLinksRepository.findByUserCode(userCode));
|
|
14349
|
+
if (record.status !== "issued") {
|
|
14350
|
+
throw new DeviceLinkNotFoundError();
|
|
14351
|
+
}
|
|
14352
|
+
const deviceCode = generateDeviceCode();
|
|
14353
|
+
const matchNumber = generateMatchNumber();
|
|
14354
|
+
const redeemed = await deviceLinksRepository.redeem(record.id, {
|
|
14355
|
+
deviceCodeHash: hashDeviceCode(deviceCode),
|
|
14356
|
+
publicKey: params.publicKey,
|
|
14357
|
+
keyId: params.keyId,
|
|
14358
|
+
fingerprint: params.fingerprint,
|
|
14359
|
+
algorithm,
|
|
14360
|
+
deviceName: params.deviceName,
|
|
14361
|
+
platform: params.platform,
|
|
14362
|
+
matchNumber,
|
|
14363
|
+
choices: generateChoices(matchNumber)
|
|
14364
|
+
});
|
|
14365
|
+
if (!redeemed) {
|
|
14366
|
+
refuseMissedTransition2(
|
|
14367
|
+
await deviceLinksRepository.findByUserCode(userCode),
|
|
14368
|
+
"issued",
|
|
14369
|
+
() => new DeviceLinkNotFoundError()
|
|
14370
|
+
);
|
|
14371
|
+
}
|
|
14372
|
+
return {
|
|
14373
|
+
deviceCode,
|
|
14374
|
+
matchNumber,
|
|
14375
|
+
expiresAtMillis: redeemed.expiresAt.getTime(),
|
|
14376
|
+
intervalMillis: getDeviceAuthConfig().intervalMs
|
|
14377
|
+
};
|
|
14378
|
+
}
|
|
14379
|
+
async function getDeviceLinkStatusService(params) {
|
|
14380
|
+
return describeLink(assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer));
|
|
14381
|
+
}
|
|
14382
|
+
async function confirmDeviceLinkService(params) {
|
|
14383
|
+
const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
|
|
14384
|
+
assertAwaitingDecision(record);
|
|
14385
|
+
if (params.choice !== record.matchNumber) {
|
|
14386
|
+
await refuse(record);
|
|
14387
|
+
throw new DeviceLinkWrongMatchError();
|
|
14388
|
+
}
|
|
14389
|
+
const approved = await deviceLinksRepository.approve(record.id);
|
|
14390
|
+
if (!approved) {
|
|
14391
|
+
refuseMissedTransition2(
|
|
14392
|
+
await deviceLinksRepository.findByLinkId(params.linkId),
|
|
14393
|
+
"redeemed",
|
|
14394
|
+
() => new DeviceLinkAlreadyHandledError()
|
|
14395
|
+
);
|
|
14396
|
+
}
|
|
14397
|
+
return describeLink(approved);
|
|
14398
|
+
}
|
|
14399
|
+
async function denyDeviceLinkService(params) {
|
|
14400
|
+
const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
|
|
14401
|
+
assertAwaitingDecision(record);
|
|
14402
|
+
return describeLink(await refuse(record));
|
|
14403
|
+
}
|
|
14404
|
+
async function refuse(record) {
|
|
14405
|
+
const denied = await deviceLinksRepository.deny(record.id);
|
|
14406
|
+
if (!denied) {
|
|
14407
|
+
refuseMissedTransition2(
|
|
14408
|
+
await deviceLinksRepository.findByLinkId(record.linkId),
|
|
14409
|
+
"redeemed",
|
|
14410
|
+
() => new DeviceLinkAlreadyHandledError()
|
|
14411
|
+
);
|
|
14412
|
+
}
|
|
14413
|
+
return denied;
|
|
14414
|
+
}
|
|
14415
|
+
async function cancelDeviceLinkService(params) {
|
|
14416
|
+
const record = assertIssuedBy(await deviceLinksRepository.findByLinkId(params.linkId), params.issuer);
|
|
14417
|
+
if (record.status !== "issued" && record.status !== "redeemed") {
|
|
14418
|
+
throw new DeviceLinkAlreadyHandledError();
|
|
14419
|
+
}
|
|
14420
|
+
const cancelled = await deviceLinksRepository.cancel(record.id);
|
|
14421
|
+
if (!cancelled) {
|
|
14422
|
+
refuseMissedTransition2(
|
|
14423
|
+
await deviceLinksRepository.findByLinkId(params.linkId),
|
|
14424
|
+
record.status,
|
|
14425
|
+
() => new DeviceLinkAlreadyHandledError()
|
|
14426
|
+
);
|
|
14427
|
+
}
|
|
14428
|
+
return describeLink(cancelled);
|
|
14429
|
+
}
|
|
14430
|
+
async function pollDeviceLinkService(params) {
|
|
14431
|
+
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
14432
|
+
const record = assertReachable(await deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash));
|
|
14433
|
+
if (record.status === "denied") {
|
|
14434
|
+
throw new DeviceLinkDeniedError();
|
|
14435
|
+
}
|
|
14436
|
+
if (record.status === "redeemed") {
|
|
14437
|
+
return {
|
|
14438
|
+
status: "pending",
|
|
14439
|
+
intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
|
|
14440
|
+
};
|
|
14441
|
+
}
|
|
14442
|
+
const consumed = await deviceLinksRepository.consumeApproved(deviceCodeHash);
|
|
14443
|
+
if (!consumed) {
|
|
14444
|
+
refuseMissedTransition2(
|
|
14445
|
+
await deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash),
|
|
14446
|
+
"approved",
|
|
14447
|
+
() => new DeviceLinkNotFoundError()
|
|
14448
|
+
);
|
|
14449
|
+
}
|
|
14450
|
+
return {
|
|
14451
|
+
status: "approved",
|
|
14452
|
+
...await completeDeviceLogin({
|
|
14453
|
+
userId: consumed.issuerUserId,
|
|
14454
|
+
key: parkedKey(consumed),
|
|
14455
|
+
channel: "device-link",
|
|
14456
|
+
provenance: params,
|
|
14457
|
+
missingAccount: () => new DeviceLinkNotFoundError()
|
|
14458
|
+
})
|
|
14459
|
+
};
|
|
14460
|
+
}
|
|
14461
|
+
function parkedKey(link) {
|
|
14462
|
+
const { keyId, publicKey, fingerprint, algorithm } = link;
|
|
14463
|
+
if (!keyId || !publicKey || !fingerprint || !algorithm) {
|
|
14464
|
+
throw new DeviceLinkNotFoundError();
|
|
14465
|
+
}
|
|
14466
|
+
return { keyId, publicKey, fingerprint, algorithm, deviceName: link.deviceName, platform: link.platform };
|
|
14467
|
+
}
|
|
14468
|
+
async function waitForDeviceLinkStatusService(params) {
|
|
14469
|
+
return holdWhileUnmoved(
|
|
14470
|
+
() => deviceLinksRepository.findByLinkId(params.linkId),
|
|
14471
|
+
(record) => isIssuedBy(record, params.issuer) && (record.status === "issued" || record.status === "approved"),
|
|
14472
|
+
params.waitMillis,
|
|
14473
|
+
params.signal
|
|
14474
|
+
);
|
|
14475
|
+
}
|
|
14476
|
+
async function waitForDeviceLinkAnswerService(params) {
|
|
14477
|
+
const deviceCodeHash = hashDeviceCode(params.deviceCode);
|
|
14478
|
+
return holdWhileUnmoved(
|
|
14479
|
+
() => deviceLinksRepository.findByDeviceCodeHash(deviceCodeHash),
|
|
14480
|
+
(record) => record.status === "redeemed",
|
|
14481
|
+
params.waitMillis,
|
|
14482
|
+
params.signal
|
|
14483
|
+
);
|
|
14484
|
+
}
|
|
14485
|
+
function isIssuedBy(record, issuer) {
|
|
14486
|
+
return record.issuerUserId === issuer.userId && record.issuerKeyId === issuer.keyId;
|
|
14487
|
+
}
|
|
14488
|
+
async function holdWhileUnmoved(read, waitable, waitMillis, signal) {
|
|
14489
|
+
const requested = Math.min(waitMillis, getDeviceAuthConfig().maxWaitMs);
|
|
14490
|
+
const record = requested > 0 ? await read().catch(() => null) : null;
|
|
14491
|
+
if (!record || isDead(record) || !waitable(record) || waitingOnDeviceLink(record.id) >= MAX_WAITERS_PER_RECORD2) {
|
|
14492
|
+
return 0;
|
|
14493
|
+
}
|
|
14494
|
+
const startedAt = Date.now();
|
|
14495
|
+
const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
|
|
14496
|
+
const unmoved = async () => {
|
|
14497
|
+
const current = await read().catch(() => null);
|
|
14498
|
+
return current !== null && !isDead(current) && current.status === record.status;
|
|
14499
|
+
};
|
|
14500
|
+
await holdDeviceLinkWait(record.id, () => waitUntil2(record.id, deadline, unmoved, signal));
|
|
14501
|
+
return Date.now() - startedAt;
|
|
14502
|
+
}
|
|
14503
|
+
async function waitUntil2(id27, deadline, unmoved, signal) {
|
|
14504
|
+
while (!signal?.aborted && !getShutdownManager2().isShuttingDown()) {
|
|
14505
|
+
const remaining = deadline - Date.now();
|
|
14506
|
+
if (remaining <= 0) {
|
|
14507
|
+
return;
|
|
14508
|
+
}
|
|
14509
|
+
await waitForDeviceLinkMove(id27, Math.min(remaining, WAIT_RECHECK_MS2), signal);
|
|
14510
|
+
if (!await unmoved()) {
|
|
14511
|
+
return;
|
|
14512
|
+
}
|
|
14513
|
+
}
|
|
14514
|
+
}
|
|
14515
|
+
|
|
13701
14516
|
// src/server/services/passkey.service.ts
|
|
13702
14517
|
init_logger();
|
|
13703
14518
|
init_config();
|
|
13704
|
-
import { onAfterCommit as
|
|
14519
|
+
import { onAfterCommit as onAfterCommit7, runInTransaction as runInTransaction4 } from "@spfn/core/db";
|
|
13705
14520
|
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
13706
14521
|
import {
|
|
13707
14522
|
AccountDisabledError as AccountDisabledError3,
|
|
@@ -13819,7 +14634,7 @@ async function finishPasskeyEnrollmentService(params) {
|
|
|
13819
14634
|
aaguid: verified.aaguid,
|
|
13820
14635
|
label: params.label ?? null
|
|
13821
14636
|
});
|
|
13822
|
-
|
|
14637
|
+
onAfterCommit7(() => passkeyEnrolledEvent.emit({
|
|
13823
14638
|
userId: String(params.userId),
|
|
13824
14639
|
passkeyId: String(row.id),
|
|
13825
14640
|
label: row.label ?? void 0
|
|
@@ -13919,7 +14734,7 @@ async function startSession(user, params) {
|
|
|
13919
14734
|
...loginBindingFields(registeredBinding(registered))
|
|
13920
14735
|
};
|
|
13921
14736
|
const mfaEnrolled = await mfaEnrolledForUser(user.id);
|
|
13922
|
-
|
|
14737
|
+
onAfterCommit7(() => authLoginEvent.emit({
|
|
13923
14738
|
userId: String(user.id),
|
|
13924
14739
|
provider: "passkey",
|
|
13925
14740
|
email: result.email,
|
|
@@ -14002,7 +14817,7 @@ async function revokePasskeyService(params) {
|
|
|
14002
14817
|
if (!revoked) {
|
|
14003
14818
|
throw new PasskeyNotFoundError2();
|
|
14004
14819
|
}
|
|
14005
|
-
|
|
14820
|
+
onAfterCommit7(() => passkeyRevokedEvent.emit({
|
|
14006
14821
|
userId: String(params.userId),
|
|
14007
14822
|
passkeyId: String(revoked.id),
|
|
14008
14823
|
reason: "user"
|
|
@@ -14085,51 +14900,51 @@ async function initializeAuth(options = {}) {
|
|
|
14085
14900
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
14086
14901
|
}
|
|
14087
14902
|
async function syncRoles(configs, existingByName) {
|
|
14088
|
-
for (const
|
|
14089
|
-
const existing = existingByName.get(
|
|
14903
|
+
for (const config5 of configs) {
|
|
14904
|
+
const existing = existingByName.get(config5.name);
|
|
14090
14905
|
if (!existing) {
|
|
14091
14906
|
await rolesRepository.create({
|
|
14092
|
-
name:
|
|
14093
|
-
displayName:
|
|
14094
|
-
description:
|
|
14095
|
-
priority:
|
|
14096
|
-
isSystem:
|
|
14097
|
-
isBuiltin:
|
|
14907
|
+
name: config5.name,
|
|
14908
|
+
displayName: config5.displayName,
|
|
14909
|
+
description: config5.description || null,
|
|
14910
|
+
priority: config5.priority ?? 10,
|
|
14911
|
+
isSystem: config5.isSystem ?? false,
|
|
14912
|
+
isBuiltin: config5.isBuiltin ?? false,
|
|
14098
14913
|
isActive: true
|
|
14099
14914
|
});
|
|
14100
|
-
authLogger.service.info(` \u2705 Created role: ${
|
|
14915
|
+
authLogger.service.info(` \u2705 Created role: ${config5.name}`);
|
|
14101
14916
|
} else {
|
|
14102
14917
|
const updateData = {
|
|
14103
|
-
displayName:
|
|
14104
|
-
description:
|
|
14918
|
+
displayName: config5.displayName,
|
|
14919
|
+
description: config5.description || null
|
|
14105
14920
|
};
|
|
14106
14921
|
if (!existing.isBuiltin) {
|
|
14107
|
-
updateData.priority =
|
|
14922
|
+
updateData.priority = config5.priority ?? existing.priority;
|
|
14108
14923
|
}
|
|
14109
14924
|
await rolesRepository.updateById(existing.id, updateData);
|
|
14110
14925
|
}
|
|
14111
14926
|
}
|
|
14112
14927
|
}
|
|
14113
14928
|
async function syncPermissions(configs, existingByName) {
|
|
14114
|
-
for (const
|
|
14115
|
-
const existing = existingByName.get(
|
|
14929
|
+
for (const config5 of configs) {
|
|
14930
|
+
const existing = existingByName.get(config5.name);
|
|
14116
14931
|
if (!existing) {
|
|
14117
14932
|
await permissionsRepository.create({
|
|
14118
|
-
name:
|
|
14119
|
-
displayName:
|
|
14120
|
-
description:
|
|
14121
|
-
category:
|
|
14122
|
-
isSystem:
|
|
14123
|
-
isBuiltin:
|
|
14933
|
+
name: config5.name,
|
|
14934
|
+
displayName: config5.displayName,
|
|
14935
|
+
description: config5.description || null,
|
|
14936
|
+
category: config5.category || null,
|
|
14937
|
+
isSystem: config5.isSystem ?? false,
|
|
14938
|
+
isBuiltin: config5.isBuiltin ?? false,
|
|
14124
14939
|
isActive: true,
|
|
14125
14940
|
metadata: null
|
|
14126
14941
|
});
|
|
14127
|
-
authLogger.service.info(` \u2705 Created permission: ${
|
|
14942
|
+
authLogger.service.info(` \u2705 Created permission: ${config5.name}`);
|
|
14128
14943
|
} else {
|
|
14129
14944
|
await permissionsRepository.updateById(existing.id, {
|
|
14130
|
-
displayName:
|
|
14131
|
-
description:
|
|
14132
|
-
category:
|
|
14945
|
+
displayName: config5.displayName,
|
|
14946
|
+
description: config5.description || null,
|
|
14947
|
+
category: config5.category || null
|
|
14133
14948
|
});
|
|
14134
14949
|
}
|
|
14135
14950
|
}
|
|
@@ -14191,7 +15006,7 @@ async function getUserPermissions(userId) {
|
|
|
14191
15006
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
14192
15007
|
if (permIds.length > 0) {
|
|
14193
15008
|
const rolePerms = await Promise.all(
|
|
14194
|
-
permIds.map((
|
|
15009
|
+
permIds.map((id27) => permissionsRepository.findById(id27))
|
|
14195
15010
|
);
|
|
14196
15011
|
for (const perm of rolePerms) {
|
|
14197
15012
|
if (perm && perm.isActive) {
|
|
@@ -14409,20 +15224,20 @@ async function acceptInvitation(params) {
|
|
|
14409
15224
|
async function listInvitations(params) {
|
|
14410
15225
|
return await invitationsRepository.list(params);
|
|
14411
15226
|
}
|
|
14412
|
-
async function cancelInvitation(
|
|
14413
|
-
const invitation = await invitationsRepository.findById(
|
|
15227
|
+
async function cancelInvitation(id27, cancelledBy, reason) {
|
|
15228
|
+
const invitation = await invitationsRepository.findById(id27);
|
|
14414
15229
|
if (!invitation) {
|
|
14415
15230
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
14416
15231
|
}
|
|
14417
15232
|
if (invitation.status !== "pending") {
|
|
14418
15233
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
14419
15234
|
}
|
|
14420
|
-
await invitationsRepository.cancel(
|
|
15235
|
+
await invitationsRepository.cancel(id27, cancelledBy, reason, invitation.metadata);
|
|
14421
15236
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
14422
15237
|
}
|
|
14423
|
-
async function deleteInvitation(
|
|
14424
|
-
await invitationsRepository.deleteById(
|
|
14425
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
15238
|
+
async function deleteInvitation(id27) {
|
|
15239
|
+
await invitationsRepository.deleteById(id27);
|
|
15240
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id27}`);
|
|
14426
15241
|
}
|
|
14427
15242
|
async function expireOldInvitations() {
|
|
14428
15243
|
const count2 = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -14431,8 +15246,8 @@ async function expireOldInvitations() {
|
|
|
14431
15246
|
}
|
|
14432
15247
|
return count2;
|
|
14433
15248
|
}
|
|
14434
|
-
async function resendInvitation(
|
|
14435
|
-
const invitation = await invitationsRepository.findById(
|
|
15249
|
+
async function resendInvitation(id27, expiresInDays = 7) {
|
|
15250
|
+
const invitation = await invitationsRepository.findById(id27);
|
|
14436
15251
|
if (!invitation) {
|
|
14437
15252
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
14438
15253
|
}
|
|
@@ -14440,7 +15255,7 @@ async function resendInvitation(id26, expiresInDays = 7) {
|
|
|
14440
15255
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
14441
15256
|
}
|
|
14442
15257
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
14443
|
-
const updated = await invitationsRepository.resend(
|
|
15258
|
+
const updated = await invitationsRepository.resend(id27, newExpiresAt);
|
|
14444
15259
|
if (!updated) {
|
|
14445
15260
|
throw new Error("Failed to update invitation");
|
|
14446
15261
|
}
|
|
@@ -14479,13 +15294,13 @@ async function getAuthSessionService(userId) {
|
|
|
14479
15294
|
// src/server/lib/one-time-token.ts
|
|
14480
15295
|
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
14481
15296
|
var manager = null;
|
|
14482
|
-
function initOneTimeTokenManager(
|
|
15297
|
+
function initOneTimeTokenManager(config5) {
|
|
14483
15298
|
if (manager) {
|
|
14484
15299
|
manager.destroy();
|
|
14485
15300
|
}
|
|
14486
15301
|
manager = new SSETokenManager({
|
|
14487
|
-
ttl:
|
|
14488
|
-
store:
|
|
15302
|
+
ttl: config5?.ttl,
|
|
15303
|
+
store: config5?.store
|
|
14489
15304
|
});
|
|
14490
15305
|
}
|
|
14491
15306
|
function getOneTimeTokenManager() {
|
|
@@ -14635,10 +15450,10 @@ function getDefaultScopes() {
|
|
|
14635
15450
|
}
|
|
14636
15451
|
function getGoogleAuthUrl(state, scopes) {
|
|
14637
15452
|
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
14638
|
-
const
|
|
15453
|
+
const config5 = getGoogleOAuthConfig();
|
|
14639
15454
|
const params = new URLSearchParams({
|
|
14640
|
-
client_id:
|
|
14641
|
-
redirect_uri:
|
|
15455
|
+
client_id: config5.clientId,
|
|
15456
|
+
redirect_uri: config5.redirectUri,
|
|
14642
15457
|
response_type: "code",
|
|
14643
15458
|
scope: resolvedScopes.join(" "),
|
|
14644
15459
|
state,
|
|
@@ -14650,16 +15465,16 @@ function getGoogleAuthUrl(state, scopes) {
|
|
|
14650
15465
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
14651
15466
|
}
|
|
14652
15467
|
async function exchangeCodeForTokens(code) {
|
|
14653
|
-
const
|
|
15468
|
+
const config5 = getGoogleOAuthConfig();
|
|
14654
15469
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
14655
15470
|
method: "POST",
|
|
14656
15471
|
headers: {
|
|
14657
15472
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
14658
15473
|
},
|
|
14659
15474
|
body: new URLSearchParams({
|
|
14660
|
-
client_id:
|
|
14661
|
-
client_secret:
|
|
14662
|
-
redirect_uri:
|
|
15475
|
+
client_id: config5.clientId,
|
|
15476
|
+
client_secret: config5.clientSecret,
|
|
15477
|
+
redirect_uri: config5.redirectUri,
|
|
14663
15478
|
grant_type: "authorization_code",
|
|
14664
15479
|
code
|
|
14665
15480
|
})
|
|
@@ -14683,15 +15498,15 @@ async function getGoogleUserInfo(accessToken) {
|
|
|
14683
15498
|
return response.json();
|
|
14684
15499
|
}
|
|
14685
15500
|
async function refreshAccessToken(refreshToken) {
|
|
14686
|
-
const
|
|
15501
|
+
const config5 = getGoogleOAuthConfig();
|
|
14687
15502
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
14688
15503
|
method: "POST",
|
|
14689
15504
|
headers: {
|
|
14690
15505
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
14691
15506
|
},
|
|
14692
15507
|
body: new URLSearchParams({
|
|
14693
|
-
client_id:
|
|
14694
|
-
client_secret:
|
|
15508
|
+
client_id: config5.clientId,
|
|
15509
|
+
client_secret: config5.clientSecret,
|
|
14695
15510
|
refresh_token: refreshToken,
|
|
14696
15511
|
grant_type: "refresh_token"
|
|
14697
15512
|
})
|
|
@@ -14756,8 +15571,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
14756
15571
|
function registerOAuthProvider(provider) {
|
|
14757
15572
|
registry2.set(provider.id, provider);
|
|
14758
15573
|
}
|
|
14759
|
-
function getOAuthProvider(
|
|
14760
|
-
return registry2.get(
|
|
15574
|
+
function getOAuthProvider(id27) {
|
|
15575
|
+
return registry2.get(id27);
|
|
14761
15576
|
}
|
|
14762
15577
|
function getRegisteredProviders() {
|
|
14763
15578
|
return [...registry2.values()];
|
|
@@ -15012,21 +15827,21 @@ var githubProvider = {
|
|
|
15012
15827
|
return !!(env4.SPFN_AUTH_GITHUB_CLIENT_ID && env4.SPFN_AUTH_GITHUB_CLIENT_SECRET);
|
|
15013
15828
|
},
|
|
15014
15829
|
getAuthUrl(state, scopes) {
|
|
15015
|
-
const
|
|
15830
|
+
const config5 = getGithubConfig();
|
|
15016
15831
|
const params = new URLSearchParams({
|
|
15017
|
-
client_id:
|
|
15018
|
-
redirect_uri:
|
|
15832
|
+
client_id: config5.clientId,
|
|
15833
|
+
redirect_uri: config5.redirectUri,
|
|
15019
15834
|
state,
|
|
15020
15835
|
scope: (scopes ?? getGithubScopes()).join(" ")
|
|
15021
15836
|
});
|
|
15022
15837
|
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
15023
15838
|
},
|
|
15024
15839
|
async exchangeCodeForTokens(code) {
|
|
15025
|
-
const
|
|
15840
|
+
const config5 = getGithubConfig();
|
|
15026
15841
|
return requestGithubTokens(new URLSearchParams({
|
|
15027
|
-
client_id:
|
|
15028
|
-
client_secret:
|
|
15029
|
-
redirect_uri:
|
|
15842
|
+
client_id: config5.clientId,
|
|
15843
|
+
client_secret: config5.clientSecret,
|
|
15844
|
+
redirect_uri: config5.redirectUri,
|
|
15030
15845
|
code
|
|
15031
15846
|
}));
|
|
15032
15847
|
},
|
|
@@ -15056,11 +15871,11 @@ var githubProvider = {
|
|
|
15056
15871
|
};
|
|
15057
15872
|
},
|
|
15058
15873
|
async refreshTokens(refreshToken) {
|
|
15059
|
-
const
|
|
15874
|
+
const config5 = getGithubConfig();
|
|
15060
15875
|
return requestGithubTokens(new URLSearchParams({
|
|
15061
15876
|
grant_type: "refresh_token",
|
|
15062
|
-
client_id:
|
|
15063
|
-
client_secret:
|
|
15877
|
+
client_id: config5.clientId,
|
|
15878
|
+
client_secret: config5.clientSecret,
|
|
15064
15879
|
refresh_token: refreshToken
|
|
15065
15880
|
}));
|
|
15066
15881
|
}
|
|
@@ -15184,26 +15999,26 @@ var kakaoProvider = {
|
|
|
15184
15999
|
return !!env4.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
15185
16000
|
},
|
|
15186
16001
|
getAuthUrl(state, scopes) {
|
|
15187
|
-
const
|
|
16002
|
+
const config5 = getKakaoConfig();
|
|
15188
16003
|
const params = new URLSearchParams({
|
|
15189
16004
|
response_type: "code",
|
|
15190
|
-
client_id:
|
|
15191
|
-
redirect_uri:
|
|
16005
|
+
client_id: config5.clientId,
|
|
16006
|
+
redirect_uri: config5.redirectUri,
|
|
15192
16007
|
state,
|
|
15193
16008
|
scope: (scopes ?? getKakaoScopes()).join(",")
|
|
15194
16009
|
});
|
|
15195
16010
|
return `${KAKAO_AUTH_URL}?${params.toString()}`;
|
|
15196
16011
|
},
|
|
15197
16012
|
async exchangeCodeForTokens(code) {
|
|
15198
|
-
const
|
|
16013
|
+
const config5 = getKakaoConfig();
|
|
15199
16014
|
const params = new URLSearchParams({
|
|
15200
16015
|
grant_type: "authorization_code",
|
|
15201
|
-
client_id:
|
|
15202
|
-
redirect_uri:
|
|
16016
|
+
client_id: config5.clientId,
|
|
16017
|
+
redirect_uri: config5.redirectUri,
|
|
15203
16018
|
code
|
|
15204
16019
|
});
|
|
15205
|
-
if (
|
|
15206
|
-
params.set("client_secret",
|
|
16020
|
+
if (config5.clientSecret) {
|
|
16021
|
+
params.set("client_secret", config5.clientSecret);
|
|
15207
16022
|
}
|
|
15208
16023
|
return requestKakaoTokens(params);
|
|
15209
16024
|
},
|
|
@@ -15245,14 +16060,14 @@ var kakaoProvider = {
|
|
|
15245
16060
|
return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
|
|
15246
16061
|
},
|
|
15247
16062
|
async refreshTokens(refreshToken) {
|
|
15248
|
-
const
|
|
16063
|
+
const config5 = getKakaoConfig();
|
|
15249
16064
|
const params = new URLSearchParams({
|
|
15250
16065
|
grant_type: "refresh_token",
|
|
15251
|
-
client_id:
|
|
16066
|
+
client_id: config5.clientId,
|
|
15252
16067
|
refresh_token: refreshToken
|
|
15253
16068
|
});
|
|
15254
|
-
if (
|
|
15255
|
-
params.set("client_secret",
|
|
16069
|
+
if (config5.clientSecret) {
|
|
16070
|
+
params.set("client_secret", config5.clientSecret);
|
|
15256
16071
|
}
|
|
15257
16072
|
return requestKakaoTokens(params);
|
|
15258
16073
|
},
|
|
@@ -15411,22 +16226,22 @@ var naverProvider = {
|
|
|
15411
16226
|
return !!(env4.SPFN_AUTH_NAVER_CLIENT_ID && env4.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
15412
16227
|
},
|
|
15413
16228
|
getAuthUrl(state) {
|
|
15414
|
-
const
|
|
16229
|
+
const config5 = getNaverConfig();
|
|
15415
16230
|
const params = new URLSearchParams({
|
|
15416
16231
|
response_type: "code",
|
|
15417
|
-
client_id:
|
|
15418
|
-
redirect_uri:
|
|
16232
|
+
client_id: config5.clientId,
|
|
16233
|
+
redirect_uri: config5.redirectUri,
|
|
15419
16234
|
state
|
|
15420
16235
|
});
|
|
15421
16236
|
return `${NAVER_AUTH_URL}?${params.toString()}`;
|
|
15422
16237
|
},
|
|
15423
16238
|
async exchangeCodeForTokens(code, options) {
|
|
15424
|
-
const
|
|
16239
|
+
const config5 = getNaverConfig();
|
|
15425
16240
|
return requestNaverTokens(new URLSearchParams({
|
|
15426
16241
|
grant_type: "authorization_code",
|
|
15427
|
-
client_id:
|
|
15428
|
-
client_secret:
|
|
15429
|
-
redirect_uri:
|
|
16242
|
+
client_id: config5.clientId,
|
|
16243
|
+
client_secret: config5.clientSecret,
|
|
16244
|
+
redirect_uri: config5.redirectUri,
|
|
15430
16245
|
code,
|
|
15431
16246
|
state: options.state
|
|
15432
16247
|
}));
|
|
@@ -15467,11 +16282,11 @@ var naverProvider = {
|
|
|
15467
16282
|
return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
|
|
15468
16283
|
},
|
|
15469
16284
|
async refreshTokens(refreshToken) {
|
|
15470
|
-
const
|
|
16285
|
+
const config5 = getNaverConfig();
|
|
15471
16286
|
return requestNaverTokens(new URLSearchParams({
|
|
15472
16287
|
grant_type: "refresh_token",
|
|
15473
|
-
client_id:
|
|
15474
|
-
client_secret:
|
|
16288
|
+
client_id: config5.clientId,
|
|
16289
|
+
client_secret: config5.clientSecret,
|
|
15475
16290
|
refresh_token: refreshToken
|
|
15476
16291
|
}));
|
|
15477
16292
|
},
|
|
@@ -15830,9 +16645,9 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
15830
16645
|
}
|
|
15831
16646
|
|
|
15832
16647
|
// src/server/services/oauth-native.service.ts
|
|
15833
|
-
import { runInTransaction as runInTransaction5, onAfterCommit as
|
|
16648
|
+
import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
|
|
15834
16649
|
import {
|
|
15835
|
-
InvalidKeyFingerprintError as
|
|
16650
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError4,
|
|
15836
16651
|
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
15837
16652
|
NonceKeyBindingError
|
|
15838
16653
|
} from "@spfn/auth/errors";
|
|
@@ -15859,7 +16674,7 @@ function assertNonceBindsPublicKey(params) {
|
|
|
15859
16674
|
throw new NonceKeyBindingError();
|
|
15860
16675
|
}
|
|
15861
16676
|
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
15862
|
-
throw new
|
|
16677
|
+
throw new InvalidKeyFingerprintError4();
|
|
15863
16678
|
}
|
|
15864
16679
|
}
|
|
15865
16680
|
async function persistNativeLogin(identity, params) {
|
|
@@ -15903,7 +16718,7 @@ async function persistNativeLogin(identity, params) {
|
|
|
15903
16718
|
}
|
|
15904
16719
|
await updateLastLoginService(userId);
|
|
15905
16720
|
const mfaEnrolled = isNewUser ? false : await mfaEnrolledForUser(userId);
|
|
15906
|
-
|
|
16721
|
+
onAfterCommit8(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
|
|
15907
16722
|
return { mfaRequired: false, userId: String(userId), keyId: params.keyId, isNewUser };
|
|
15908
16723
|
}, { context: "auth:oauth-native" });
|
|
15909
16724
|
}
|
|
@@ -15953,8 +16768,8 @@ async function verifyOpsTokenService(token) {
|
|
|
15953
16768
|
scopes: record.scopes
|
|
15954
16769
|
};
|
|
15955
16770
|
}
|
|
15956
|
-
async function revokeOpsTokenService(
|
|
15957
|
-
return await opsTokensRepository.revokeById(
|
|
16771
|
+
async function revokeOpsTokenService(id27) {
|
|
16772
|
+
return await opsTokensRepository.revokeById(id27);
|
|
15958
16773
|
}
|
|
15959
16774
|
async function listOpsTokensService() {
|
|
15960
16775
|
return await opsTokensRepository.list();
|
|
@@ -15970,7 +16785,7 @@ var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
|
15970
16785
|
var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
15971
16786
|
var DEFAULT_CODE_TTL_MS = 60 * 1e3;
|
|
15972
16787
|
var AUTHORIZE_PATH = "/oauth/authorize";
|
|
15973
|
-
var
|
|
16788
|
+
var config4 = null;
|
|
15974
16789
|
function resolveIssuerSource(env21 = process.env) {
|
|
15975
16790
|
return { value: env21.SPFN_API_URL, variable: "SPFN_API_URL" };
|
|
15976
16791
|
}
|
|
@@ -15980,7 +16795,7 @@ function resolveAuthorizeUrl(env21) {
|
|
|
15980
16795
|
}
|
|
15981
16796
|
function configureAuthorizationServer(options, env21 = process.env) {
|
|
15982
16797
|
if (!options) {
|
|
15983
|
-
|
|
16798
|
+
config4 = null;
|
|
15984
16799
|
return;
|
|
15985
16800
|
}
|
|
15986
16801
|
const scopeNames = Object.keys(options.scopes ?? {});
|
|
@@ -15989,7 +16804,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
|
|
|
15989
16804
|
"authorizationServer.scopes must name at least one scope. The names are published in /.well-known/oauth-authorization-server and shown on the consent screen, so there is nothing to derive them from."
|
|
15990
16805
|
);
|
|
15991
16806
|
}
|
|
15992
|
-
|
|
16807
|
+
config4 = {
|
|
15993
16808
|
issuer: canonicalIssuer(options.issuer ?? resolveIssuerSource(env21).value ?? ""),
|
|
15994
16809
|
issuerSource: options.issuer ? "authorizationServer.issuer" : resolveIssuerSource(env21).variable,
|
|
15995
16810
|
authorizeUrl: options.authorizeUrl ?? resolveAuthorizeUrl(env21),
|
|
@@ -16000,7 +16815,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
|
|
|
16000
16815
|
refreshTokenTtlMs: options.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
16001
16816
|
codeTtlMs: options.codeTtlMs ?? DEFAULT_CODE_TTL_MS
|
|
16002
16817
|
};
|
|
16003
|
-
assertKnownDefaultScopes(
|
|
16818
|
+
assertKnownDefaultScopes(config4);
|
|
16004
16819
|
}
|
|
16005
16820
|
function assertKnownDefaultScopes(resolved) {
|
|
16006
16821
|
const unknown = resolved.defaultScopes.filter((scope) => !(scope in resolved.scopes));
|
|
@@ -16011,7 +16826,7 @@ function assertKnownDefaultScopes(resolved) {
|
|
|
16011
16826
|
}
|
|
16012
16827
|
}
|
|
16013
16828
|
function getAuthorizationServerConfig() {
|
|
16014
|
-
return
|
|
16829
|
+
return config4;
|
|
16015
16830
|
}
|
|
16016
16831
|
function isLoopbackHostname(hostname) {
|
|
16017
16832
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
@@ -16234,15 +17049,15 @@ var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
|
16234
17049
|
var SUPPORTED_AUTH_METHOD = "none";
|
|
16235
17050
|
var SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
|
|
16236
17051
|
var SUPPORTED_RESPONSE_TYPES = ["code"];
|
|
16237
|
-
function
|
|
17052
|
+
function refuse2(status, error, description) {
|
|
16238
17053
|
return { ok: false, status, error, description };
|
|
16239
17054
|
}
|
|
16240
17055
|
function requireConfig() {
|
|
16241
|
-
const
|
|
16242
|
-
if (!
|
|
17056
|
+
const config5 = getAuthorizationServerConfig();
|
|
17057
|
+
if (!config5) {
|
|
16243
17058
|
throw new Error("OAuth2 client service called with no authorization server configured.");
|
|
16244
17059
|
}
|
|
16245
|
-
return
|
|
17060
|
+
return config5;
|
|
16246
17061
|
}
|
|
16247
17062
|
function asStringArray(value) {
|
|
16248
17063
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
@@ -16253,7 +17068,7 @@ function asStringArray(value) {
|
|
|
16253
17068
|
function refuseDeclaredMetadata(request) {
|
|
16254
17069
|
const { token_endpoint_auth_method: authMethod } = request;
|
|
16255
17070
|
if (authMethod !== void 0 && authMethod !== SUPPORTED_AUTH_METHOD) {
|
|
16256
|
-
return
|
|
17071
|
+
return refuse2(
|
|
16257
17072
|
400,
|
|
16258
17073
|
"invalid_client_metadata",
|
|
16259
17074
|
`token_endpoint_auth_method must be "${SUPPORTED_AUTH_METHOD}". This authorization server registers public clients only \u2014 a client on a user's own machine cannot keep a secret.`
|
|
@@ -16267,7 +17082,7 @@ function refuseListedMetadata(field, value, supported) {
|
|
|
16267
17082
|
}
|
|
16268
17083
|
const declared = asStringArray(value);
|
|
16269
17084
|
if (!declared || declared.length === 0 || declared.some((entry) => !supported.includes(entry))) {
|
|
16270
|
-
return
|
|
17085
|
+
return refuse2(
|
|
16271
17086
|
400,
|
|
16272
17087
|
"invalid_client_metadata",
|
|
16273
17088
|
`${field} must be a non-empty subset of ${supported.join(", ")}.`
|
|
@@ -16278,7 +17093,7 @@ function refuseListedMetadata(field, value, supported) {
|
|
|
16278
17093
|
function refuseRedirectUris(uris, allowedRedirectOrigins) {
|
|
16279
17094
|
const declared = asStringArray(uris);
|
|
16280
17095
|
if (!declared || declared.length === 0) {
|
|
16281
|
-
return
|
|
17096
|
+
return refuse2(
|
|
16282
17097
|
400,
|
|
16283
17098
|
"invalid_client_metadata",
|
|
16284
17099
|
"redirect_uris must list at least one absolute URI. There is nowhere to send an authorization code without one."
|
|
@@ -16287,20 +17102,20 @@ function refuseRedirectUris(uris, allowedRedirectOrigins) {
|
|
|
16287
17102
|
for (const uri of declared) {
|
|
16288
17103
|
const detail = refuseRedirectUriRegistration(uri, allowedRedirectOrigins);
|
|
16289
17104
|
if (detail) {
|
|
16290
|
-
return
|
|
17105
|
+
return refuse2(400, "invalid_redirect_uri", detail);
|
|
16291
17106
|
}
|
|
16292
17107
|
}
|
|
16293
17108
|
return null;
|
|
16294
17109
|
}
|
|
16295
17110
|
async function registerOAuth2ClientService(request, clientIp) {
|
|
16296
|
-
const
|
|
16297
|
-
const refusal = refuseRedirectUris(request.redirect_uris,
|
|
17111
|
+
const config5 = requireConfig();
|
|
17112
|
+
const refusal = refuseRedirectUris(request.redirect_uris, config5.allowedRedirectOrigins) ?? refuseDeclaredMetadata(request);
|
|
16298
17113
|
if (refusal) {
|
|
16299
17114
|
return refusal;
|
|
16300
17115
|
}
|
|
16301
17116
|
const record = await createUnderStandingCap(newClientRow(request, clientIp), clientIp);
|
|
16302
17117
|
if (!record) {
|
|
16303
|
-
return
|
|
17118
|
+
return refuse2(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
|
|
16304
17119
|
}
|
|
16305
17120
|
authLogger.service.info("OAuth2 client registered", { clientName: record.clientName });
|
|
16306
17121
|
return { ok: true, client: describeClient(record) };
|
|
@@ -16379,11 +17194,11 @@ function sameResource(presented, granted) {
|
|
|
16379
17194
|
|
|
16380
17195
|
// src/server/services/oauth2-authorize.service.ts
|
|
16381
17196
|
function requireConfig2() {
|
|
16382
|
-
const
|
|
16383
|
-
if (!
|
|
17197
|
+
const config5 = getAuthorizationServerConfig();
|
|
17198
|
+
if (!config5) {
|
|
16384
17199
|
throw new Error("OAuth2 authorize service called with no authorization server configured.");
|
|
16385
17200
|
}
|
|
16386
|
-
return
|
|
17201
|
+
return config5;
|
|
16387
17202
|
}
|
|
16388
17203
|
async function resolveRedirectTarget(params) {
|
|
16389
17204
|
const client = await oauth2ClientsRepository.findByClientId(params.clientId);
|
|
@@ -16395,10 +17210,10 @@ async function resolveRedirectTarget(params) {
|
|
|
16395
17210
|
}
|
|
16396
17211
|
return { client, redirectUri: params.redirectUri };
|
|
16397
17212
|
}
|
|
16398
|
-
function resolveScopes(params,
|
|
17213
|
+
function resolveScopes(params, config5) {
|
|
16399
17214
|
const requested = params.scope?.trim();
|
|
16400
17215
|
if (!requested) {
|
|
16401
|
-
return
|
|
17216
|
+
return config5.defaultScopes;
|
|
16402
17217
|
}
|
|
16403
17218
|
return requested.split(/\s+/);
|
|
16404
17219
|
}
|
|
@@ -16410,7 +17225,7 @@ function refuseRedirectable(params, redirectUri, error, message) {
|
|
|
16410
17225
|
function hasUsableS256Challenge(params) {
|
|
16411
17226
|
return params.codeChallengeMethod === "S256" && !!params.codeChallenge && isPkceS256ChallengeShaped(params.codeChallenge);
|
|
16412
17227
|
}
|
|
16413
|
-
function assertRedirectableRules(params, redirectUri,
|
|
17228
|
+
function assertRedirectableRules(params, redirectUri, config5) {
|
|
16414
17229
|
if (!hasUsableS256Challenge(params)) {
|
|
16415
17230
|
refuseRedirectable(params, redirectUri, "invalid_request", PKCE_REQUIRED_MESSAGE);
|
|
16416
17231
|
}
|
|
@@ -16418,31 +17233,31 @@ function assertRedirectableRules(params, redirectUri, config4) {
|
|
|
16418
17233
|
if (!resource) {
|
|
16419
17234
|
refuseRedirectable(params, redirectUri, "invalid_target", RESOURCE_REQUIRED_MESSAGE);
|
|
16420
17235
|
}
|
|
16421
|
-
const scopes = resolveScopes(params,
|
|
16422
|
-
const unknown = scopes.filter((scope) => !(scope in
|
|
17236
|
+
const scopes = resolveScopes(params, config5);
|
|
17237
|
+
const unknown = scopes.filter((scope) => !(scope in config5.scopes));
|
|
16423
17238
|
if (unknown.length > 0) {
|
|
16424
17239
|
refuseRedirectable(params, redirectUri, "invalid_scope", `Unknown scope: ${unknown.join(", ")}.`);
|
|
16425
17240
|
}
|
|
16426
17241
|
return { resource, scopes, codeChallenge: params.codeChallenge };
|
|
16427
17242
|
}
|
|
16428
17243
|
async function validate(params) {
|
|
16429
|
-
const
|
|
17244
|
+
const config5 = requireConfig2();
|
|
16430
17245
|
const { client, redirectUri } = await resolveRedirectTarget(params);
|
|
16431
|
-
const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri,
|
|
17246
|
+
const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config5);
|
|
16432
17247
|
return { client, redirectUri, resource, scopes, codeChallenge, state: params.state };
|
|
16433
17248
|
}
|
|
16434
17249
|
async function describeOAuth2AuthorizeRequestService(params) {
|
|
16435
|
-
const
|
|
17250
|
+
const config5 = requireConfig2();
|
|
16436
17251
|
const validated = await validate(params);
|
|
16437
17252
|
return {
|
|
16438
17253
|
clientName: validated.client.clientName,
|
|
16439
17254
|
redirectHost: redirectHostOf(validated.redirectUri),
|
|
16440
|
-
scopes: validated.scopes.map((name) => ({ name, description:
|
|
17255
|
+
scopes: validated.scopes.map((name) => ({ name, description: config5.scopes[name] })),
|
|
16441
17256
|
resource: validated.resource
|
|
16442
17257
|
};
|
|
16443
17258
|
}
|
|
16444
17259
|
async function approveOAuth2AuthorizeService(params, userId) {
|
|
16445
|
-
const
|
|
17260
|
+
const config5 = requireConfig2();
|
|
16446
17261
|
const validated = await validate(params);
|
|
16447
17262
|
const grant = await oauth2GrantsRepository.upsert({
|
|
16448
17263
|
client: validated.client.id,
|
|
@@ -16456,7 +17271,7 @@ async function approveOAuth2AuthorizeService(params, userId) {
|
|
|
16456
17271
|
grant: grant.id,
|
|
16457
17272
|
redirectUri: validated.redirectUri,
|
|
16458
17273
|
codeChallenge: validated.codeChallenge,
|
|
16459
|
-
expiresAt: new Date(Date.now() +
|
|
17274
|
+
expiresAt: new Date(Date.now() + config5.codeTtlMs)
|
|
16460
17275
|
});
|
|
16461
17276
|
return { code, redirectUri: validated.redirectUri, state: validated.state };
|
|
16462
17277
|
}
|
|
@@ -16484,15 +17299,15 @@ function invalidGrant() {
|
|
|
16484
17299
|
description: "The authorization code or refresh token is invalid, expired, already used, or was issued to another client."
|
|
16485
17300
|
};
|
|
16486
17301
|
}
|
|
16487
|
-
function
|
|
17302
|
+
function refuse3(error, description) {
|
|
16488
17303
|
return { ok: false, error, description };
|
|
16489
17304
|
}
|
|
16490
17305
|
function requireConfig3() {
|
|
16491
|
-
const
|
|
16492
|
-
if (!
|
|
17306
|
+
const config5 = getAuthorizationServerConfig();
|
|
17307
|
+
if (!config5) {
|
|
16493
17308
|
throw new Error("OAuth2 token service called with no authorization server configured.");
|
|
16494
17309
|
}
|
|
16495
|
-
return
|
|
17310
|
+
return config5;
|
|
16496
17311
|
}
|
|
16497
17312
|
async function oauth2TokenService(request) {
|
|
16498
17313
|
if (request.grant_type === "authorization_code") {
|
|
@@ -16501,14 +17316,14 @@ async function oauth2TokenService(request) {
|
|
|
16501
17316
|
if (request.grant_type === "refresh_token") {
|
|
16502
17317
|
return await refreshTokens(request);
|
|
16503
17318
|
}
|
|
16504
|
-
return
|
|
17319
|
+
return refuse3(
|
|
16505
17320
|
"unsupported_grant_type",
|
|
16506
17321
|
"grant_type must be authorization_code or refresh_token."
|
|
16507
17322
|
);
|
|
16508
17323
|
}
|
|
16509
17324
|
async function exchangeAuthorizationCode(request) {
|
|
16510
17325
|
if (!request.code || !request.code_verifier || !request.client_id || !request.redirect_uri) {
|
|
16511
|
-
return
|
|
17326
|
+
return refuse3(
|
|
16512
17327
|
"invalid_request",
|
|
16513
17328
|
"authorization_code requires code, code_verifier, client_id and redirect_uri."
|
|
16514
17329
|
);
|
|
@@ -16542,13 +17357,13 @@ async function spendBoundCode(request, record, pair) {
|
|
|
16542
17357
|
return invalidGrant();
|
|
16543
17358
|
}
|
|
16544
17359
|
if (resolveResource(request.resource, pair.grant) === null) {
|
|
16545
|
-
return
|
|
17360
|
+
return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
16546
17361
|
}
|
|
16547
17362
|
return { ok: true, tokens: await issueTokenPair(pair.grant, pair.grant.scopes) };
|
|
16548
17363
|
}
|
|
16549
17364
|
async function refreshTokens(request) {
|
|
16550
17365
|
if (!request.refresh_token || !request.client_id) {
|
|
16551
|
-
return
|
|
17366
|
+
return refuse3("invalid_request", "refresh_token requires refresh_token and client_id.");
|
|
16552
17367
|
}
|
|
16553
17368
|
const tokenHash = hashOAuth2Secret(request.refresh_token);
|
|
16554
17369
|
const presented = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
@@ -16570,11 +17385,11 @@ async function rotateRefresh(request, tokenHash, grantId, presentedScopes) {
|
|
|
16570
17385
|
return invalidGrant();
|
|
16571
17386
|
}
|
|
16572
17387
|
if (resolveResource(request.resource, pair.grant) === null) {
|
|
16573
|
-
return
|
|
17388
|
+
return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
16574
17389
|
}
|
|
16575
17390
|
const scopes = resolveRefreshScopes(request.scope, pair.grant, presentedScopes);
|
|
16576
17391
|
if (!scopes) {
|
|
16577
|
-
return
|
|
17392
|
+
return refuse3("invalid_scope", "A refresh may ask for a subset of the granted scopes, never more.");
|
|
16578
17393
|
}
|
|
16579
17394
|
if (!await oauth2TokensRepository.rotate(tokenHash)) {
|
|
16580
17395
|
return await refuseLostRotation(tokenHash, grantId);
|
|
@@ -16603,10 +17418,10 @@ function resolveResource(requested, grant) {
|
|
|
16603
17418
|
return sameResource(requested, grant.resource) ? grant.resource : null;
|
|
16604
17419
|
}
|
|
16605
17420
|
async function issueTokenPair(grant, scopes) {
|
|
16606
|
-
const
|
|
17421
|
+
const config5 = requireConfig3();
|
|
16607
17422
|
const accessToken = generateAccessToken();
|
|
16608
17423
|
const refreshToken = generateRefreshToken();
|
|
16609
|
-
const expiresAt = new Date(Date.now() +
|
|
17424
|
+
const expiresAt = new Date(Date.now() + config5.accessTokenTtlMs);
|
|
16610
17425
|
await runInTransaction6(async () => {
|
|
16611
17426
|
await storeToken(accessToken, "access", grant.id, scopes, expiresAt);
|
|
16612
17427
|
await storeToken(
|
|
@@ -16614,7 +17429,7 @@ async function issueTokenPair(grant, scopes) {
|
|
|
16614
17429
|
"refresh",
|
|
16615
17430
|
grant.id,
|
|
16616
17431
|
scopes,
|
|
16617
|
-
new Date(Date.now() +
|
|
17432
|
+
new Date(Date.now() + config5.refreshTokenTtlMs)
|
|
16618
17433
|
);
|
|
16619
17434
|
});
|
|
16620
17435
|
oauth2ClientsRepository.updateLastUsedById(grant.client).catch((err) => authLogger.service.error("Failed to update OAuth2 client lastUsedAt", err));
|
|
@@ -16937,6 +17752,72 @@ function attestedClientIp(c) {
|
|
|
16937
17752
|
return provenance.webProxy ? provenance.ip ?? null : null;
|
|
16938
17753
|
}
|
|
16939
17754
|
|
|
17755
|
+
// src/server/middleware/device-auth-long-poll.ts
|
|
17756
|
+
var DEVICE_AUTH_WAITED_MILLIS = "deviceAuthWaitedMillis";
|
|
17757
|
+
function deviceAuthLongPoll() {
|
|
17758
|
+
return async (c, next) => {
|
|
17759
|
+
const target = waitTarget(await c.req.json().catch(() => null));
|
|
17760
|
+
if (!target) {
|
|
17761
|
+
return next();
|
|
17762
|
+
}
|
|
17763
|
+
const signal = c.req.raw.signal;
|
|
17764
|
+
const waitedMillis = await waitForDeviceAuthAnswerService({ ...target, signal });
|
|
17765
|
+
if (signal.aborted) {
|
|
17766
|
+
return c.body(null, 204);
|
|
17767
|
+
}
|
|
17768
|
+
c.set(DEVICE_AUTH_WAITED_MILLIS, waitedMillis);
|
|
17769
|
+
return next();
|
|
17770
|
+
};
|
|
17771
|
+
}
|
|
17772
|
+
function waitTarget(body) {
|
|
17773
|
+
const { deviceCode, waitMillis } = body ?? {};
|
|
17774
|
+
if (typeof deviceCode !== "string" || !Number.isInteger(waitMillis) || waitMillis <= 0) {
|
|
17775
|
+
return null;
|
|
17776
|
+
}
|
|
17777
|
+
return { deviceCode, waitMillis };
|
|
17778
|
+
}
|
|
17779
|
+
|
|
17780
|
+
// src/server/middleware/device-link-long-poll.ts
|
|
17781
|
+
var DEVICE_LINK_WAITED_MILLIS = "deviceLinkWaitedMillis";
|
|
17782
|
+
function deviceLinkStatusLongPoll() {
|
|
17783
|
+
return longPoll(async (c, body, waitMillis, signal) => {
|
|
17784
|
+
const auth = getOptionalAuth(c);
|
|
17785
|
+
if (typeof body.linkId !== "string" || !auth) {
|
|
17786
|
+
return 0;
|
|
17787
|
+
}
|
|
17788
|
+
return waitForDeviceLinkStatusService({
|
|
17789
|
+
linkId: body.linkId,
|
|
17790
|
+
issuer: { userId: Number(auth.userId), keyId: auth.keyId },
|
|
17791
|
+
waitMillis,
|
|
17792
|
+
signal
|
|
17793
|
+
});
|
|
17794
|
+
});
|
|
17795
|
+
}
|
|
17796
|
+
function deviceLinkPollLongPoll() {
|
|
17797
|
+
return longPoll(async (_c, body, waitMillis, signal) => {
|
|
17798
|
+
if (typeof body.deviceCode !== "string") {
|
|
17799
|
+
return 0;
|
|
17800
|
+
}
|
|
17801
|
+
return waitForDeviceLinkAnswerService({ deviceCode: body.deviceCode, waitMillis, signal });
|
|
17802
|
+
});
|
|
17803
|
+
}
|
|
17804
|
+
function longPoll(wait) {
|
|
17805
|
+
return async (c, next) => {
|
|
17806
|
+
const body = await c.req.json().catch(() => null) ?? {};
|
|
17807
|
+
const { waitMillis } = body;
|
|
17808
|
+
if (!Number.isInteger(waitMillis) || waitMillis <= 0) {
|
|
17809
|
+
return next();
|
|
17810
|
+
}
|
|
17811
|
+
const signal = c.req.raw.signal;
|
|
17812
|
+
const waitedMillis = await wait(c, body, waitMillis, signal);
|
|
17813
|
+
if (signal.aborted) {
|
|
17814
|
+
return c.body(null, 204);
|
|
17815
|
+
}
|
|
17816
|
+
c.set(DEVICE_LINK_WAITED_MILLIS, waitedMillis);
|
|
17817
|
+
return next();
|
|
17818
|
+
};
|
|
17819
|
+
}
|
|
17820
|
+
|
|
16940
17821
|
// src/server/routes/auth/index.ts
|
|
16941
17822
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
16942
17823
|
body: Type.Object({
|
|
@@ -17095,11 +17976,23 @@ var startDeviceAuth = route.post("/_auth/device/start").input({
|
|
|
17095
17976
|
});
|
|
17096
17977
|
var pollDeviceAuth = route.post("/_auth/device/poll").input({
|
|
17097
17978
|
body: Type.Object({
|
|
17098
|
-
deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
|
|
17979
|
+
deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" }),
|
|
17980
|
+
waitMillis: Type.Optional(Type.Integer({
|
|
17981
|
+
minimum: 0,
|
|
17982
|
+
description: "Longest to hold the request while nobody has answered; capped by the server"
|
|
17983
|
+
}))
|
|
17099
17984
|
})
|
|
17100
|
-
}).use([
|
|
17985
|
+
}).use([
|
|
17986
|
+
rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }),
|
|
17987
|
+
deviceAuthLongPoll(),
|
|
17988
|
+
Transactional()
|
|
17989
|
+
]).skip(["auth"]).handler(async (c) => {
|
|
17101
17990
|
const { body } = await c.data();
|
|
17102
|
-
return await pollDeviceAuthService({
|
|
17991
|
+
return await pollDeviceAuthService({
|
|
17992
|
+
deviceCode: body.deviceCode,
|
|
17993
|
+
...deviceProvenance(c.raw),
|
|
17994
|
+
waitedMillis: Number(c.raw.get(DEVICE_AUTH_WAITED_MILLIS) ?? 0)
|
|
17995
|
+
});
|
|
17103
17996
|
});
|
|
17104
17997
|
var getDeviceAuthInfo = route.post("/_auth/device/info").input({
|
|
17105
17998
|
body: Type.Object({
|
|
@@ -17136,6 +18029,101 @@ var denyDeviceAuth = route.post("/_auth/device/deny").input({
|
|
|
17136
18029
|
await denyDeviceAuthService(body);
|
|
17137
18030
|
return c.noContent();
|
|
17138
18031
|
});
|
|
18032
|
+
function issuerOf(c) {
|
|
18033
|
+
const { userId, keyId } = getAuth(c);
|
|
18034
|
+
return { userId: Number(userId), keyId };
|
|
18035
|
+
}
|
|
18036
|
+
var issueDeviceLink = route.post("/_auth/device/link/issue").use([
|
|
18037
|
+
rateLimitPolicy("auth-device-link-issue", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
18038
|
+
Transactional()
|
|
18039
|
+
]).handler(async (c) => {
|
|
18040
|
+
return await issueDeviceLinkService(issuerOf(c));
|
|
18041
|
+
});
|
|
18042
|
+
var redeemDeviceLink = route.post("/_auth/device/link/redeem").input({
|
|
18043
|
+
body: Type.Object({
|
|
18044
|
+
userCode: UserCodeSchema,
|
|
18045
|
+
publicKey: PublicKeySchema,
|
|
18046
|
+
keyId: KeyIdSchema,
|
|
18047
|
+
fingerprint: FingerprintSchema,
|
|
18048
|
+
algorithm: Type.Optional(Type.Union(
|
|
18049
|
+
KEY_ALGORITHM.map((algo) => Type.Literal(algo)),
|
|
18050
|
+
{ description: "Signature algorithm" }
|
|
18051
|
+
)),
|
|
18052
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
18053
|
+
platform: Type.Optional(PlatformSchema)
|
|
18054
|
+
})
|
|
18055
|
+
}).use([rateLimitPolicy("auth-device-link-redeem", { limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
18056
|
+
const { body } = await c.data();
|
|
18057
|
+
return await redeemDeviceLinkService(body);
|
|
18058
|
+
});
|
|
18059
|
+
var getDeviceLinkStatus = route.post("/_auth/device/link/status").input({
|
|
18060
|
+
body: Type.Object({
|
|
18061
|
+
linkId: LinkIdSchema,
|
|
18062
|
+
waitMillis: Type.Optional(Type.Integer({
|
|
18063
|
+
minimum: 0,
|
|
18064
|
+
description: "Longest to hold the request while the link waits on the other device; capped by the server"
|
|
18065
|
+
}))
|
|
18066
|
+
})
|
|
18067
|
+
}).use([
|
|
18068
|
+
rateLimitPolicy("auth-device-link-status", { limit: 30, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 150 }) }),
|
|
18069
|
+
deviceLinkStatusLongPoll()
|
|
18070
|
+
]).handler(async (c) => {
|
|
18071
|
+
const { body } = await c.data();
|
|
18072
|
+
return await getDeviceLinkStatusService({ linkId: body.linkId, issuer: issuerOf(c) });
|
|
18073
|
+
});
|
|
18074
|
+
var confirmDeviceLink = route.post("/_auth/device/link/confirm").input({
|
|
18075
|
+
body: Type.Object({
|
|
18076
|
+
linkId: LinkIdSchema,
|
|
18077
|
+
choice: MatchChoiceSchema
|
|
18078
|
+
})
|
|
18079
|
+
}).use([
|
|
18080
|
+
rateLimitPolicy("auth-device-link-confirm", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) })
|
|
18081
|
+
]).handler(async (c) => {
|
|
18082
|
+
const { body } = await c.data();
|
|
18083
|
+
return await confirmDeviceLinkService({ linkId: body.linkId, choice: body.choice, issuer: issuerOf(c) });
|
|
18084
|
+
});
|
|
18085
|
+
var denyDeviceLink = route.post("/_auth/device/link/deny").input({
|
|
18086
|
+
body: Type.Object({
|
|
18087
|
+
linkId: LinkIdSchema
|
|
18088
|
+
})
|
|
18089
|
+
}).use([
|
|
18090
|
+
rateLimitPolicy("auth-device-link-deny", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
18091
|
+
Transactional()
|
|
18092
|
+
]).handler(async (c) => {
|
|
18093
|
+
const { body } = await c.data();
|
|
18094
|
+
return await denyDeviceLinkService({ linkId: body.linkId, issuer: issuerOf(c) });
|
|
18095
|
+
});
|
|
18096
|
+
var cancelDeviceLink = route.post("/_auth/device/link/cancel").input({
|
|
18097
|
+
body: Type.Object({
|
|
18098
|
+
linkId: LinkIdSchema
|
|
18099
|
+
})
|
|
18100
|
+
}).use([
|
|
18101
|
+
rateLimitPolicy("auth-device-link-cancel", { limit: 10, windowMs: 6e4, by: byIpAndCaller({ ipLimit: 50 }) }),
|
|
18102
|
+
Transactional()
|
|
18103
|
+
]).handler(async (c) => {
|
|
18104
|
+
const { body } = await c.data();
|
|
18105
|
+
return await cancelDeviceLinkService({ linkId: body.linkId, issuer: issuerOf(c) });
|
|
18106
|
+
});
|
|
18107
|
+
var pollDeviceLink = route.post("/_auth/device/link/poll").input({
|
|
18108
|
+
body: Type.Object({
|
|
18109
|
+
deviceCode: Type.String({ description: "Device code returned by /_auth/device/link/redeem" }),
|
|
18110
|
+
waitMillis: Type.Optional(Type.Integer({
|
|
18111
|
+
minimum: 0,
|
|
18112
|
+
description: "Longest to hold the request while the issuer has not picked; capped by the server"
|
|
18113
|
+
}))
|
|
18114
|
+
})
|
|
18115
|
+
}).use([
|
|
18116
|
+
rateLimitPolicy("auth-device-link-poll", { limit: 30, windowMs: 6e4 }),
|
|
18117
|
+
deviceLinkPollLongPoll(),
|
|
18118
|
+
Transactional()
|
|
18119
|
+
]).skip(["auth"]).handler(async (c) => {
|
|
18120
|
+
const { body } = await c.data();
|
|
18121
|
+
return await pollDeviceLinkService({
|
|
18122
|
+
deviceCode: body.deviceCode,
|
|
18123
|
+
...deviceProvenance(c.raw),
|
|
18124
|
+
waitedMillis: Number(c.raw.get(DEVICE_LINK_WAITED_MILLIS) ?? 0)
|
|
18125
|
+
});
|
|
18126
|
+
});
|
|
17139
18127
|
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
17140
18128
|
const auth = getAuth(c);
|
|
17141
18129
|
if (!auth) {
|
|
@@ -17251,6 +18239,13 @@ var authRouter = defineRouter({
|
|
|
17251
18239
|
getDeviceAuthInfo,
|
|
17252
18240
|
approveDeviceAuth,
|
|
17253
18241
|
denyDeviceAuth,
|
|
18242
|
+
issueDeviceLink,
|
|
18243
|
+
redeemDeviceLink,
|
|
18244
|
+
getDeviceLinkStatus,
|
|
18245
|
+
confirmDeviceLink,
|
|
18246
|
+
denyDeviceLink,
|
|
18247
|
+
cancelDeviceLink,
|
|
18248
|
+
pollDeviceLink,
|
|
17254
18249
|
logout,
|
|
17255
18250
|
rotateKey,
|
|
17256
18251
|
listKeys,
|
|
@@ -17558,13 +18553,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
17558
18553
|
var INT64_MIN = -(2n ** 63n);
|
|
17559
18554
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
17560
18555
|
function parseCanonicalJson(bytes) {
|
|
17561
|
-
let
|
|
18556
|
+
let text28;
|
|
17562
18557
|
try {
|
|
17563
|
-
|
|
18558
|
+
text28 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
17564
18559
|
} catch {
|
|
17565
18560
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
17566
18561
|
}
|
|
17567
|
-
const parser = new Parser(
|
|
18562
|
+
const parser = new Parser(text28);
|
|
17568
18563
|
const value = parser.parseValue();
|
|
17569
18564
|
parser.skipWhitespace();
|
|
17570
18565
|
if (!parser.atEnd()) {
|
|
@@ -17585,8 +18580,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
17585
18580
|
return true;
|
|
17586
18581
|
}
|
|
17587
18582
|
var Parser = class {
|
|
17588
|
-
constructor(
|
|
17589
|
-
this.text =
|
|
18583
|
+
constructor(text28) {
|
|
18584
|
+
this.text = text28;
|
|
17590
18585
|
}
|
|
17591
18586
|
pos = 0;
|
|
17592
18587
|
atEnd() {
|
|
@@ -18176,7 +19171,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
18176
19171
|
|
|
18177
19172
|
// src/server/client-proof/contract-bundle.ts
|
|
18178
19173
|
init_wire_headers();
|
|
18179
|
-
var CONTRACT_VERSION = "0.13.
|
|
19174
|
+
var CONTRACT_VERSION = "0.13.2";
|
|
18180
19175
|
var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
|
|
18181
19176
|
function required(name, type) {
|
|
18182
19177
|
return { name, type, optional: false };
|
|
@@ -18510,7 +19505,8 @@ var CONTRACT_TYPES = [
|
|
|
18510
19505
|
{
|
|
18511
19506
|
name: "PollDeviceAuthRequest",
|
|
18512
19507
|
fields: [
|
|
18513
|
-
required("deviceCode", "string")
|
|
19508
|
+
required("deviceCode", "string"),
|
|
19509
|
+
optional("waitMillis", "integer")
|
|
18514
19510
|
]
|
|
18515
19511
|
},
|
|
18516
19512
|
/**
|
|
@@ -18571,6 +19567,39 @@ var CONTRACT_TYPES = [
|
|
|
18571
19567
|
fields: [
|
|
18572
19568
|
required("userCode", "string")
|
|
18573
19569
|
]
|
|
19570
|
+
},
|
|
19571
|
+
/**
|
|
19572
|
+
* `StartDeviceAuthRequest` with the code it redeems in front: the same key
|
|
19573
|
+
* material under the same bounds, since this is the other place a caller
|
|
19574
|
+
* with nothing to authenticate parks a key before anyone agreed to it.
|
|
19575
|
+
*/
|
|
19576
|
+
{
|
|
19577
|
+
name: "RedeemDeviceLinkRequest",
|
|
19578
|
+
fields: [
|
|
19579
|
+
required("userCode", "string"),
|
|
19580
|
+
required("publicKey", "string"),
|
|
19581
|
+
required("keyId", "string"),
|
|
19582
|
+
required("fingerprint", "string"),
|
|
19583
|
+
optional("algorithm", "KeyAlgorithm"),
|
|
19584
|
+
optional("deviceName", "string"),
|
|
19585
|
+
optional("platform", "KeyPlatform")
|
|
19586
|
+
]
|
|
19587
|
+
},
|
|
19588
|
+
{
|
|
19589
|
+
name: "RedeemDeviceLinkResponse",
|
|
19590
|
+
fields: [
|
|
19591
|
+
required("deviceCode", "string"),
|
|
19592
|
+
required("matchNumber", "integer"),
|
|
19593
|
+
required("expiresAtMillis", "integer"),
|
|
19594
|
+
required("intervalMillis", "integer")
|
|
19595
|
+
]
|
|
19596
|
+
},
|
|
19597
|
+
{
|
|
19598
|
+
name: "PollDeviceLinkRequest",
|
|
19599
|
+
fields: [
|
|
19600
|
+
required("deviceCode", "string"),
|
|
19601
|
+
optional("waitMillis", "integer")
|
|
19602
|
+
]
|
|
18574
19603
|
}
|
|
18575
19604
|
];
|
|
18576
19605
|
var CONTRACT_ENUMS = [
|
|
@@ -19638,16 +20667,16 @@ function extractBearer2(header) {
|
|
|
19638
20667
|
}
|
|
19639
20668
|
|
|
19640
20669
|
// src/server/middleware/ops-or-user.ts
|
|
19641
|
-
function opsOrUser(
|
|
19642
|
-
const roles2 =
|
|
19643
|
-
const permissions2 =
|
|
19644
|
-
if (!
|
|
20670
|
+
function opsOrUser(config5) {
|
|
20671
|
+
const roles2 = config5.roles ?? [];
|
|
20672
|
+
const permissions2 = config5.permissions ?? [];
|
|
20673
|
+
if (!config5.opsScopes || config5.opsScopes.length === 0) {
|
|
19645
20674
|
throw new Error("opsOrUser: opsScopes must name at least one scope \u2014 an ops token would otherwise be admitted unchecked.");
|
|
19646
20675
|
}
|
|
19647
20676
|
if (roles2.length === 0 && permissions2.length === 0) {
|
|
19648
20677
|
throw new Error("opsOrUser: give roles, permissions, or both \u2014 a user session would otherwise be admitted unchecked.");
|
|
19649
20678
|
}
|
|
19650
|
-
const ops = chain([opsTokenAuth.handler, requireOpsScope(...
|
|
20679
|
+
const ops = chain([opsTokenAuth.handler, requireOpsScope(...config5.opsScopes)]);
|
|
19651
20680
|
const user = chain([
|
|
19652
20681
|
authenticate.handler,
|
|
19653
20682
|
...roles2.length > 0 ? [requireRole(...roles2)] : [],
|
|
@@ -19662,8 +20691,8 @@ function bearerOf(header) {
|
|
|
19662
20691
|
function chain(handlers) {
|
|
19663
20692
|
return async (c, next) => {
|
|
19664
20693
|
let answer;
|
|
19665
|
-
const runFrom = async (
|
|
19666
|
-
const produced =
|
|
20694
|
+
const runFrom = async (index22) => {
|
|
20695
|
+
const produced = index22 < handlers.length ? await handlers[index22](c, () => runFrom(index22 + 1)) : await next();
|
|
19667
20696
|
if (produced instanceof Response) {
|
|
19668
20697
|
answer = produced;
|
|
19669
20698
|
}
|
|
@@ -20690,13 +21719,13 @@ var NO_STORE_HEADERS = {
|
|
|
20690
21719
|
Pragma: "no-cache"
|
|
20691
21720
|
};
|
|
20692
21721
|
function requireAuthorizationServer() {
|
|
20693
|
-
const
|
|
20694
|
-
if (!
|
|
21722
|
+
const config5 = getAuthorizationServerConfig();
|
|
21723
|
+
if (!config5) {
|
|
20695
21724
|
throw new NotFoundError6({
|
|
20696
21725
|
message: "This application does not run an OAuth 2.1 authorization server. Pass `authorizationServer` to createAuthLifecycle() to enable one."
|
|
20697
21726
|
});
|
|
20698
21727
|
}
|
|
20699
|
-
return
|
|
21728
|
+
return config5;
|
|
20700
21729
|
}
|
|
20701
21730
|
function oauth2ErrorResponse(c, status, error, description) {
|
|
20702
21731
|
return c.json({ error, error_description: description }, status, NO_STORE_HEADERS);
|
|
@@ -20767,18 +21796,18 @@ var revokeOAuth2Grant = route14.delete("/_auth/oauth2/grants/:id").input({ param
|
|
|
20767
21796
|
return { revoked: true };
|
|
20768
21797
|
});
|
|
20769
21798
|
var oauth2AuthorizationServerMetadata = route14.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
|
|
20770
|
-
const
|
|
21799
|
+
const config5 = requireAuthorizationServer();
|
|
20771
21800
|
return c.json({
|
|
20772
|
-
issuer:
|
|
20773
|
-
authorization_endpoint:
|
|
20774
|
-
token_endpoint: new URL("/_auth/oauth2/token",
|
|
20775
|
-
registration_endpoint: new URL("/_auth/oauth2/register",
|
|
20776
|
-
revocation_endpoint: new URL("/_auth/oauth2/revoke",
|
|
21801
|
+
issuer: config5.issuer,
|
|
21802
|
+
authorization_endpoint: config5.authorizeUrl,
|
|
21803
|
+
token_endpoint: new URL("/_auth/oauth2/token", config5.issuer).toString(),
|
|
21804
|
+
registration_endpoint: new URL("/_auth/oauth2/register", config5.issuer).toString(),
|
|
21805
|
+
revocation_endpoint: new URL("/_auth/oauth2/revoke", config5.issuer).toString(),
|
|
20777
21806
|
response_types_supported: SUPPORTED_RESPONSE_TYPES,
|
|
20778
21807
|
grant_types_supported: SUPPORTED_GRANT_TYPES,
|
|
20779
21808
|
token_endpoint_auth_methods_supported: ["none"],
|
|
20780
21809
|
code_challenge_methods_supported: ["S256"],
|
|
20781
|
-
scopes_supported: Object.keys(
|
|
21810
|
+
scopes_supported: Object.keys(config5.scopes)
|
|
20782
21811
|
});
|
|
20783
21812
|
});
|
|
20784
21813
|
|
|
@@ -20878,6 +21907,14 @@ var mainAuthRouter = defineRouter6({
|
|
|
20878
21907
|
getDeviceAuthInfo,
|
|
20879
21908
|
approveDeviceAuth,
|
|
20880
21909
|
denyDeviceAuth,
|
|
21910
|
+
// Device link routes
|
|
21911
|
+
issueDeviceLink,
|
|
21912
|
+
redeemDeviceLink,
|
|
21913
|
+
getDeviceLinkStatus,
|
|
21914
|
+
confirmDeviceLink,
|
|
21915
|
+
denyDeviceLink,
|
|
21916
|
+
cancelDeviceLink,
|
|
21917
|
+
pollDeviceLink,
|
|
20881
21918
|
// Passkey routes (WebAuthn)
|
|
20882
21919
|
passkeyRegisterOptions,
|
|
20883
21920
|
passkeyRegisterVerify,
|
|
@@ -21365,6 +22402,7 @@ function assertOAuthRedirectUris(env21 = process.env) {
|
|
|
21365
22402
|
function createAuthLifecycle(options = {}) {
|
|
21366
22403
|
configureDeletion(options.deletion);
|
|
21367
22404
|
configureDeviceAuth(options.deviceAuth);
|
|
22405
|
+
configureDeviceLink(options.deviceLink);
|
|
21368
22406
|
configureAuthorizationServer(options.authorizationServer);
|
|
21369
22407
|
return {
|
|
21370
22408
|
/**
|
|
@@ -21479,12 +22517,16 @@ export {
|
|
|
21479
22517
|
DEFAULT_DELETION_PURGE_STRATEGY,
|
|
21480
22518
|
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
21481
22519
|
DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
22520
|
+
DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
|
|
21482
22521
|
DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
22522
|
+
DEFAULT_DEVICE_LINK_TTL_MS,
|
|
21483
22523
|
DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
21484
22524
|
DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,
|
|
21485
22525
|
DEVICE_AUTH_STATUSES,
|
|
22526
|
+
DEVICE_LINK_STATUSES,
|
|
21486
22527
|
DeviceAuthPollResponseSchema,
|
|
21487
22528
|
DeviceAuthorizationsRepository,
|
|
22529
|
+
DeviceLinksRepository,
|
|
21488
22530
|
DeviceNameSchema,
|
|
21489
22531
|
EmailSchema,
|
|
21490
22532
|
EnvironmentKeyringTokenCipher,
|
|
@@ -21498,11 +22540,13 @@ export {
|
|
|
21498
22540
|
KeyIdSchema,
|
|
21499
22541
|
KeyRevokeAllTokensRepository,
|
|
21500
22542
|
KeysRepository,
|
|
22543
|
+
LinkIdSchema,
|
|
21501
22544
|
MAX_UNGRANTED_CLIENTS_PER_IP,
|
|
21502
22545
|
MFA_CHALLENGE_ATTEMPT_LIMIT,
|
|
21503
22546
|
MFA_CHALLENGE_CHANNELS,
|
|
21504
22547
|
MFA_CONFIRM_ATTEMPT_LIMIT,
|
|
21505
22548
|
MFA_VERIFICATION_METHODS,
|
|
22549
|
+
MatchChoiceSchema,
|
|
21506
22550
|
MfaChallengesRepository,
|
|
21507
22551
|
MfaEnrolmentRepository,
|
|
21508
22552
|
MfaRecoveryCodesRepository,
|
|
@@ -21587,6 +22631,7 @@ export {
|
|
|
21587
22631
|
bearerAuthContext,
|
|
21588
22632
|
buildOAuthErrorUrl,
|
|
21589
22633
|
cancelAccountDeletionService,
|
|
22634
|
+
cancelDeviceLinkService,
|
|
21590
22635
|
cancelInvitation,
|
|
21591
22636
|
carryStepUpVerification,
|
|
21592
22637
|
changePasswordService,
|
|
@@ -21597,7 +22642,9 @@ export {
|
|
|
21597
22642
|
configureAuthorizationServer,
|
|
21598
22643
|
configureDeletion,
|
|
21599
22644
|
configureDeviceAuth,
|
|
22645
|
+
configureDeviceLink,
|
|
21600
22646
|
configureOAuthTokenCipher,
|
|
22647
|
+
confirmDeviceLinkService,
|
|
21601
22648
|
confirmPasswordResetService,
|
|
21602
22649
|
confirmSignupLinkService,
|
|
21603
22650
|
confirmTotpEnrolmentService,
|
|
@@ -21616,12 +22663,15 @@ export {
|
|
|
21616
22663
|
deleteInvitation,
|
|
21617
22664
|
deleteRole,
|
|
21618
22665
|
denyDeviceAuthService,
|
|
22666
|
+
denyDeviceLinkService,
|
|
21619
22667
|
denyOAuth2AuthorizeService,
|
|
21620
22668
|
deriveCsrfToken,
|
|
21621
22669
|
describeOAuth2AuthorizeRequestService,
|
|
21622
22670
|
describeRevokeAllLink,
|
|
21623
22671
|
deviceAuthorizations,
|
|
21624
22672
|
deviceAuthorizationsRepository,
|
|
22673
|
+
deviceLinks,
|
|
22674
|
+
deviceLinksRepository,
|
|
21625
22675
|
disableMfaService,
|
|
21626
22676
|
disableSessionBindingService,
|
|
21627
22677
|
enableSessionBindingService,
|
|
@@ -21656,6 +22706,8 @@ export {
|
|
|
21656
22706
|
getDeletionConfig,
|
|
21657
22707
|
getDeviceAuthConfig,
|
|
21658
22708
|
getDeviceAuthInfoService,
|
|
22709
|
+
getDeviceLinkConfig,
|
|
22710
|
+
getDeviceLinkStatusService,
|
|
21659
22711
|
getDummyPasswordHash,
|
|
21660
22712
|
getEnabledOAuthProviders,
|
|
21661
22713
|
getEncryptionKeyring,
|
|
@@ -21716,6 +22768,7 @@ export {
|
|
|
21716
22768
|
isPkceS256ChallengeShaped,
|
|
21717
22769
|
isPkceVerifierShaped,
|
|
21718
22770
|
isSafeReturnPath,
|
|
22771
|
+
issueDeviceLinkService,
|
|
21719
22772
|
issueOneTimeTokenService,
|
|
21720
22773
|
issueOpsTokenService,
|
|
21721
22774
|
kakaoProvider,
|
|
@@ -21783,9 +22836,11 @@ export {
|
|
|
21783
22836
|
permissionsRepository,
|
|
21784
22837
|
pkceChallengeFor,
|
|
21785
22838
|
pollDeviceAuthService,
|
|
22839
|
+
pollDeviceLinkService,
|
|
21786
22840
|
purgeRevokeAllTokensService,
|
|
21787
22841
|
purgeStaleOAuth2ClientsService,
|
|
21788
22842
|
purgeUserService,
|
|
22843
|
+
redeemDeviceLinkService,
|
|
21789
22844
|
redirectHostOf,
|
|
21790
22845
|
refreshAccessToken,
|
|
21791
22846
|
refuseRedirectUriRegistration,
|