@spfn/auth 0.3.0-beta.27 → 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 +146 -2
- package/dist/client-proof.d.ts +8 -1
- package/dist/client-proof.js +56 -1
- 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 +389 -336
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-BvRw8b8t.d.ts → machine-principals-B0bjs-0K.d.ts} +896 -285
- package/dist/server.d.ts +396 -174
- package/dist/server.js +1557 -679
- 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,74 +8605,104 @@ var init_mfa_enrolment_repository = __esm({
|
|
|
8505
8605
|
}
|
|
8506
8606
|
});
|
|
8507
8607
|
|
|
8508
|
-
// src/server/lib/
|
|
8608
|
+
// src/server/lib/answer-waiters.ts
|
|
8509
8609
|
import { onAfterCommit } from "@spfn/core/db";
|
|
8510
|
-
function
|
|
8511
|
-
|
|
8512
|
-
|
|
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
|
+
}
|
|
8513
8622
|
}
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8623
|
+
function unpark(id27, parked, resolver) {
|
|
8624
|
+
parked.delete(resolver);
|
|
8625
|
+
if (parked.size === 0 && parkedById.get(id27) === parked) {
|
|
8626
|
+
parkedById.delete(id27);
|
|
8517
8627
|
}
|
|
8518
|
-
});
|
|
8519
|
-
}
|
|
8520
|
-
function wake(id26) {
|
|
8521
|
-
const parked = waiters.get(id26);
|
|
8522
|
-
if (!parked) {
|
|
8523
|
-
return;
|
|
8524
8628
|
}
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
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
|
+
});
|
|
8528
8638
|
}
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
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
|
+
});
|
|
8533
8656
|
}
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
}
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
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
|
+
};
|
|
8547
8680
|
}
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
waiters.delete(id26);
|
|
8681
|
+
var init_answer_waiters = __esm({
|
|
8682
|
+
"src/server/lib/answer-waiters.ts"() {
|
|
8683
|
+
"use strict";
|
|
8552
8684
|
}
|
|
8685
|
+
});
|
|
8686
|
+
|
|
8687
|
+
// src/server/lib/device-auth-waiters.ts
|
|
8688
|
+
function announceDeviceAuthAnswered(ids) {
|
|
8689
|
+
waiters.announce(ids);
|
|
8553
8690
|
}
|
|
8554
|
-
function
|
|
8555
|
-
return
|
|
8691
|
+
function waitForDeviceAuthAnswer(id27, timeoutMs, signal) {
|
|
8692
|
+
return waiters.wait(id27, timeoutMs, signal);
|
|
8556
8693
|
}
|
|
8557
|
-
|
|
8558
|
-
waiting
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
const left = waitingOnDeviceAuth(id26) - 1;
|
|
8563
|
-
if (left > 0) {
|
|
8564
|
-
waiting.set(id26, left);
|
|
8565
|
-
} else {
|
|
8566
|
-
waiting.delete(id26);
|
|
8567
|
-
}
|
|
8568
|
-
}
|
|
8694
|
+
function waitingOnDeviceAuth(id27) {
|
|
8695
|
+
return waiters.waiting(id27);
|
|
8696
|
+
}
|
|
8697
|
+
function holdDeviceAuthWait(id27, wait) {
|
|
8698
|
+
return waiters.hold(id27, wait);
|
|
8569
8699
|
}
|
|
8570
|
-
var waiters
|
|
8700
|
+
var waiters;
|
|
8571
8701
|
var init_device_auth_waiters = __esm({
|
|
8572
8702
|
"src/server/lib/device-auth-waiters.ts"() {
|
|
8573
8703
|
"use strict";
|
|
8574
|
-
|
|
8575
|
-
|
|
8704
|
+
init_answer_waiters();
|
|
8705
|
+
waiters = createAnswerWaiters();
|
|
8576
8706
|
}
|
|
8577
8707
|
});
|
|
8578
8708
|
|
|
@@ -8649,10 +8779,10 @@ var init_device_authorizations_repository = __esm({
|
|
|
8649
8779
|
*
|
|
8650
8780
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8651
8781
|
*/
|
|
8652
|
-
async approve(
|
|
8782
|
+
async approve(id27, userId) {
|
|
8653
8783
|
const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
|
|
8654
8784
|
and14(
|
|
8655
|
-
eq14(deviceAuthorizations.id,
|
|
8785
|
+
eq14(deviceAuthorizations.id, id27),
|
|
8656
8786
|
eq14(deviceAuthorizations.status, "pending"),
|
|
8657
8787
|
notExpired()
|
|
8658
8788
|
)
|
|
@@ -8666,10 +8796,10 @@ var init_device_authorizations_repository = __esm({
|
|
|
8666
8796
|
*
|
|
8667
8797
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
8668
8798
|
*/
|
|
8669
|
-
async deny(
|
|
8799
|
+
async deny(id27) {
|
|
8670
8800
|
const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
8671
8801
|
and14(
|
|
8672
|
-
eq14(deviceAuthorizations.id,
|
|
8802
|
+
eq14(deviceAuthorizations.id, id27),
|
|
8673
8803
|
eq14(deviceAuthorizations.status, "pending"),
|
|
8674
8804
|
notExpired()
|
|
8675
8805
|
)
|
|
@@ -8744,27 +8874,243 @@ var init_device_authorizations_repository = __esm({
|
|
|
8744
8874
|
}
|
|
8745
8875
|
});
|
|
8746
8876
|
|
|
8747
|
-
// 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
|
|
8748
8900
|
import { BaseRepository as BaseRepository15 } from "@spfn/core/db";
|
|
8749
|
-
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";
|
|
8750
9096
|
var RolesRepository, rolesRepository;
|
|
8751
9097
|
var init_roles_repository = __esm({
|
|
8752
9098
|
"src/server/repositories/roles.repository.ts"() {
|
|
8753
9099
|
"use strict";
|
|
8754
9100
|
init_roles();
|
|
8755
|
-
RolesRepository = class extends
|
|
9101
|
+
RolesRepository = class extends BaseRepository16 {
|
|
8756
9102
|
/**
|
|
8757
9103
|
* ID로 역할 조회
|
|
8758
9104
|
*/
|
|
8759
|
-
async findById(
|
|
8760
|
-
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);
|
|
8761
9107
|
return result[0] ?? null;
|
|
8762
9108
|
}
|
|
8763
9109
|
/**
|
|
8764
9110
|
* Name으로 역할 조회
|
|
8765
9111
|
*/
|
|
8766
9112
|
async findByName(name) {
|
|
8767
|
-
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);
|
|
8768
9114
|
return result[0] ?? null;
|
|
8769
9115
|
}
|
|
8770
9116
|
/**
|
|
@@ -8777,7 +9123,7 @@ var init_roles_repository = __esm({
|
|
|
8777
9123
|
* 활성 역할만 조회
|
|
8778
9124
|
*/
|
|
8779
9125
|
async findActive() {
|
|
8780
|
-
return this.readDb.select().from(roles).where(
|
|
9126
|
+
return this.readDb.select().from(roles).where(eq16(roles.isActive, true)).orderBy(asc(roles.priority));
|
|
8781
9127
|
}
|
|
8782
9128
|
/**
|
|
8783
9129
|
* 역할 생성
|
|
@@ -8788,15 +9134,15 @@ var init_roles_repository = __esm({
|
|
|
8788
9134
|
/**
|
|
8789
9135
|
* 역할 업데이트
|
|
8790
9136
|
*/
|
|
8791
|
-
async updateById(
|
|
8792
|
-
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();
|
|
8793
9139
|
return result[0] ?? null;
|
|
8794
9140
|
}
|
|
8795
9141
|
/**
|
|
8796
9142
|
* 역할 삭제
|
|
8797
9143
|
*/
|
|
8798
|
-
async deleteById(
|
|
8799
|
-
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();
|
|
8800
9146
|
return result[0] ?? null;
|
|
8801
9147
|
}
|
|
8802
9148
|
};
|
|
@@ -8805,26 +9151,26 @@ var init_roles_repository = __esm({
|
|
|
8805
9151
|
});
|
|
8806
9152
|
|
|
8807
9153
|
// src/server/repositories/permissions.repository.ts
|
|
8808
|
-
import { BaseRepository as
|
|
8809
|
-
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";
|
|
8810
9156
|
var PermissionsRepository, permissionsRepository;
|
|
8811
9157
|
var init_permissions_repository = __esm({
|
|
8812
9158
|
"src/server/repositories/permissions.repository.ts"() {
|
|
8813
9159
|
"use strict";
|
|
8814
9160
|
init_permissions();
|
|
8815
|
-
PermissionsRepository = class extends
|
|
9161
|
+
PermissionsRepository = class extends BaseRepository17 {
|
|
8816
9162
|
/**
|
|
8817
9163
|
* ID로 권한 조회
|
|
8818
9164
|
*/
|
|
8819
|
-
async findById(
|
|
8820
|
-
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);
|
|
8821
9167
|
return result[0] ?? null;
|
|
8822
9168
|
}
|
|
8823
9169
|
/**
|
|
8824
9170
|
* Name으로 권한 조회
|
|
8825
9171
|
*/
|
|
8826
9172
|
async findByName(name) {
|
|
8827
|
-
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);
|
|
8828
9174
|
return result[0] ?? null;
|
|
8829
9175
|
}
|
|
8830
9176
|
/**
|
|
@@ -8832,7 +9178,7 @@ var init_permissions_repository = __esm({
|
|
|
8832
9178
|
*/
|
|
8833
9179
|
async findByNames(names) {
|
|
8834
9180
|
if (names.length === 0) return [];
|
|
8835
|
-
return this.readDb.select().from(permissions).where(
|
|
9181
|
+
return this.readDb.select().from(permissions).where(inArray4(permissions.name, names));
|
|
8836
9182
|
}
|
|
8837
9183
|
/**
|
|
8838
9184
|
* 모든 권한 조회
|
|
@@ -8844,13 +9190,13 @@ var init_permissions_repository = __esm({
|
|
|
8844
9190
|
* 활성 권한만 조회
|
|
8845
9191
|
*/
|
|
8846
9192
|
async findActive() {
|
|
8847
|
-
return this.readDb.select().from(permissions).where(
|
|
9193
|
+
return this.readDb.select().from(permissions).where(eq17(permissions.isActive, true)).orderBy(asc2(permissions.name));
|
|
8848
9194
|
}
|
|
8849
9195
|
/**
|
|
8850
9196
|
* 카테고리별 권한 조회
|
|
8851
9197
|
*/
|
|
8852
9198
|
async findByCategory(category) {
|
|
8853
|
-
return this.readDb.select().from(permissions).where(
|
|
9199
|
+
return this.readDb.select().from(permissions).where(eq17(permissions.category, category)).orderBy(asc2(permissions.name));
|
|
8854
9200
|
}
|
|
8855
9201
|
/**
|
|
8856
9202
|
* 권한 생성
|
|
@@ -8868,15 +9214,15 @@ var init_permissions_repository = __esm({
|
|
|
8868
9214
|
/**
|
|
8869
9215
|
* 권한 업데이트
|
|
8870
9216
|
*/
|
|
8871
|
-
async updateById(
|
|
8872
|
-
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();
|
|
8873
9219
|
return result[0] ?? null;
|
|
8874
9220
|
}
|
|
8875
9221
|
/**
|
|
8876
9222
|
* 권한 삭제
|
|
8877
9223
|
*/
|
|
8878
|
-
async deleteById(
|
|
8879
|
-
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();
|
|
8880
9226
|
return result[0] ?? null;
|
|
8881
9227
|
}
|
|
8882
9228
|
};
|
|
@@ -8885,25 +9231,25 @@ var init_permissions_repository = __esm({
|
|
|
8885
9231
|
});
|
|
8886
9232
|
|
|
8887
9233
|
// src/server/repositories/role-permissions.repository.ts
|
|
8888
|
-
import { BaseRepository as
|
|
8889
|
-
import { and as
|
|
9234
|
+
import { BaseRepository as BaseRepository18 } from "@spfn/core/db";
|
|
9235
|
+
import { and as and16, eq as eq18 } from "drizzle-orm";
|
|
8890
9236
|
var RolePermissionsRepository, rolePermissionsRepository;
|
|
8891
9237
|
var init_role_permissions_repository = __esm({
|
|
8892
9238
|
"src/server/repositories/role-permissions.repository.ts"() {
|
|
8893
9239
|
"use strict";
|
|
8894
9240
|
init_role_permissions();
|
|
8895
|
-
RolePermissionsRepository = class extends
|
|
9241
|
+
RolePermissionsRepository = class extends BaseRepository18 {
|
|
8896
9242
|
/**
|
|
8897
9243
|
* 역할 ID로 모든 권한 조회
|
|
8898
9244
|
*/
|
|
8899
9245
|
async findByRoleId(roleId) {
|
|
8900
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
9246
|
+
return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.roleId, roleId));
|
|
8901
9247
|
}
|
|
8902
9248
|
/**
|
|
8903
9249
|
* 권한 ID로 모든 역할 조회
|
|
8904
9250
|
*/
|
|
8905
9251
|
async findByPermissionId(permissionId) {
|
|
8906
|
-
return this.readDb.select().from(rolePermissions).where(
|
|
9252
|
+
return this.readDb.select().from(rolePermissions).where(eq18(rolePermissions.permissionId, permissionId));
|
|
8907
9253
|
}
|
|
8908
9254
|
/**
|
|
8909
9255
|
* 역할-권한 매핑 생성
|
|
@@ -8923,9 +9269,9 @@ var init_role_permissions_repository = __esm({
|
|
|
8923
9269
|
*/
|
|
8924
9270
|
async deleteByRoleIdAndPermissionId(roleId, permissionId) {
|
|
8925
9271
|
const result = await this.db.delete(rolePermissions).where(
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
9272
|
+
and16(
|
|
9273
|
+
eq18(rolePermissions.roleId, roleId),
|
|
9274
|
+
eq18(rolePermissions.permissionId, permissionId)
|
|
8929
9275
|
)
|
|
8930
9276
|
).returning();
|
|
8931
9277
|
return result[0] ?? null;
|
|
@@ -8934,7 +9280,7 @@ var init_role_permissions_repository = __esm({
|
|
|
8934
9280
|
* 역할의 모든 권한 매핑 삭제
|
|
8935
9281
|
*/
|
|
8936
9282
|
async deleteByRoleId(roleId) {
|
|
8937
|
-
const result = await this.db.delete(rolePermissions).where(
|
|
9283
|
+
const result = await this.db.delete(rolePermissions).where(eq18(rolePermissions.roleId, roleId)).returning();
|
|
8938
9284
|
return result.length;
|
|
8939
9285
|
}
|
|
8940
9286
|
/**
|
|
@@ -8955,19 +9301,19 @@ var init_role_permissions_repository = __esm({
|
|
|
8955
9301
|
});
|
|
8956
9302
|
|
|
8957
9303
|
// src/server/repositories/user-permissions.repository.ts
|
|
8958
|
-
import { BaseRepository as
|
|
8959
|
-
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";
|
|
8960
9306
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
8961
9307
|
var init_user_permissions_repository = __esm({
|
|
8962
9308
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
8963
9309
|
"use strict";
|
|
8964
9310
|
init_user_permissions();
|
|
8965
|
-
UserPermissionsRepository = class extends
|
|
9311
|
+
UserPermissionsRepository = class extends BaseRepository19 {
|
|
8966
9312
|
/**
|
|
8967
9313
|
* 사용자 ID로 모든 권한 오버라이드 조회
|
|
8968
9314
|
*/
|
|
8969
9315
|
async findByUserId(userId) {
|
|
8970
|
-
return this.readDb.select().from(userPermissions).where(
|
|
9316
|
+
return this.readDb.select().from(userPermissions).where(eq19(userPermissions.userId, userId));
|
|
8971
9317
|
}
|
|
8972
9318
|
/**
|
|
8973
9319
|
* 사용자 ID로 유효한 권한 오버라이드만 조회
|
|
@@ -8976,11 +9322,11 @@ var init_user_permissions_repository = __esm({
|
|
|
8976
9322
|
async findValidByUserId(userId) {
|
|
8977
9323
|
const now = /* @__PURE__ */ new Date();
|
|
8978
9324
|
return this.readDb.select().from(userPermissions).where(
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
9325
|
+
and17(
|
|
9326
|
+
eq19(userPermissions.userId, userId),
|
|
9327
|
+
or5(
|
|
9328
|
+
isNull13(userPermissions.expiresAt),
|
|
9329
|
+
gt9(userPermissions.expiresAt, now)
|
|
8984
9330
|
)
|
|
8985
9331
|
)
|
|
8986
9332
|
);
|
|
@@ -8990,9 +9336,9 @@ var init_user_permissions_repository = __esm({
|
|
|
8990
9336
|
*/
|
|
8991
9337
|
async findByUserIdAndPermissionId(userId, permissionId) {
|
|
8992
9338
|
const result = await this.readDb.select().from(userPermissions).where(
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
9339
|
+
and17(
|
|
9340
|
+
eq19(userPermissions.userId, userId),
|
|
9341
|
+
eq19(userPermissions.permissionId, permissionId)
|
|
8996
9342
|
)
|
|
8997
9343
|
).limit(1);
|
|
8998
9344
|
return result[0] ?? null;
|
|
@@ -9006,8 +9352,8 @@ var init_user_permissions_repository = __esm({
|
|
|
9006
9352
|
/**
|
|
9007
9353
|
* 사용자 권한 오버라이드 업데이트
|
|
9008
9354
|
*/
|
|
9009
|
-
async updateById(
|
|
9010
|
-
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();
|
|
9011
9357
|
return result[0] ?? null;
|
|
9012
9358
|
}
|
|
9013
9359
|
/**
|
|
@@ -9015,9 +9361,9 @@ var init_user_permissions_repository = __esm({
|
|
|
9015
9361
|
*/
|
|
9016
9362
|
async deleteByUserIdAndPermissionId(userId, permissionId) {
|
|
9017
9363
|
const result = await this.db.delete(userPermissions).where(
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9364
|
+
and17(
|
|
9365
|
+
eq19(userPermissions.userId, userId),
|
|
9366
|
+
eq19(userPermissions.permissionId, permissionId)
|
|
9021
9367
|
)
|
|
9022
9368
|
).returning();
|
|
9023
9369
|
return result[0] ?? null;
|
|
@@ -9026,7 +9372,7 @@ var init_user_permissions_repository = __esm({
|
|
|
9026
9372
|
* 사용자의 모든 권한 오버라이드 삭제
|
|
9027
9373
|
*/
|
|
9028
9374
|
async deleteByUserId(userId) {
|
|
9029
|
-
const result = await this.db.delete(userPermissions).where(
|
|
9375
|
+
const result = await this.db.delete(userPermissions).where(eq19(userPermissions.userId, userId)).returning();
|
|
9030
9376
|
return result.length;
|
|
9031
9377
|
}
|
|
9032
9378
|
/**
|
|
@@ -9035,7 +9381,7 @@ var init_user_permissions_repository = __esm({
|
|
|
9035
9381
|
async deleteExpired() {
|
|
9036
9382
|
const now = /* @__PURE__ */ new Date();
|
|
9037
9383
|
const result = await this.db.delete(userPermissions).where(
|
|
9038
|
-
|
|
9384
|
+
and17(
|
|
9039
9385
|
isNotNull3(userPermissions.expiresAt),
|
|
9040
9386
|
lt6(userPermissions.expiresAt, now)
|
|
9041
9387
|
)
|
|
@@ -9048,33 +9394,33 @@ var init_user_permissions_repository = __esm({
|
|
|
9048
9394
|
});
|
|
9049
9395
|
|
|
9050
9396
|
// src/server/repositories/user-profiles.repository.ts
|
|
9051
|
-
import { BaseRepository as
|
|
9052
|
-
import { eq as
|
|
9397
|
+
import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
|
|
9398
|
+
import { eq as eq20 } from "drizzle-orm";
|
|
9053
9399
|
var UserProfilesRepository, userProfilesRepository;
|
|
9054
9400
|
var init_user_profiles_repository = __esm({
|
|
9055
9401
|
"src/server/repositories/user-profiles.repository.ts"() {
|
|
9056
9402
|
"use strict";
|
|
9057
9403
|
init_user_profiles();
|
|
9058
|
-
UserProfilesRepository = class extends
|
|
9404
|
+
UserProfilesRepository = class extends BaseRepository20 {
|
|
9059
9405
|
/**
|
|
9060
9406
|
* ID로 프로필 조회
|
|
9061
9407
|
*/
|
|
9062
|
-
async findById(
|
|
9063
|
-
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);
|
|
9064
9410
|
return result[0] ?? null;
|
|
9065
9411
|
}
|
|
9066
9412
|
/**
|
|
9067
9413
|
* User ID로 locale만 조회 (경량)
|
|
9068
9414
|
*/
|
|
9069
9415
|
async findLocaleByUserId(userId) {
|
|
9070
|
-
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);
|
|
9071
9417
|
return result[0]?.locale || "en";
|
|
9072
9418
|
}
|
|
9073
9419
|
/**
|
|
9074
9420
|
* User ID로 프로필 조회
|
|
9075
9421
|
*/
|
|
9076
9422
|
async findByUserId(userId) {
|
|
9077
|
-
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);
|
|
9078
9424
|
return result[0] ?? null;
|
|
9079
9425
|
}
|
|
9080
9426
|
/**
|
|
@@ -9086,29 +9432,29 @@ var init_user_profiles_repository = __esm({
|
|
|
9086
9432
|
/**
|
|
9087
9433
|
* 프로필 업데이트 (by ID)
|
|
9088
9434
|
*/
|
|
9089
|
-
async updateById(
|
|
9090
|
-
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();
|
|
9091
9437
|
return result[0] ?? null;
|
|
9092
9438
|
}
|
|
9093
9439
|
/**
|
|
9094
9440
|
* 프로필 업데이트 (by User ID)
|
|
9095
9441
|
*/
|
|
9096
9442
|
async updateByUserId(userId, data) {
|
|
9097
|
-
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();
|
|
9098
9444
|
return result[0] ?? null;
|
|
9099
9445
|
}
|
|
9100
9446
|
/**
|
|
9101
9447
|
* 프로필 삭제 (by ID)
|
|
9102
9448
|
*/
|
|
9103
|
-
async deleteById(
|
|
9104
|
-
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();
|
|
9105
9451
|
return result[0] ?? null;
|
|
9106
9452
|
}
|
|
9107
9453
|
/**
|
|
9108
9454
|
* 프로필 삭제 (by User ID)
|
|
9109
9455
|
*/
|
|
9110
9456
|
async deleteByUserId(userId) {
|
|
9111
|
-
const result = await this.db.delete(userProfiles).where(
|
|
9457
|
+
const result = await this.db.delete(userProfiles).where(eq20(userProfiles.userId, userId)).returning();
|
|
9112
9458
|
return result[0] ?? null;
|
|
9113
9459
|
}
|
|
9114
9460
|
/**
|
|
@@ -9150,7 +9496,7 @@ var init_user_profiles_repository = __esm({
|
|
|
9150
9496
|
metadata: userProfiles.metadata,
|
|
9151
9497
|
createdAt: userProfiles.createdAt,
|
|
9152
9498
|
updatedAt: userProfiles.updatedAt
|
|
9153
|
-
}).from(userProfiles).where(
|
|
9499
|
+
}).from(userProfiles).where(eq20(userProfiles.userId, userId)).limit(1).then((rows) => rows[0] ?? null);
|
|
9154
9500
|
if (!profile) {
|
|
9155
9501
|
return null;
|
|
9156
9502
|
}
|
|
@@ -9178,8 +9524,8 @@ var init_user_profiles_repository = __esm({
|
|
|
9178
9524
|
});
|
|
9179
9525
|
|
|
9180
9526
|
// src/server/repositories/invitations.repository.ts
|
|
9181
|
-
import { eq as
|
|
9182
|
-
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";
|
|
9183
9529
|
var InvitationsRepository, invitationsRepository;
|
|
9184
9530
|
var init_invitations_repository = __esm({
|
|
9185
9531
|
"src/server/repositories/invitations.repository.ts"() {
|
|
@@ -9188,19 +9534,19 @@ var init_invitations_repository = __esm({
|
|
|
9188
9534
|
init_roles();
|
|
9189
9535
|
init_user_invitations();
|
|
9190
9536
|
init_email();
|
|
9191
|
-
InvitationsRepository = class extends
|
|
9537
|
+
InvitationsRepository = class extends BaseRepository21 {
|
|
9192
9538
|
/**
|
|
9193
9539
|
* ID로 초대 조회
|
|
9194
9540
|
*/
|
|
9195
|
-
async findById(
|
|
9196
|
-
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);
|
|
9197
9543
|
return result[0] ?? null;
|
|
9198
9544
|
}
|
|
9199
9545
|
/**
|
|
9200
9546
|
* Token으로 초대 조회
|
|
9201
9547
|
*/
|
|
9202
9548
|
async findByToken(token) {
|
|
9203
|
-
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);
|
|
9204
9550
|
return result[0] ?? null;
|
|
9205
9551
|
}
|
|
9206
9552
|
/**
|
|
@@ -9208,9 +9554,9 @@ var init_invitations_repository = __esm({
|
|
|
9208
9554
|
*/
|
|
9209
9555
|
async findPendingByEmail(email) {
|
|
9210
9556
|
const result = await this.readDb.select().from(userInvitations).where(
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9557
|
+
and18(
|
|
9558
|
+
eq21(userInvitations.email, normalizeEmail(email)),
|
|
9559
|
+
eq21(userInvitations.status, "pending")
|
|
9214
9560
|
)
|
|
9215
9561
|
).limit(1);
|
|
9216
9562
|
return result[0] ?? null;
|
|
@@ -9219,13 +9565,13 @@ var init_invitations_repository = __esm({
|
|
|
9219
9565
|
* 초대자 ID로 모든 초대 조회
|
|
9220
9566
|
*/
|
|
9221
9567
|
async findByInvitedBy(invitedBy) {
|
|
9222
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9568
|
+
return this.readDb.select().from(userInvitations).where(eq21(userInvitations.invitedBy, invitedBy));
|
|
9223
9569
|
}
|
|
9224
9570
|
/**
|
|
9225
9571
|
* 상태별 초대 조회
|
|
9226
9572
|
*/
|
|
9227
9573
|
async findByStatus(status) {
|
|
9228
|
-
return this.readDb.select().from(userInvitations).where(
|
|
9574
|
+
return this.readDb.select().from(userInvitations).where(eq21(userInvitations.status, status));
|
|
9229
9575
|
}
|
|
9230
9576
|
/**
|
|
9231
9577
|
* 초대 생성
|
|
@@ -9236,7 +9582,7 @@ var init_invitations_repository = __esm({
|
|
|
9236
9582
|
/**
|
|
9237
9583
|
* 초대 상태 업데이트
|
|
9238
9584
|
*/
|
|
9239
|
-
async updateStatus(
|
|
9585
|
+
async updateStatus(id27, status, timestamp2) {
|
|
9240
9586
|
const updates = {
|
|
9241
9587
|
status
|
|
9242
9588
|
};
|
|
@@ -9247,14 +9593,14 @@ var init_invitations_repository = __esm({
|
|
|
9247
9593
|
updates.cancelledAt = timestamp2;
|
|
9248
9594
|
}
|
|
9249
9595
|
}
|
|
9250
|
-
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();
|
|
9251
9597
|
return result[0] ?? null;
|
|
9252
9598
|
}
|
|
9253
9599
|
/**
|
|
9254
9600
|
* 초대 삭제
|
|
9255
9601
|
*/
|
|
9256
|
-
async deleteById(
|
|
9257
|
-
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();
|
|
9258
9604
|
return result[0] ?? null;
|
|
9259
9605
|
}
|
|
9260
9606
|
/**
|
|
@@ -9263,8 +9609,8 @@ var init_invitations_repository = __esm({
|
|
|
9263
9609
|
async updateExpiredInvitations() {
|
|
9264
9610
|
const now = /* @__PURE__ */ new Date();
|
|
9265
9611
|
const result = await this.db.update(userInvitations).set({ status: "expired" }).where(
|
|
9266
|
-
|
|
9267
|
-
|
|
9612
|
+
and18(
|
|
9613
|
+
eq21(userInvitations.status, "pending"),
|
|
9268
9614
|
lt7(userInvitations.expiresAt, now)
|
|
9269
9615
|
)
|
|
9270
9616
|
).returning();
|
|
@@ -9296,7 +9642,7 @@ var init_invitations_repository = __esm({
|
|
|
9296
9642
|
id: users.id,
|
|
9297
9643
|
email: users.email
|
|
9298
9644
|
}
|
|
9299
|
-
}).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);
|
|
9300
9646
|
return result[0] ?? null;
|
|
9301
9647
|
}
|
|
9302
9648
|
/**
|
|
@@ -9307,13 +9653,13 @@ var init_invitations_repository = __esm({
|
|
|
9307
9653
|
const offset = (page - 1) * limit;
|
|
9308
9654
|
const conditions = [];
|
|
9309
9655
|
if (status) {
|
|
9310
|
-
conditions.push(
|
|
9656
|
+
conditions.push(eq21(userInvitations.status, status));
|
|
9311
9657
|
}
|
|
9312
9658
|
if (invitedBy) {
|
|
9313
|
-
conditions.push(
|
|
9659
|
+
conditions.push(eq21(userInvitations.invitedBy, invitedBy));
|
|
9314
9660
|
}
|
|
9315
|
-
const whereClause = conditions.length > 0 ?
|
|
9316
|
-
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);
|
|
9317
9663
|
const total = Number(countResult[0]?.count || 0);
|
|
9318
9664
|
const results = await this.readDb.select({
|
|
9319
9665
|
id: userInvitations.id,
|
|
@@ -9337,7 +9683,7 @@ var init_invitations_repository = __esm({
|
|
|
9337
9683
|
id: users.id,
|
|
9338
9684
|
email: users.email
|
|
9339
9685
|
}
|
|
9340
|
-
}).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);
|
|
9341
9687
|
return {
|
|
9342
9688
|
invitations: results,
|
|
9343
9689
|
total,
|
|
@@ -9349,31 +9695,31 @@ var init_invitations_repository = __esm({
|
|
|
9349
9695
|
/**
|
|
9350
9696
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
9351
9697
|
*/
|
|
9352
|
-
async updateById(
|
|
9698
|
+
async updateById(id27, data) {
|
|
9353
9699
|
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
9354
|
-
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();
|
|
9355
9701
|
return result[0] ?? null;
|
|
9356
9702
|
}
|
|
9357
9703
|
/**
|
|
9358
9704
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
9359
9705
|
*/
|
|
9360
|
-
async resend(
|
|
9706
|
+
async resend(id27, newExpiresAt) {
|
|
9361
9707
|
const result = await this.db.update(userInvitations).set({
|
|
9362
9708
|
status: "pending",
|
|
9363
9709
|
expiresAt: newExpiresAt
|
|
9364
|
-
}).where(
|
|
9710
|
+
}).where(eq21(userInvitations.id, id27)).returning();
|
|
9365
9711
|
return result[0] ?? null;
|
|
9366
9712
|
}
|
|
9367
9713
|
/**
|
|
9368
9714
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
9369
9715
|
*/
|
|
9370
|
-
async cancel(
|
|
9716
|
+
async cancel(id27, cancelledBy, reason, currentMetadata) {
|
|
9371
9717
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
9372
9718
|
const result = await this.db.update(userInvitations).set({
|
|
9373
9719
|
status: "cancelled",
|
|
9374
9720
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
9375
9721
|
metadata: newMetadata
|
|
9376
|
-
}).where(
|
|
9722
|
+
}).where(eq21(userInvitations.id, id27)).returning();
|
|
9377
9723
|
return result[0] ?? null;
|
|
9378
9724
|
}
|
|
9379
9725
|
};
|
|
@@ -10213,15 +10559,15 @@ var init_token_cipher = __esm({
|
|
|
10213
10559
|
});
|
|
10214
10560
|
|
|
10215
10561
|
// src/server/repositories/social-accounts.repository.ts
|
|
10216
|
-
import { eq as
|
|
10217
|
-
import { BaseRepository as
|
|
10562
|
+
import { eq as eq22, and as and19 } from "drizzle-orm";
|
|
10563
|
+
import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
|
|
10218
10564
|
var SocialAccountsRepository, socialAccountsRepository;
|
|
10219
10565
|
var init_social_accounts_repository = __esm({
|
|
10220
10566
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
10221
10567
|
"use strict";
|
|
10222
10568
|
init_entities();
|
|
10223
10569
|
init_token_cipher();
|
|
10224
|
-
SocialAccountsRepository = class extends
|
|
10570
|
+
SocialAccountsRepository = class extends BaseRepository22 {
|
|
10225
10571
|
/**
|
|
10226
10572
|
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
10227
10573
|
*
|
|
@@ -10249,10 +10595,10 @@ var init_social_accounts_repository = __esm({
|
|
|
10249
10595
|
if (refresh?.needsRotation) {
|
|
10250
10596
|
heal.refreshToken = await encryptToken(refresh.value, context("refresh"));
|
|
10251
10597
|
}
|
|
10252
|
-
await this.db.update(userSocialAccounts).set(heal).where(
|
|
10253
|
-
|
|
10254
|
-
access?.needsRotation && account.accessToken !== null ?
|
|
10255
|
-
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
|
|
10256
10602
|
));
|
|
10257
10603
|
} catch {
|
|
10258
10604
|
}
|
|
@@ -10269,9 +10615,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10269
10615
|
*/
|
|
10270
10616
|
async findByProviderAndProviderId(provider, providerUserId) {
|
|
10271
10617
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10618
|
+
and19(
|
|
10619
|
+
eq22(userSocialAccounts.provider, provider),
|
|
10620
|
+
eq22(userSocialAccounts.providerUserId, providerUserId)
|
|
10275
10621
|
)
|
|
10276
10622
|
).limit(1);
|
|
10277
10623
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -10281,7 +10627,7 @@ var init_social_accounts_repository = __esm({
|
|
|
10281
10627
|
* Read replica 사용
|
|
10282
10628
|
*/
|
|
10283
10629
|
async findByUserId(userId) {
|
|
10284
|
-
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10630
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId));
|
|
10285
10631
|
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
10286
10632
|
}
|
|
10287
10633
|
/**
|
|
@@ -10290,9 +10636,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10290
10636
|
*/
|
|
10291
10637
|
async findByUserIdAndProvider(userId, provider) {
|
|
10292
10638
|
const result = await this.readDb.select().from(userSocialAccounts).where(
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10639
|
+
and19(
|
|
10640
|
+
eq22(userSocialAccounts.userId, userId),
|
|
10641
|
+
eq22(userSocialAccounts.provider, provider)
|
|
10296
10642
|
)
|
|
10297
10643
|
).limit(1);
|
|
10298
10644
|
return this.decryptAccount(result[0] ?? null);
|
|
@@ -10318,11 +10664,11 @@ var init_social_accounts_repository = __esm({
|
|
|
10318
10664
|
* 토큰 정보 업데이트
|
|
10319
10665
|
* Write primary 사용
|
|
10320
10666
|
*/
|
|
10321
|
-
async updateTokens(
|
|
10667
|
+
async updateTokens(id27, data) {
|
|
10322
10668
|
const accounts = await this.db.select({
|
|
10323
10669
|
provider: userSocialAccounts.provider,
|
|
10324
10670
|
providerUserId: userSocialAccounts.providerUserId
|
|
10325
|
-
}).from(userSocialAccounts).where(
|
|
10671
|
+
}).from(userSocialAccounts).where(eq22(userSocialAccounts.id, id27)).limit(1);
|
|
10326
10672
|
const account = accounts[0];
|
|
10327
10673
|
if (!account) {
|
|
10328
10674
|
return null;
|
|
@@ -10336,15 +10682,15 @@ var init_social_accounts_repository = __esm({
|
|
|
10336
10682
|
...data,
|
|
10337
10683
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
10338
10684
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
10339
|
-
}).where(
|
|
10685
|
+
}).where(eq22(userSocialAccounts.id, id27)).returning();
|
|
10340
10686
|
return this.decryptAccount(result[0] ?? null);
|
|
10341
10687
|
}
|
|
10342
10688
|
/**
|
|
10343
10689
|
* 소셜 계정 삭제
|
|
10344
10690
|
* Write primary 사용
|
|
10345
10691
|
*/
|
|
10346
|
-
async deleteById(
|
|
10347
|
-
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();
|
|
10348
10694
|
return result[0] ?? null;
|
|
10349
10695
|
}
|
|
10350
10696
|
/**
|
|
@@ -10353,9 +10699,9 @@ var init_social_accounts_repository = __esm({
|
|
|
10353
10699
|
*/
|
|
10354
10700
|
async deleteByUserIdAndProvider(userId, provider) {
|
|
10355
10701
|
const result = await this.db.delete(userSocialAccounts).where(
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10702
|
+
and19(
|
|
10703
|
+
eq22(userSocialAccounts.userId, userId),
|
|
10704
|
+
eq22(userSocialAccounts.provider, provider)
|
|
10359
10705
|
)
|
|
10360
10706
|
).returning();
|
|
10361
10707
|
return result[0] ?? null;
|
|
@@ -10368,7 +10714,7 @@ var init_social_accounts_repository = __esm({
|
|
|
10368
10714
|
* Write primary 사용
|
|
10369
10715
|
*/
|
|
10370
10716
|
async deleteAllByUserId(userId) {
|
|
10371
|
-
const result = await this.db.delete(userSocialAccounts).where(
|
|
10717
|
+
const result = await this.db.delete(userSocialAccounts).where(eq22(userSocialAccounts.userId, userId)).returning();
|
|
10372
10718
|
return result.length;
|
|
10373
10719
|
}
|
|
10374
10720
|
};
|
|
@@ -10377,19 +10723,19 @@ var init_social_accounts_repository = __esm({
|
|
|
10377
10723
|
});
|
|
10378
10724
|
|
|
10379
10725
|
// src/server/repositories/auth-metadata.repository.ts
|
|
10380
|
-
import { BaseRepository as
|
|
10381
|
-
import { eq as
|
|
10726
|
+
import { BaseRepository as BaseRepository23 } from "@spfn/core/db";
|
|
10727
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
10382
10728
|
var AuthMetadataRepository, authMetadataRepository;
|
|
10383
10729
|
var init_auth_metadata_repository = __esm({
|
|
10384
10730
|
"src/server/repositories/auth-metadata.repository.ts"() {
|
|
10385
10731
|
"use strict";
|
|
10386
10732
|
init_auth_metadata();
|
|
10387
|
-
AuthMetadataRepository = class extends
|
|
10733
|
+
AuthMetadataRepository = class extends BaseRepository23 {
|
|
10388
10734
|
/**
|
|
10389
10735
|
* 키로 값 조회
|
|
10390
10736
|
*/
|
|
10391
10737
|
async get(key) {
|
|
10392
|
-
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);
|
|
10393
10739
|
return result[0]?.value ?? null;
|
|
10394
10740
|
}
|
|
10395
10741
|
/**
|
|
@@ -10412,20 +10758,20 @@ var init_auth_metadata_repository = __esm({
|
|
|
10412
10758
|
});
|
|
10413
10759
|
|
|
10414
10760
|
// src/server/repositories/account-deletion-requests.repository.ts
|
|
10415
|
-
import { eq as
|
|
10416
|
-
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";
|
|
10417
10763
|
var AccountDeletionRequestsRepository, accountDeletionRequestsRepository;
|
|
10418
10764
|
var init_account_deletion_requests_repository = __esm({
|
|
10419
10765
|
"src/server/repositories/account-deletion-requests.repository.ts"() {
|
|
10420
10766
|
"use strict";
|
|
10421
10767
|
init_account_deletion_requests();
|
|
10422
|
-
AccountDeletionRequestsRepository = class extends
|
|
10768
|
+
AccountDeletionRequestsRepository = class extends BaseRepository24 {
|
|
10423
10769
|
/**
|
|
10424
10770
|
* ID로 요청 조회
|
|
10425
10771
|
* Read replica 사용
|
|
10426
10772
|
*/
|
|
10427
|
-
async findById(
|
|
10428
|
-
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);
|
|
10429
10775
|
return result[0] ?? null;
|
|
10430
10776
|
}
|
|
10431
10777
|
/**
|
|
@@ -10434,9 +10780,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10434
10780
|
*/
|
|
10435
10781
|
async findPendingByUserId(userId) {
|
|
10436
10782
|
const result = await this.readDb.select().from(accountDeletionRequests).where(
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10783
|
+
and20(
|
|
10784
|
+
eq24(accountDeletionRequests.userId, userId),
|
|
10785
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10440
10786
|
)
|
|
10441
10787
|
).limit(1);
|
|
10442
10788
|
return result[0] ?? null;
|
|
@@ -10450,9 +10796,9 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10450
10796
|
*/
|
|
10451
10797
|
async findPendingByUserIdOnPrimary(userId) {
|
|
10452
10798
|
const result = await this.db.select().from(accountDeletionRequests).where(
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10799
|
+
and20(
|
|
10800
|
+
eq24(accountDeletionRequests.userId, userId),
|
|
10801
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10456
10802
|
)
|
|
10457
10803
|
).limit(1);
|
|
10458
10804
|
return result[0] ?? null;
|
|
@@ -10463,8 +10809,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10463
10809
|
*/
|
|
10464
10810
|
async findDueForPurge(now) {
|
|
10465
10811
|
return this.readDb.select().from(accountDeletionRequests).where(
|
|
10466
|
-
|
|
10467
|
-
|
|
10812
|
+
and20(
|
|
10813
|
+
eq24(accountDeletionRequests.status, "pending"),
|
|
10468
10814
|
lte2(accountDeletionRequests.purgeScheduledAt, now)
|
|
10469
10815
|
)
|
|
10470
10816
|
);
|
|
@@ -10484,14 +10830,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10484
10830
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
10485
10831
|
* Write primary 사용
|
|
10486
10832
|
*/
|
|
10487
|
-
async markCancelled(
|
|
10833
|
+
async markCancelled(id27) {
|
|
10488
10834
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10489
10835
|
status: "cancelled",
|
|
10490
10836
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
10491
10837
|
}).where(
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10838
|
+
and20(
|
|
10839
|
+
eq24(accountDeletionRequests.id, id27),
|
|
10840
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10495
10841
|
)
|
|
10496
10842
|
).returning();
|
|
10497
10843
|
return result[0] ?? null;
|
|
@@ -10506,15 +10852,15 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10506
10852
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
10507
10853
|
* Write primary 사용
|
|
10508
10854
|
*/
|
|
10509
|
-
async markCompleted(
|
|
10855
|
+
async markCompleted(id27, purgeStrategy) {
|
|
10510
10856
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
10511
10857
|
status: "completed",
|
|
10512
10858
|
completedAt: /* @__PURE__ */ new Date(),
|
|
10513
10859
|
purgeStrategy
|
|
10514
10860
|
}).where(
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10861
|
+
and20(
|
|
10862
|
+
eq24(accountDeletionRequests.id, id27),
|
|
10863
|
+
eq24(accountDeletionRequests.status, "pending")
|
|
10518
10864
|
)
|
|
10519
10865
|
).returning();
|
|
10520
10866
|
return result[0] ?? null;
|
|
@@ -10525,14 +10871,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
10525
10871
|
});
|
|
10526
10872
|
|
|
10527
10873
|
// src/server/repositories/ops-tokens.repository.ts
|
|
10528
|
-
import { and as
|
|
10529
|
-
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";
|
|
10530
10876
|
var OpsTokensRepository, opsTokensRepository;
|
|
10531
10877
|
var init_ops_tokens_repository = __esm({
|
|
10532
10878
|
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
10533
10879
|
"use strict";
|
|
10534
10880
|
init_ops_tokens();
|
|
10535
|
-
OpsTokensRepository = class extends
|
|
10881
|
+
OpsTokensRepository = class extends BaseRepository25 {
|
|
10536
10882
|
/**
|
|
10537
10883
|
* Lookup by the secret's hash — the verification path.
|
|
10538
10884
|
*
|
|
@@ -10542,7 +10888,7 @@ var init_ops_tokens_repository = __esm({
|
|
|
10542
10888
|
* and revocation is documented as taking effect immediately.
|
|
10543
10889
|
*/
|
|
10544
10890
|
async findByTokenHash(tokenHash) {
|
|
10545
|
-
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);
|
|
10546
10892
|
return result[0] ?? null;
|
|
10547
10893
|
}
|
|
10548
10894
|
async create(data) {
|
|
@@ -10557,13 +10903,13 @@ var init_ops_tokens_repository = __esm({
|
|
|
10557
10903
|
* token is already revoked — the first revocation's timestamp is never
|
|
10558
10904
|
* overwritten.
|
|
10559
10905
|
*/
|
|
10560
|
-
async revokeById(
|
|
10561
|
-
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();
|
|
10562
10908
|
return result[0] ?? null;
|
|
10563
10909
|
}
|
|
10564
10910
|
/** Fire-and-forget from the verification path. */
|
|
10565
|
-
async updateLastUsedById(
|
|
10566
|
-
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));
|
|
10567
10913
|
}
|
|
10568
10914
|
};
|
|
10569
10915
|
opsTokensRepository = new OpsTokensRepository();
|
|
@@ -10571,15 +10917,15 @@ var init_ops_tokens_repository = __esm({
|
|
|
10571
10917
|
});
|
|
10572
10918
|
|
|
10573
10919
|
// src/server/repositories/oauth2-clients.repository.ts
|
|
10574
|
-
import { and as
|
|
10575
|
-
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";
|
|
10576
10922
|
var OAuth2ClientsRepository, oauth2ClientsRepository;
|
|
10577
10923
|
var init_oauth2_clients_repository = __esm({
|
|
10578
10924
|
"src/server/repositories/oauth2-clients.repository.ts"() {
|
|
10579
10925
|
"use strict";
|
|
10580
10926
|
init_oauth2_clients();
|
|
10581
10927
|
init_oauth2_grants();
|
|
10582
|
-
OAuth2ClientsRepository = class extends
|
|
10928
|
+
OAuth2ClientsRepository = class extends BaseRepository26 {
|
|
10583
10929
|
async create(data) {
|
|
10584
10930
|
const result = await this.db.insert(oauth2Clients).values(data).returning();
|
|
10585
10931
|
return result[0];
|
|
@@ -10593,12 +10939,12 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10593
10939
|
* at the one moment a CLI is being connected.
|
|
10594
10940
|
*/
|
|
10595
10941
|
async findByClientId(clientId) {
|
|
10596
|
-
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);
|
|
10597
10943
|
return result[0] ?? null;
|
|
10598
10944
|
}
|
|
10599
10945
|
/** Fire-and-forget from the token-issuing path. */
|
|
10600
|
-
async updateLastUsedById(
|
|
10601
|
-
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));
|
|
10602
10948
|
}
|
|
10603
10949
|
/**
|
|
10604
10950
|
* Register a client unless this address is already at its standing cap.
|
|
@@ -10620,7 +10966,7 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10620
10966
|
*/
|
|
10621
10967
|
async createWithinStandingCap(data, limit) {
|
|
10622
10968
|
return await runInTransaction(async () => {
|
|
10623
|
-
await this.db.execute(
|
|
10969
|
+
await this.db.execute(sql12`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
|
|
10624
10970
|
const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
|
|
10625
10971
|
return standing < limit.max ? await this.create(data) : null;
|
|
10626
10972
|
});
|
|
@@ -10642,11 +10988,11 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10642
10988
|
* count exists to see.
|
|
10643
10989
|
*/
|
|
10644
10990
|
async countRecentUngrantedByIp(ip, windowMs) {
|
|
10645
|
-
const result = await this.db.select({ count:
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
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})`
|
|
10650
10996
|
)
|
|
10651
10997
|
);
|
|
10652
10998
|
return result[0]?.count ?? 0;
|
|
@@ -10663,9 +11009,9 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10663
11009
|
*/
|
|
10664
11010
|
async deleteStaleUngranted(before) {
|
|
10665
11011
|
const deleted = await this.db.delete(oauth2Clients).where(
|
|
10666
|
-
|
|
11012
|
+
and22(
|
|
10667
11013
|
lt8(oauth2Clients.createdAt, before),
|
|
10668
|
-
|
|
11014
|
+
sql12`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
10669
11015
|
)
|
|
10670
11016
|
).returning({ id: oauth2Clients.id });
|
|
10671
11017
|
return deleted.length;
|
|
@@ -10676,8 +11022,8 @@ var init_oauth2_clients_repository = __esm({
|
|
|
10676
11022
|
});
|
|
10677
11023
|
|
|
10678
11024
|
// src/server/repositories/oauth2-grants.repository.ts
|
|
10679
|
-
import { and as
|
|
10680
|
-
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";
|
|
10681
11027
|
var OAuth2GrantsRepository, oauth2GrantsRepository;
|
|
10682
11028
|
var init_oauth2_grants_repository = __esm({
|
|
10683
11029
|
"src/server/repositories/oauth2-grants.repository.ts"() {
|
|
@@ -10685,7 +11031,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10685
11031
|
init_oauth2_grants();
|
|
10686
11032
|
init_oauth2_clients();
|
|
10687
11033
|
init_oauth2_tokens();
|
|
10688
|
-
OAuth2GrantsRepository = class extends
|
|
11034
|
+
OAuth2GrantsRepository = class extends BaseRepository27 {
|
|
10689
11035
|
/**
|
|
10690
11036
|
* Record a consent, or refresh the scopes of the one already there.
|
|
10691
11037
|
*
|
|
@@ -10712,13 +11058,13 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10712
11058
|
*
|
|
10713
11059
|
* Read primary: revocation is documented as taking effect immediately.
|
|
10714
11060
|
*/
|
|
10715
|
-
async findWithClientById(
|
|
10716
|
-
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);
|
|
10717
11063
|
return result[0] ?? null;
|
|
10718
11064
|
}
|
|
10719
11065
|
/** What the user's "connected apps" screen lists. Read replica. */
|
|
10720
11066
|
async listActiveByUserId(userId) {
|
|
10721
|
-
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));
|
|
10722
11068
|
}
|
|
10723
11069
|
/**
|
|
10724
11070
|
* Revoke one live grant belonging to one user.
|
|
@@ -10731,19 +11077,19 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10731
11077
|
* @returns the revoked row, or null if there was no live grant of that id
|
|
10732
11078
|
* for that user
|
|
10733
11079
|
*/
|
|
10734
|
-
async revokeByIdForUser(
|
|
11080
|
+
async revokeByIdForUser(id27, userId) {
|
|
10735
11081
|
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
11082
|
+
and23(
|
|
11083
|
+
eq27(oauth2Grants.id, id27),
|
|
11084
|
+
eq27(oauth2Grants.user, userId),
|
|
11085
|
+
isNull15(oauth2Grants.revokedAt)
|
|
10740
11086
|
)
|
|
10741
11087
|
).returning();
|
|
10742
11088
|
return result[0] ?? null;
|
|
10743
11089
|
}
|
|
10744
11090
|
/** Revoke one grant by id, whoever it belongs to — the replay detections. */
|
|
10745
|
-
async revokeById(
|
|
10746
|
-
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();
|
|
10747
11093
|
return result[0] ?? null;
|
|
10748
11094
|
}
|
|
10749
11095
|
/**
|
|
@@ -10753,7 +11099,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10753
11099
|
* @returns the grant ids this call revoked
|
|
10754
11100
|
*/
|
|
10755
11101
|
async revokeAllActiveByUserId(userId) {
|
|
10756
|
-
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 });
|
|
10757
11103
|
return revoked.map((row) => row.id);
|
|
10758
11104
|
}
|
|
10759
11105
|
/**
|
|
@@ -10769,7 +11115,7 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10769
11115
|
if (grantIds.length === 0) {
|
|
10770
11116
|
return 0;
|
|
10771
11117
|
}
|
|
10772
|
-
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 });
|
|
10773
11119
|
return revoked.length;
|
|
10774
11120
|
}
|
|
10775
11121
|
};
|
|
@@ -10778,14 +11124,14 @@ var init_oauth2_grants_repository = __esm({
|
|
|
10778
11124
|
});
|
|
10779
11125
|
|
|
10780
11126
|
// src/server/repositories/oauth2-authorization-codes.repository.ts
|
|
10781
|
-
import { and as
|
|
10782
|
-
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";
|
|
10783
11129
|
var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
|
|
10784
11130
|
var init_oauth2_authorization_codes_repository = __esm({
|
|
10785
11131
|
"src/server/repositories/oauth2-authorization-codes.repository.ts"() {
|
|
10786
11132
|
"use strict";
|
|
10787
11133
|
init_oauth2_authorization_codes();
|
|
10788
|
-
OAuth2AuthorizationCodesRepository = class extends
|
|
11134
|
+
OAuth2AuthorizationCodesRepository = class extends BaseRepository28 {
|
|
10789
11135
|
async create(data) {
|
|
10790
11136
|
const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
|
|
10791
11137
|
return result[0];
|
|
@@ -10797,11 +11143,11 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10797
11143
|
* spent, or past its 60 seconds
|
|
10798
11144
|
*/
|
|
10799
11145
|
async consume(codeHash) {
|
|
10800
|
-
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt:
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
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()`)
|
|
10805
11151
|
)
|
|
10806
11152
|
).returning();
|
|
10807
11153
|
return result[0] ?? null;
|
|
@@ -10812,7 +11158,7 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10812
11158
|
* would be indistinguishable from a code that never existed.
|
|
10813
11159
|
*/
|
|
10814
11160
|
async findByCodeHash(codeHash) {
|
|
10815
|
-
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);
|
|
10816
11162
|
return result[0] ?? null;
|
|
10817
11163
|
}
|
|
10818
11164
|
};
|
|
@@ -10821,14 +11167,14 @@ var init_oauth2_authorization_codes_repository = __esm({
|
|
|
10821
11167
|
});
|
|
10822
11168
|
|
|
10823
11169
|
// src/server/repositories/oauth2-tokens.repository.ts
|
|
10824
|
-
import { and as
|
|
10825
|
-
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";
|
|
10826
11172
|
var OAuth2TokensRepository, oauth2TokensRepository;
|
|
10827
11173
|
var init_oauth2_tokens_repository = __esm({
|
|
10828
11174
|
"src/server/repositories/oauth2-tokens.repository.ts"() {
|
|
10829
11175
|
"use strict";
|
|
10830
11176
|
init_oauth2_tokens();
|
|
10831
|
-
OAuth2TokensRepository = class extends
|
|
11177
|
+
OAuth2TokensRepository = class extends BaseRepository29 {
|
|
10832
11178
|
async create(data) {
|
|
10833
11179
|
const result = await this.db.insert(oauth2Tokens).values(data).returning();
|
|
10834
11180
|
return result[0];
|
|
@@ -10842,7 +11188,7 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10842
11188
|
* Unfiltered, so the service can tell revoked from expired from unknown.
|
|
10843
11189
|
*/
|
|
10844
11190
|
async findByTokenHash(tokenHash) {
|
|
10845
|
-
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);
|
|
10846
11192
|
return result[0] ?? null;
|
|
10847
11193
|
}
|
|
10848
11194
|
/**
|
|
@@ -10852,13 +11198,13 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10852
11198
|
* rotated, revoked, or expired
|
|
10853
11199
|
*/
|
|
10854
11200
|
async rotate(tokenHash) {
|
|
10855
|
-
const result = await this.db.update(oauth2Tokens).set({ replacedAt:
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
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()`)
|
|
10862
11208
|
)
|
|
10863
11209
|
).returning();
|
|
10864
11210
|
return result[0] ?? null;
|
|
@@ -10871,12 +11217,12 @@ var init_oauth2_tokens_repository = __esm({
|
|
|
10871
11217
|
* about whether the value it presented ever existed.
|
|
10872
11218
|
*/
|
|
10873
11219
|
async revokeByTokenHash(tokenHash) {
|
|
10874
|
-
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();
|
|
10875
11221
|
return result[0] ?? null;
|
|
10876
11222
|
}
|
|
10877
11223
|
/** Fire-and-forget from the verification path, as ops tokens do. */
|
|
10878
|
-
async updateLastUsedById(
|
|
10879
|
-
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));
|
|
10880
11226
|
}
|
|
10881
11227
|
};
|
|
10882
11228
|
oauth2TokensRepository = new OAuth2TokensRepository();
|
|
@@ -10901,6 +11247,7 @@ var init_repositories = __esm({
|
|
|
10901
11247
|
init_mfa_challenges_repository();
|
|
10902
11248
|
init_mfa_enrolment_repository();
|
|
10903
11249
|
init_device_authorizations_repository();
|
|
11250
|
+
init_device_links_repository();
|
|
10904
11251
|
init_roles_repository();
|
|
10905
11252
|
init_permissions_repository();
|
|
10906
11253
|
init_role_permissions_repository();
|
|
@@ -11006,7 +11353,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
11006
11353
|
}
|
|
11007
11354
|
async function setRolePermissions(roleId, permissionIds) {
|
|
11008
11355
|
const roleIdNum = Number(roleId);
|
|
11009
|
-
const permissionIdNums = permissionIds.map((
|
|
11356
|
+
const permissionIdNums = permissionIds.map((id27) => Number(id27));
|
|
11010
11357
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
11011
11358
|
}
|
|
11012
11359
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -11026,7 +11373,7 @@ async function getRolePermissions(roleId) {
|
|
|
11026
11373
|
}
|
|
11027
11374
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
11028
11375
|
const perms = await Promise.all(
|
|
11029
|
-
permissionIds.map((
|
|
11376
|
+
permissionIds.map((id27) => permissionsRepository.findById(id27))
|
|
11030
11377
|
);
|
|
11031
11378
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
11032
11379
|
}
|
|
@@ -11290,8 +11637,8 @@ async function listOAuth2GrantsService(userId) {
|
|
|
11290
11637
|
lastUsedAtMillis: client.lastUsedAt?.getTime()
|
|
11291
11638
|
}));
|
|
11292
11639
|
}
|
|
11293
|
-
async function revokeOAuth2GrantService(
|
|
11294
|
-
const revoked = await oauth2GrantsRepository.revokeByIdForUser(
|
|
11640
|
+
async function revokeOAuth2GrantService(id27, userId) {
|
|
11641
|
+
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id27, userId);
|
|
11295
11642
|
if (!revoked) {
|
|
11296
11643
|
throw new OAuth2GrantNotFoundError();
|
|
11297
11644
|
}
|
|
@@ -11717,6 +12064,7 @@ var DeviceRegistrationChannelSchema = Type.Union([
|
|
|
11717
12064
|
Type.Literal("oauth"),
|
|
11718
12065
|
Type.Literal("oauth-native"),
|
|
11719
12066
|
Type.Literal("device-code"),
|
|
12067
|
+
Type.Literal("device-link"),
|
|
11720
12068
|
Type.Literal("password-reset"),
|
|
11721
12069
|
Type.Literal("passkey"),
|
|
11722
12070
|
Type.Literal("renewal")
|
|
@@ -11949,16 +12297,16 @@ function encodeBase32(bytes) {
|
|
|
11949
12297
|
}
|
|
11950
12298
|
return bits > 0 ? output + BASE32_ALPHABET[value << 5 - bits & 31] : output;
|
|
11951
12299
|
}
|
|
11952
|
-
function decodeBase32(
|
|
12300
|
+
function decodeBase32(text28) {
|
|
11953
12301
|
let bits = 0;
|
|
11954
12302
|
let value = 0;
|
|
11955
12303
|
const bytes = [];
|
|
11956
|
-
for (const character of
|
|
11957
|
-
const
|
|
11958
|
-
if (
|
|
12304
|
+
for (const character of text28.replace(/=+$/, "").toUpperCase()) {
|
|
12305
|
+
const index22 = BASE32_ALPHABET.indexOf(character);
|
|
12306
|
+
if (index22 < 0) {
|
|
11959
12307
|
throw new Error("Value is not RFC 4648 base32");
|
|
11960
12308
|
}
|
|
11961
|
-
value = value << 5 |
|
|
12309
|
+
value = value << 5 | index22;
|
|
11962
12310
|
bits += 5;
|
|
11963
12311
|
if (bits >= 8) {
|
|
11964
12312
|
bytes.push(value >>> bits - 8 & 255);
|
|
@@ -12248,9 +12596,9 @@ function presentedChallenge(clientDataJSON) {
|
|
|
12248
12596
|
const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
|
|
12249
12597
|
return typeof decoded?.challenge === "string" ? decoded.challenge : "";
|
|
12250
12598
|
}
|
|
12251
|
-
function parseJson(
|
|
12599
|
+
function parseJson(text28) {
|
|
12252
12600
|
try {
|
|
12253
|
-
return JSON.parse(
|
|
12601
|
+
return JSON.parse(text28);
|
|
12254
12602
|
} catch {
|
|
12255
12603
|
return null;
|
|
12256
12604
|
}
|
|
@@ -12717,6 +13065,7 @@ async function revokeAllKeysService(params) {
|
|
|
12717
13065
|
}
|
|
12718
13066
|
const revoked = !includeCurrent && currentKeyId ? await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason) : await keysRepository.revokeAllActiveByUserId(userId, reason);
|
|
12719
13067
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
13068
|
+
await deviceLinksRepository.expireAllLiveByUserId(userId);
|
|
12720
13069
|
await revokeAllOAuth2GrantsForUser(userId);
|
|
12721
13070
|
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
12722
13071
|
}
|
|
@@ -12802,8 +13151,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
12802
13151
|
throw new VerificationTokenTargetMismatchError();
|
|
12803
13152
|
}
|
|
12804
13153
|
}
|
|
12805
|
-
async function sendDeletionEmail(to, subject,
|
|
12806
|
-
const result = await sendEmail3({ to, subject, text:
|
|
13154
|
+
async function sendDeletionEmail(to, subject, text28) {
|
|
13155
|
+
const result = await sendEmail3({ to, subject, text: text28 });
|
|
12807
13156
|
if (!result.success) {
|
|
12808
13157
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
12809
13158
|
}
|
|
@@ -12855,12 +13204,12 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
12855
13204
|
if (requestedBy === "self") {
|
|
12856
13205
|
await verifyReauthCredential(user, { password, verificationToken });
|
|
12857
13206
|
}
|
|
12858
|
-
const
|
|
13207
|
+
const config5 = getDeletionConfig();
|
|
12859
13208
|
const wantsImmediate = immediate === true;
|
|
12860
|
-
if (wantsImmediate && requestedBy === "self" && !
|
|
13209
|
+
if (wantsImmediate && requestedBy === "self" && !config5.allowSelfImmediate) {
|
|
12861
13210
|
throw new ImmediateDeletionNotAllowedError();
|
|
12862
13211
|
}
|
|
12863
|
-
const gracePeriodDays = wantsImmediate ? 0 :
|
|
13212
|
+
const gracePeriodDays = wantsImmediate ? 0 : config5.gracePeriodDays;
|
|
12864
13213
|
const requestedAt = /* @__PURE__ */ new Date();
|
|
12865
13214
|
const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
|
|
12866
13215
|
await usersRepository.updateById(user.id, { status: "pending_deletion" });
|
|
@@ -12883,6 +13232,7 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
12883
13232
|
}
|
|
12884
13233
|
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
12885
13234
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
13235
|
+
await deviceLinksRepository.expireAllLiveByUserId(user.id);
|
|
12886
13236
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
12887
13237
|
onAfterCommit4(() => authDeletionRequestedEvent.emit({
|
|
12888
13238
|
userId: String(user.id),
|
|
@@ -12967,10 +13317,10 @@ async function purgePendingRequest(request) {
|
|
|
12967
13317
|
if (!precheckUser || precheckUser.status !== "pending_deletion") {
|
|
12968
13318
|
return { outcome: "skipped" };
|
|
12969
13319
|
}
|
|
12970
|
-
const
|
|
12971
|
-
if (
|
|
13320
|
+
const config5 = getDeletionConfig();
|
|
13321
|
+
if (config5.onBeforePurge) {
|
|
12972
13322
|
try {
|
|
12973
|
-
await
|
|
13323
|
+
await config5.onBeforePurge({
|
|
12974
13324
|
id: precheckUser.id,
|
|
12975
13325
|
publicId: precheckUser.publicId,
|
|
12976
13326
|
email: precheckUser.email,
|
|
@@ -12984,7 +13334,7 @@ async function purgePendingRequest(request) {
|
|
|
12984
13334
|
return { outcome: "skipped" };
|
|
12985
13335
|
}
|
|
12986
13336
|
}
|
|
12987
|
-
const purgeStrategy =
|
|
13337
|
+
const purgeStrategy = config5.purgeStrategy;
|
|
12988
13338
|
let purgedUser = null;
|
|
12989
13339
|
await runInTransaction3(async () => {
|
|
12990
13340
|
const user = await usersRepository.findById(userId);
|
|
@@ -13229,6 +13579,7 @@ async function changePasswordService(params) {
|
|
|
13229
13579
|
const newPasswordHash = await hashPassword(newPassword);
|
|
13230
13580
|
await usersRepository.updatePassword(userId, newPasswordHash, true);
|
|
13231
13581
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
13582
|
+
await deviceLinksRepository.expireAllLiveByUserId(userId);
|
|
13232
13583
|
await revokeAllOAuth2GrantsForUser(userId);
|
|
13233
13584
|
await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
|
|
13234
13585
|
}
|
|
@@ -13430,7 +13781,9 @@ async function replaceCredentials(row, user, params) {
|
|
|
13430
13781
|
// for accounts created before the register flows stamped it.
|
|
13431
13782
|
...user.emailVerifiedAt ? {} : { emailVerifiedAt: /* @__PURE__ */ new Date() }
|
|
13432
13783
|
});
|
|
13784
|
+
await keysRepository.lockActiveByUserId(user.id);
|
|
13433
13785
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
13786
|
+
await deviceLinksRepository.expireAllLiveByUserId(user.id);
|
|
13434
13787
|
await revokeAllOAuth2GrantsForUser(user.id);
|
|
13435
13788
|
await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
|
|
13436
13789
|
const registered = await registerPublicKeyService({
|
|
@@ -13739,13 +14092,13 @@ async function waitForDeviceAuthAnswerService(params) {
|
|
|
13739
14092
|
await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
|
|
13740
14093
|
return Date.now() - startedAt;
|
|
13741
14094
|
}
|
|
13742
|
-
async function waitUntil(
|
|
14095
|
+
async function waitUntil(id27, deviceCodeHash, deadline, signal) {
|
|
13743
14096
|
while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
|
|
13744
14097
|
const remaining = deadline - Date.now();
|
|
13745
14098
|
if (remaining <= 0) {
|
|
13746
14099
|
return;
|
|
13747
14100
|
}
|
|
13748
|
-
await waitForDeviceAuthAnswer(
|
|
14101
|
+
await waitForDeviceAuthAnswer(id27, Math.min(remaining, WAIT_RECHECK_MS), signal);
|
|
13749
14102
|
if (!await readWaitable(deviceCodeHash)) {
|
|
13750
14103
|
return;
|
|
13751
14104
|
}
|
|
@@ -13780,15 +14133,25 @@ async function pollDeviceAuthService(params) {
|
|
|
13780
14133
|
() => new DeviceAuthNotFoundError()
|
|
13781
14134
|
);
|
|
13782
14135
|
}
|
|
13783
|
-
|
|
13784
|
-
}
|
|
13785
|
-
async function completeDeviceLogin(record, provenance) {
|
|
13786
|
-
if (record.userId === null) {
|
|
14136
|
+
if (consumed.userId === null) {
|
|
13787
14137
|
throw new DeviceAuthNotFoundError();
|
|
13788
14138
|
}
|
|
13789
|
-
|
|
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);
|
|
13790
14153
|
if (!user) {
|
|
13791
|
-
throw
|
|
14154
|
+
throw params.missingAccount();
|
|
13792
14155
|
}
|
|
13793
14156
|
if (user.status !== "active") {
|
|
13794
14157
|
if (user.status === "pending_deletion") {
|
|
@@ -13801,22 +14164,22 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13801
14164
|
}
|
|
13802
14165
|
const registered = await registerPublicKeyService({
|
|
13803
14166
|
userId: user.id,
|
|
13804
|
-
keyId:
|
|
13805
|
-
publicKey:
|
|
13806
|
-
fingerprint:
|
|
13807
|
-
algorithm:
|
|
13808
|
-
deviceName:
|
|
13809
|
-
platform:
|
|
13810
|
-
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,
|
|
13811
14174
|
ip: provenance.ip,
|
|
13812
14175
|
userAgent: provenance.userAgent,
|
|
13813
14176
|
binding: decideKeyBinding(user.sessionBinding, provenance.webProxy)
|
|
13814
14177
|
});
|
|
13815
14178
|
await updateLastLoginService(user.id);
|
|
13816
14179
|
const result = {
|
|
13817
|
-
//
|
|
13818
|
-
//
|
|
13819
|
-
// 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).
|
|
13820
14183
|
mfaRequired: false,
|
|
13821
14184
|
userId: String(user.id),
|
|
13822
14185
|
publicId: user.publicId,
|
|
@@ -13836,6 +14199,320 @@ async function completeDeviceLogin(record, provenance) {
|
|
|
13836
14199
|
return result;
|
|
13837
14200
|
}
|
|
13838
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
|
+
|
|
13839
14516
|
// src/server/services/passkey.service.ts
|
|
13840
14517
|
init_logger();
|
|
13841
14518
|
init_config();
|
|
@@ -14223,51 +14900,51 @@ async function initializeAuth(options = {}) {
|
|
|
14223
14900
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
14224
14901
|
}
|
|
14225
14902
|
async function syncRoles(configs, existingByName) {
|
|
14226
|
-
for (const
|
|
14227
|
-
const existing = existingByName.get(
|
|
14903
|
+
for (const config5 of configs) {
|
|
14904
|
+
const existing = existingByName.get(config5.name);
|
|
14228
14905
|
if (!existing) {
|
|
14229
14906
|
await rolesRepository.create({
|
|
14230
|
-
name:
|
|
14231
|
-
displayName:
|
|
14232
|
-
description:
|
|
14233
|
-
priority:
|
|
14234
|
-
isSystem:
|
|
14235
|
-
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,
|
|
14236
14913
|
isActive: true
|
|
14237
14914
|
});
|
|
14238
|
-
authLogger.service.info(` \u2705 Created role: ${
|
|
14915
|
+
authLogger.service.info(` \u2705 Created role: ${config5.name}`);
|
|
14239
14916
|
} else {
|
|
14240
14917
|
const updateData = {
|
|
14241
|
-
displayName:
|
|
14242
|
-
description:
|
|
14918
|
+
displayName: config5.displayName,
|
|
14919
|
+
description: config5.description || null
|
|
14243
14920
|
};
|
|
14244
14921
|
if (!existing.isBuiltin) {
|
|
14245
|
-
updateData.priority =
|
|
14922
|
+
updateData.priority = config5.priority ?? existing.priority;
|
|
14246
14923
|
}
|
|
14247
14924
|
await rolesRepository.updateById(existing.id, updateData);
|
|
14248
14925
|
}
|
|
14249
14926
|
}
|
|
14250
14927
|
}
|
|
14251
14928
|
async function syncPermissions(configs, existingByName) {
|
|
14252
|
-
for (const
|
|
14253
|
-
const existing = existingByName.get(
|
|
14929
|
+
for (const config5 of configs) {
|
|
14930
|
+
const existing = existingByName.get(config5.name);
|
|
14254
14931
|
if (!existing) {
|
|
14255
14932
|
await permissionsRepository.create({
|
|
14256
|
-
name:
|
|
14257
|
-
displayName:
|
|
14258
|
-
description:
|
|
14259
|
-
category:
|
|
14260
|
-
isSystem:
|
|
14261
|
-
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,
|
|
14262
14939
|
isActive: true,
|
|
14263
14940
|
metadata: null
|
|
14264
14941
|
});
|
|
14265
|
-
authLogger.service.info(` \u2705 Created permission: ${
|
|
14942
|
+
authLogger.service.info(` \u2705 Created permission: ${config5.name}`);
|
|
14266
14943
|
} else {
|
|
14267
14944
|
await permissionsRepository.updateById(existing.id, {
|
|
14268
|
-
displayName:
|
|
14269
|
-
description:
|
|
14270
|
-
category:
|
|
14945
|
+
displayName: config5.displayName,
|
|
14946
|
+
description: config5.description || null,
|
|
14947
|
+
category: config5.category || null
|
|
14271
14948
|
});
|
|
14272
14949
|
}
|
|
14273
14950
|
}
|
|
@@ -14329,7 +15006,7 @@ async function getUserPermissions(userId) {
|
|
|
14329
15006
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
14330
15007
|
if (permIds.length > 0) {
|
|
14331
15008
|
const rolePerms = await Promise.all(
|
|
14332
|
-
permIds.map((
|
|
15009
|
+
permIds.map((id27) => permissionsRepository.findById(id27))
|
|
14333
15010
|
);
|
|
14334
15011
|
for (const perm of rolePerms) {
|
|
14335
15012
|
if (perm && perm.isActive) {
|
|
@@ -14547,20 +15224,20 @@ async function acceptInvitation(params) {
|
|
|
14547
15224
|
async function listInvitations(params) {
|
|
14548
15225
|
return await invitationsRepository.list(params);
|
|
14549
15226
|
}
|
|
14550
|
-
async function cancelInvitation(
|
|
14551
|
-
const invitation = await invitationsRepository.findById(
|
|
15227
|
+
async function cancelInvitation(id27, cancelledBy, reason) {
|
|
15228
|
+
const invitation = await invitationsRepository.findById(id27);
|
|
14552
15229
|
if (!invitation) {
|
|
14553
15230
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
14554
15231
|
}
|
|
14555
15232
|
if (invitation.status !== "pending") {
|
|
14556
15233
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
14557
15234
|
}
|
|
14558
|
-
await invitationsRepository.cancel(
|
|
15235
|
+
await invitationsRepository.cancel(id27, cancelledBy, reason, invitation.metadata);
|
|
14559
15236
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
14560
15237
|
}
|
|
14561
|
-
async function deleteInvitation(
|
|
14562
|
-
await invitationsRepository.deleteById(
|
|
14563
|
-
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}`);
|
|
14564
15241
|
}
|
|
14565
15242
|
async function expireOldInvitations() {
|
|
14566
15243
|
const count2 = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -14569,8 +15246,8 @@ async function expireOldInvitations() {
|
|
|
14569
15246
|
}
|
|
14570
15247
|
return count2;
|
|
14571
15248
|
}
|
|
14572
|
-
async function resendInvitation(
|
|
14573
|
-
const invitation = await invitationsRepository.findById(
|
|
15249
|
+
async function resendInvitation(id27, expiresInDays = 7) {
|
|
15250
|
+
const invitation = await invitationsRepository.findById(id27);
|
|
14574
15251
|
if (!invitation) {
|
|
14575
15252
|
throw new NotFoundError4({ message: "Invitation not found", resource: "Invitation" });
|
|
14576
15253
|
}
|
|
@@ -14578,7 +15255,7 @@ async function resendInvitation(id26, expiresInDays = 7) {
|
|
|
14578
15255
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
14579
15256
|
}
|
|
14580
15257
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
14581
|
-
const updated = await invitationsRepository.resend(
|
|
15258
|
+
const updated = await invitationsRepository.resend(id27, newExpiresAt);
|
|
14582
15259
|
if (!updated) {
|
|
14583
15260
|
throw new Error("Failed to update invitation");
|
|
14584
15261
|
}
|
|
@@ -14617,13 +15294,13 @@ async function getAuthSessionService(userId) {
|
|
|
14617
15294
|
// src/server/lib/one-time-token.ts
|
|
14618
15295
|
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
14619
15296
|
var manager = null;
|
|
14620
|
-
function initOneTimeTokenManager(
|
|
15297
|
+
function initOneTimeTokenManager(config5) {
|
|
14621
15298
|
if (manager) {
|
|
14622
15299
|
manager.destroy();
|
|
14623
15300
|
}
|
|
14624
15301
|
manager = new SSETokenManager({
|
|
14625
|
-
ttl:
|
|
14626
|
-
store:
|
|
15302
|
+
ttl: config5?.ttl,
|
|
15303
|
+
store: config5?.store
|
|
14627
15304
|
});
|
|
14628
15305
|
}
|
|
14629
15306
|
function getOneTimeTokenManager() {
|
|
@@ -14773,10 +15450,10 @@ function getDefaultScopes() {
|
|
|
14773
15450
|
}
|
|
14774
15451
|
function getGoogleAuthUrl(state, scopes) {
|
|
14775
15452
|
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
14776
|
-
const
|
|
15453
|
+
const config5 = getGoogleOAuthConfig();
|
|
14777
15454
|
const params = new URLSearchParams({
|
|
14778
|
-
client_id:
|
|
14779
|
-
redirect_uri:
|
|
15455
|
+
client_id: config5.clientId,
|
|
15456
|
+
redirect_uri: config5.redirectUri,
|
|
14780
15457
|
response_type: "code",
|
|
14781
15458
|
scope: resolvedScopes.join(" "),
|
|
14782
15459
|
state,
|
|
@@ -14788,16 +15465,16 @@ function getGoogleAuthUrl(state, scopes) {
|
|
|
14788
15465
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
14789
15466
|
}
|
|
14790
15467
|
async function exchangeCodeForTokens(code) {
|
|
14791
|
-
const
|
|
15468
|
+
const config5 = getGoogleOAuthConfig();
|
|
14792
15469
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
14793
15470
|
method: "POST",
|
|
14794
15471
|
headers: {
|
|
14795
15472
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
14796
15473
|
},
|
|
14797
15474
|
body: new URLSearchParams({
|
|
14798
|
-
client_id:
|
|
14799
|
-
client_secret:
|
|
14800
|
-
redirect_uri:
|
|
15475
|
+
client_id: config5.clientId,
|
|
15476
|
+
client_secret: config5.clientSecret,
|
|
15477
|
+
redirect_uri: config5.redirectUri,
|
|
14801
15478
|
grant_type: "authorization_code",
|
|
14802
15479
|
code
|
|
14803
15480
|
})
|
|
@@ -14821,15 +15498,15 @@ async function getGoogleUserInfo(accessToken) {
|
|
|
14821
15498
|
return response.json();
|
|
14822
15499
|
}
|
|
14823
15500
|
async function refreshAccessToken(refreshToken) {
|
|
14824
|
-
const
|
|
15501
|
+
const config5 = getGoogleOAuthConfig();
|
|
14825
15502
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
14826
15503
|
method: "POST",
|
|
14827
15504
|
headers: {
|
|
14828
15505
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
14829
15506
|
},
|
|
14830
15507
|
body: new URLSearchParams({
|
|
14831
|
-
client_id:
|
|
14832
|
-
client_secret:
|
|
15508
|
+
client_id: config5.clientId,
|
|
15509
|
+
client_secret: config5.clientSecret,
|
|
14833
15510
|
refresh_token: refreshToken,
|
|
14834
15511
|
grant_type: "refresh_token"
|
|
14835
15512
|
})
|
|
@@ -14894,8 +15571,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
14894
15571
|
function registerOAuthProvider(provider) {
|
|
14895
15572
|
registry2.set(provider.id, provider);
|
|
14896
15573
|
}
|
|
14897
|
-
function getOAuthProvider(
|
|
14898
|
-
return registry2.get(
|
|
15574
|
+
function getOAuthProvider(id27) {
|
|
15575
|
+
return registry2.get(id27);
|
|
14899
15576
|
}
|
|
14900
15577
|
function getRegisteredProviders() {
|
|
14901
15578
|
return [...registry2.values()];
|
|
@@ -15150,21 +15827,21 @@ var githubProvider = {
|
|
|
15150
15827
|
return !!(env4.SPFN_AUTH_GITHUB_CLIENT_ID && env4.SPFN_AUTH_GITHUB_CLIENT_SECRET);
|
|
15151
15828
|
},
|
|
15152
15829
|
getAuthUrl(state, scopes) {
|
|
15153
|
-
const
|
|
15830
|
+
const config5 = getGithubConfig();
|
|
15154
15831
|
const params = new URLSearchParams({
|
|
15155
|
-
client_id:
|
|
15156
|
-
redirect_uri:
|
|
15832
|
+
client_id: config5.clientId,
|
|
15833
|
+
redirect_uri: config5.redirectUri,
|
|
15157
15834
|
state,
|
|
15158
15835
|
scope: (scopes ?? getGithubScopes()).join(" ")
|
|
15159
15836
|
});
|
|
15160
15837
|
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
15161
15838
|
},
|
|
15162
15839
|
async exchangeCodeForTokens(code) {
|
|
15163
|
-
const
|
|
15840
|
+
const config5 = getGithubConfig();
|
|
15164
15841
|
return requestGithubTokens(new URLSearchParams({
|
|
15165
|
-
client_id:
|
|
15166
|
-
client_secret:
|
|
15167
|
-
redirect_uri:
|
|
15842
|
+
client_id: config5.clientId,
|
|
15843
|
+
client_secret: config5.clientSecret,
|
|
15844
|
+
redirect_uri: config5.redirectUri,
|
|
15168
15845
|
code
|
|
15169
15846
|
}));
|
|
15170
15847
|
},
|
|
@@ -15194,11 +15871,11 @@ var githubProvider = {
|
|
|
15194
15871
|
};
|
|
15195
15872
|
},
|
|
15196
15873
|
async refreshTokens(refreshToken) {
|
|
15197
|
-
const
|
|
15874
|
+
const config5 = getGithubConfig();
|
|
15198
15875
|
return requestGithubTokens(new URLSearchParams({
|
|
15199
15876
|
grant_type: "refresh_token",
|
|
15200
|
-
client_id:
|
|
15201
|
-
client_secret:
|
|
15877
|
+
client_id: config5.clientId,
|
|
15878
|
+
client_secret: config5.clientSecret,
|
|
15202
15879
|
refresh_token: refreshToken
|
|
15203
15880
|
}));
|
|
15204
15881
|
}
|
|
@@ -15322,26 +15999,26 @@ var kakaoProvider = {
|
|
|
15322
15999
|
return !!env4.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
15323
16000
|
},
|
|
15324
16001
|
getAuthUrl(state, scopes) {
|
|
15325
|
-
const
|
|
16002
|
+
const config5 = getKakaoConfig();
|
|
15326
16003
|
const params = new URLSearchParams({
|
|
15327
16004
|
response_type: "code",
|
|
15328
|
-
client_id:
|
|
15329
|
-
redirect_uri:
|
|
16005
|
+
client_id: config5.clientId,
|
|
16006
|
+
redirect_uri: config5.redirectUri,
|
|
15330
16007
|
state,
|
|
15331
16008
|
scope: (scopes ?? getKakaoScopes()).join(",")
|
|
15332
16009
|
});
|
|
15333
16010
|
return `${KAKAO_AUTH_URL}?${params.toString()}`;
|
|
15334
16011
|
},
|
|
15335
16012
|
async exchangeCodeForTokens(code) {
|
|
15336
|
-
const
|
|
16013
|
+
const config5 = getKakaoConfig();
|
|
15337
16014
|
const params = new URLSearchParams({
|
|
15338
16015
|
grant_type: "authorization_code",
|
|
15339
|
-
client_id:
|
|
15340
|
-
redirect_uri:
|
|
16016
|
+
client_id: config5.clientId,
|
|
16017
|
+
redirect_uri: config5.redirectUri,
|
|
15341
16018
|
code
|
|
15342
16019
|
});
|
|
15343
|
-
if (
|
|
15344
|
-
params.set("client_secret",
|
|
16020
|
+
if (config5.clientSecret) {
|
|
16021
|
+
params.set("client_secret", config5.clientSecret);
|
|
15345
16022
|
}
|
|
15346
16023
|
return requestKakaoTokens(params);
|
|
15347
16024
|
},
|
|
@@ -15383,14 +16060,14 @@ var kakaoProvider = {
|
|
|
15383
16060
|
return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
|
|
15384
16061
|
},
|
|
15385
16062
|
async refreshTokens(refreshToken) {
|
|
15386
|
-
const
|
|
16063
|
+
const config5 = getKakaoConfig();
|
|
15387
16064
|
const params = new URLSearchParams({
|
|
15388
16065
|
grant_type: "refresh_token",
|
|
15389
|
-
client_id:
|
|
16066
|
+
client_id: config5.clientId,
|
|
15390
16067
|
refresh_token: refreshToken
|
|
15391
16068
|
});
|
|
15392
|
-
if (
|
|
15393
|
-
params.set("client_secret",
|
|
16069
|
+
if (config5.clientSecret) {
|
|
16070
|
+
params.set("client_secret", config5.clientSecret);
|
|
15394
16071
|
}
|
|
15395
16072
|
return requestKakaoTokens(params);
|
|
15396
16073
|
},
|
|
@@ -15549,22 +16226,22 @@ var naverProvider = {
|
|
|
15549
16226
|
return !!(env4.SPFN_AUTH_NAVER_CLIENT_ID && env4.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
15550
16227
|
},
|
|
15551
16228
|
getAuthUrl(state) {
|
|
15552
|
-
const
|
|
16229
|
+
const config5 = getNaverConfig();
|
|
15553
16230
|
const params = new URLSearchParams({
|
|
15554
16231
|
response_type: "code",
|
|
15555
|
-
client_id:
|
|
15556
|
-
redirect_uri:
|
|
16232
|
+
client_id: config5.clientId,
|
|
16233
|
+
redirect_uri: config5.redirectUri,
|
|
15557
16234
|
state
|
|
15558
16235
|
});
|
|
15559
16236
|
return `${NAVER_AUTH_URL}?${params.toString()}`;
|
|
15560
16237
|
},
|
|
15561
16238
|
async exchangeCodeForTokens(code, options) {
|
|
15562
|
-
const
|
|
16239
|
+
const config5 = getNaverConfig();
|
|
15563
16240
|
return requestNaverTokens(new URLSearchParams({
|
|
15564
16241
|
grant_type: "authorization_code",
|
|
15565
|
-
client_id:
|
|
15566
|
-
client_secret:
|
|
15567
|
-
redirect_uri:
|
|
16242
|
+
client_id: config5.clientId,
|
|
16243
|
+
client_secret: config5.clientSecret,
|
|
16244
|
+
redirect_uri: config5.redirectUri,
|
|
15568
16245
|
code,
|
|
15569
16246
|
state: options.state
|
|
15570
16247
|
}));
|
|
@@ -15605,11 +16282,11 @@ var naverProvider = {
|
|
|
15605
16282
|
return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
|
|
15606
16283
|
},
|
|
15607
16284
|
async refreshTokens(refreshToken) {
|
|
15608
|
-
const
|
|
16285
|
+
const config5 = getNaverConfig();
|
|
15609
16286
|
return requestNaverTokens(new URLSearchParams({
|
|
15610
16287
|
grant_type: "refresh_token",
|
|
15611
|
-
client_id:
|
|
15612
|
-
client_secret:
|
|
16288
|
+
client_id: config5.clientId,
|
|
16289
|
+
client_secret: config5.clientSecret,
|
|
15613
16290
|
refresh_token: refreshToken
|
|
15614
16291
|
}));
|
|
15615
16292
|
},
|
|
@@ -15970,7 +16647,7 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
15970
16647
|
// src/server/services/oauth-native.service.ts
|
|
15971
16648
|
import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
|
|
15972
16649
|
import {
|
|
15973
|
-
InvalidKeyFingerprintError as
|
|
16650
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError4,
|
|
15974
16651
|
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
15975
16652
|
NonceKeyBindingError
|
|
15976
16653
|
} from "@spfn/auth/errors";
|
|
@@ -15997,7 +16674,7 @@ function assertNonceBindsPublicKey(params) {
|
|
|
15997
16674
|
throw new NonceKeyBindingError();
|
|
15998
16675
|
}
|
|
15999
16676
|
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
16000
|
-
throw new
|
|
16677
|
+
throw new InvalidKeyFingerprintError4();
|
|
16001
16678
|
}
|
|
16002
16679
|
}
|
|
16003
16680
|
async function persistNativeLogin(identity, params) {
|
|
@@ -16091,8 +16768,8 @@ async function verifyOpsTokenService(token) {
|
|
|
16091
16768
|
scopes: record.scopes
|
|
16092
16769
|
};
|
|
16093
16770
|
}
|
|
16094
|
-
async function revokeOpsTokenService(
|
|
16095
|
-
return await opsTokensRepository.revokeById(
|
|
16771
|
+
async function revokeOpsTokenService(id27) {
|
|
16772
|
+
return await opsTokensRepository.revokeById(id27);
|
|
16096
16773
|
}
|
|
16097
16774
|
async function listOpsTokensService() {
|
|
16098
16775
|
return await opsTokensRepository.list();
|
|
@@ -16108,7 +16785,7 @@ var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
|
16108
16785
|
var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
16109
16786
|
var DEFAULT_CODE_TTL_MS = 60 * 1e3;
|
|
16110
16787
|
var AUTHORIZE_PATH = "/oauth/authorize";
|
|
16111
|
-
var
|
|
16788
|
+
var config4 = null;
|
|
16112
16789
|
function resolveIssuerSource(env21 = process.env) {
|
|
16113
16790
|
return { value: env21.SPFN_API_URL, variable: "SPFN_API_URL" };
|
|
16114
16791
|
}
|
|
@@ -16118,7 +16795,7 @@ function resolveAuthorizeUrl(env21) {
|
|
|
16118
16795
|
}
|
|
16119
16796
|
function configureAuthorizationServer(options, env21 = process.env) {
|
|
16120
16797
|
if (!options) {
|
|
16121
|
-
|
|
16798
|
+
config4 = null;
|
|
16122
16799
|
return;
|
|
16123
16800
|
}
|
|
16124
16801
|
const scopeNames = Object.keys(options.scopes ?? {});
|
|
@@ -16127,7 +16804,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
|
|
|
16127
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."
|
|
16128
16805
|
);
|
|
16129
16806
|
}
|
|
16130
|
-
|
|
16807
|
+
config4 = {
|
|
16131
16808
|
issuer: canonicalIssuer(options.issuer ?? resolveIssuerSource(env21).value ?? ""),
|
|
16132
16809
|
issuerSource: options.issuer ? "authorizationServer.issuer" : resolveIssuerSource(env21).variable,
|
|
16133
16810
|
authorizeUrl: options.authorizeUrl ?? resolveAuthorizeUrl(env21),
|
|
@@ -16138,7 +16815,7 @@ function configureAuthorizationServer(options, env21 = process.env) {
|
|
|
16138
16815
|
refreshTokenTtlMs: options.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
16139
16816
|
codeTtlMs: options.codeTtlMs ?? DEFAULT_CODE_TTL_MS
|
|
16140
16817
|
};
|
|
16141
|
-
assertKnownDefaultScopes(
|
|
16818
|
+
assertKnownDefaultScopes(config4);
|
|
16142
16819
|
}
|
|
16143
16820
|
function assertKnownDefaultScopes(resolved) {
|
|
16144
16821
|
const unknown = resolved.defaultScopes.filter((scope) => !(scope in resolved.scopes));
|
|
@@ -16149,7 +16826,7 @@ function assertKnownDefaultScopes(resolved) {
|
|
|
16149
16826
|
}
|
|
16150
16827
|
}
|
|
16151
16828
|
function getAuthorizationServerConfig() {
|
|
16152
|
-
return
|
|
16829
|
+
return config4;
|
|
16153
16830
|
}
|
|
16154
16831
|
function isLoopbackHostname(hostname) {
|
|
16155
16832
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
@@ -16372,15 +17049,15 @@ var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
|
16372
17049
|
var SUPPORTED_AUTH_METHOD = "none";
|
|
16373
17050
|
var SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
|
|
16374
17051
|
var SUPPORTED_RESPONSE_TYPES = ["code"];
|
|
16375
|
-
function
|
|
17052
|
+
function refuse2(status, error, description) {
|
|
16376
17053
|
return { ok: false, status, error, description };
|
|
16377
17054
|
}
|
|
16378
17055
|
function requireConfig() {
|
|
16379
|
-
const
|
|
16380
|
-
if (!
|
|
17056
|
+
const config5 = getAuthorizationServerConfig();
|
|
17057
|
+
if (!config5) {
|
|
16381
17058
|
throw new Error("OAuth2 client service called with no authorization server configured.");
|
|
16382
17059
|
}
|
|
16383
|
-
return
|
|
17060
|
+
return config5;
|
|
16384
17061
|
}
|
|
16385
17062
|
function asStringArray(value) {
|
|
16386
17063
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
@@ -16391,7 +17068,7 @@ function asStringArray(value) {
|
|
|
16391
17068
|
function refuseDeclaredMetadata(request) {
|
|
16392
17069
|
const { token_endpoint_auth_method: authMethod } = request;
|
|
16393
17070
|
if (authMethod !== void 0 && authMethod !== SUPPORTED_AUTH_METHOD) {
|
|
16394
|
-
return
|
|
17071
|
+
return refuse2(
|
|
16395
17072
|
400,
|
|
16396
17073
|
"invalid_client_metadata",
|
|
16397
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.`
|
|
@@ -16405,7 +17082,7 @@ function refuseListedMetadata(field, value, supported) {
|
|
|
16405
17082
|
}
|
|
16406
17083
|
const declared = asStringArray(value);
|
|
16407
17084
|
if (!declared || declared.length === 0 || declared.some((entry) => !supported.includes(entry))) {
|
|
16408
|
-
return
|
|
17085
|
+
return refuse2(
|
|
16409
17086
|
400,
|
|
16410
17087
|
"invalid_client_metadata",
|
|
16411
17088
|
`${field} must be a non-empty subset of ${supported.join(", ")}.`
|
|
@@ -16416,7 +17093,7 @@ function refuseListedMetadata(field, value, supported) {
|
|
|
16416
17093
|
function refuseRedirectUris(uris, allowedRedirectOrigins) {
|
|
16417
17094
|
const declared = asStringArray(uris);
|
|
16418
17095
|
if (!declared || declared.length === 0) {
|
|
16419
|
-
return
|
|
17096
|
+
return refuse2(
|
|
16420
17097
|
400,
|
|
16421
17098
|
"invalid_client_metadata",
|
|
16422
17099
|
"redirect_uris must list at least one absolute URI. There is nowhere to send an authorization code without one."
|
|
@@ -16425,20 +17102,20 @@ function refuseRedirectUris(uris, allowedRedirectOrigins) {
|
|
|
16425
17102
|
for (const uri of declared) {
|
|
16426
17103
|
const detail = refuseRedirectUriRegistration(uri, allowedRedirectOrigins);
|
|
16427
17104
|
if (detail) {
|
|
16428
|
-
return
|
|
17105
|
+
return refuse2(400, "invalid_redirect_uri", detail);
|
|
16429
17106
|
}
|
|
16430
17107
|
}
|
|
16431
17108
|
return null;
|
|
16432
17109
|
}
|
|
16433
17110
|
async function registerOAuth2ClientService(request, clientIp) {
|
|
16434
|
-
const
|
|
16435
|
-
const refusal = refuseRedirectUris(request.redirect_uris,
|
|
17111
|
+
const config5 = requireConfig();
|
|
17112
|
+
const refusal = refuseRedirectUris(request.redirect_uris, config5.allowedRedirectOrigins) ?? refuseDeclaredMetadata(request);
|
|
16436
17113
|
if (refusal) {
|
|
16437
17114
|
return refusal;
|
|
16438
17115
|
}
|
|
16439
17116
|
const record = await createUnderStandingCap(newClientRow(request, clientIp), clientIp);
|
|
16440
17117
|
if (!record) {
|
|
16441
|
-
return
|
|
17118
|
+
return refuse2(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
|
|
16442
17119
|
}
|
|
16443
17120
|
authLogger.service.info("OAuth2 client registered", { clientName: record.clientName });
|
|
16444
17121
|
return { ok: true, client: describeClient(record) };
|
|
@@ -16517,11 +17194,11 @@ function sameResource(presented, granted) {
|
|
|
16517
17194
|
|
|
16518
17195
|
// src/server/services/oauth2-authorize.service.ts
|
|
16519
17196
|
function requireConfig2() {
|
|
16520
|
-
const
|
|
16521
|
-
if (!
|
|
17197
|
+
const config5 = getAuthorizationServerConfig();
|
|
17198
|
+
if (!config5) {
|
|
16522
17199
|
throw new Error("OAuth2 authorize service called with no authorization server configured.");
|
|
16523
17200
|
}
|
|
16524
|
-
return
|
|
17201
|
+
return config5;
|
|
16525
17202
|
}
|
|
16526
17203
|
async function resolveRedirectTarget(params) {
|
|
16527
17204
|
const client = await oauth2ClientsRepository.findByClientId(params.clientId);
|
|
@@ -16533,10 +17210,10 @@ async function resolveRedirectTarget(params) {
|
|
|
16533
17210
|
}
|
|
16534
17211
|
return { client, redirectUri: params.redirectUri };
|
|
16535
17212
|
}
|
|
16536
|
-
function resolveScopes(params,
|
|
17213
|
+
function resolveScopes(params, config5) {
|
|
16537
17214
|
const requested = params.scope?.trim();
|
|
16538
17215
|
if (!requested) {
|
|
16539
|
-
return
|
|
17216
|
+
return config5.defaultScopes;
|
|
16540
17217
|
}
|
|
16541
17218
|
return requested.split(/\s+/);
|
|
16542
17219
|
}
|
|
@@ -16548,7 +17225,7 @@ function refuseRedirectable(params, redirectUri, error, message) {
|
|
|
16548
17225
|
function hasUsableS256Challenge(params) {
|
|
16549
17226
|
return params.codeChallengeMethod === "S256" && !!params.codeChallenge && isPkceS256ChallengeShaped(params.codeChallenge);
|
|
16550
17227
|
}
|
|
16551
|
-
function assertRedirectableRules(params, redirectUri,
|
|
17228
|
+
function assertRedirectableRules(params, redirectUri, config5) {
|
|
16552
17229
|
if (!hasUsableS256Challenge(params)) {
|
|
16553
17230
|
refuseRedirectable(params, redirectUri, "invalid_request", PKCE_REQUIRED_MESSAGE);
|
|
16554
17231
|
}
|
|
@@ -16556,31 +17233,31 @@ function assertRedirectableRules(params, redirectUri, config4) {
|
|
|
16556
17233
|
if (!resource) {
|
|
16557
17234
|
refuseRedirectable(params, redirectUri, "invalid_target", RESOURCE_REQUIRED_MESSAGE);
|
|
16558
17235
|
}
|
|
16559
|
-
const scopes = resolveScopes(params,
|
|
16560
|
-
const unknown = scopes.filter((scope) => !(scope in
|
|
17236
|
+
const scopes = resolveScopes(params, config5);
|
|
17237
|
+
const unknown = scopes.filter((scope) => !(scope in config5.scopes));
|
|
16561
17238
|
if (unknown.length > 0) {
|
|
16562
17239
|
refuseRedirectable(params, redirectUri, "invalid_scope", `Unknown scope: ${unknown.join(", ")}.`);
|
|
16563
17240
|
}
|
|
16564
17241
|
return { resource, scopes, codeChallenge: params.codeChallenge };
|
|
16565
17242
|
}
|
|
16566
17243
|
async function validate(params) {
|
|
16567
|
-
const
|
|
17244
|
+
const config5 = requireConfig2();
|
|
16568
17245
|
const { client, redirectUri } = await resolveRedirectTarget(params);
|
|
16569
|
-
const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri,
|
|
17246
|
+
const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config5);
|
|
16570
17247
|
return { client, redirectUri, resource, scopes, codeChallenge, state: params.state };
|
|
16571
17248
|
}
|
|
16572
17249
|
async function describeOAuth2AuthorizeRequestService(params) {
|
|
16573
|
-
const
|
|
17250
|
+
const config5 = requireConfig2();
|
|
16574
17251
|
const validated = await validate(params);
|
|
16575
17252
|
return {
|
|
16576
17253
|
clientName: validated.client.clientName,
|
|
16577
17254
|
redirectHost: redirectHostOf(validated.redirectUri),
|
|
16578
|
-
scopes: validated.scopes.map((name) => ({ name, description:
|
|
17255
|
+
scopes: validated.scopes.map((name) => ({ name, description: config5.scopes[name] })),
|
|
16579
17256
|
resource: validated.resource
|
|
16580
17257
|
};
|
|
16581
17258
|
}
|
|
16582
17259
|
async function approveOAuth2AuthorizeService(params, userId) {
|
|
16583
|
-
const
|
|
17260
|
+
const config5 = requireConfig2();
|
|
16584
17261
|
const validated = await validate(params);
|
|
16585
17262
|
const grant = await oauth2GrantsRepository.upsert({
|
|
16586
17263
|
client: validated.client.id,
|
|
@@ -16594,7 +17271,7 @@ async function approveOAuth2AuthorizeService(params, userId) {
|
|
|
16594
17271
|
grant: grant.id,
|
|
16595
17272
|
redirectUri: validated.redirectUri,
|
|
16596
17273
|
codeChallenge: validated.codeChallenge,
|
|
16597
|
-
expiresAt: new Date(Date.now() +
|
|
17274
|
+
expiresAt: new Date(Date.now() + config5.codeTtlMs)
|
|
16598
17275
|
});
|
|
16599
17276
|
return { code, redirectUri: validated.redirectUri, state: validated.state };
|
|
16600
17277
|
}
|
|
@@ -16622,15 +17299,15 @@ function invalidGrant() {
|
|
|
16622
17299
|
description: "The authorization code or refresh token is invalid, expired, already used, or was issued to another client."
|
|
16623
17300
|
};
|
|
16624
17301
|
}
|
|
16625
|
-
function
|
|
17302
|
+
function refuse3(error, description) {
|
|
16626
17303
|
return { ok: false, error, description };
|
|
16627
17304
|
}
|
|
16628
17305
|
function requireConfig3() {
|
|
16629
|
-
const
|
|
16630
|
-
if (!
|
|
17306
|
+
const config5 = getAuthorizationServerConfig();
|
|
17307
|
+
if (!config5) {
|
|
16631
17308
|
throw new Error("OAuth2 token service called with no authorization server configured.");
|
|
16632
17309
|
}
|
|
16633
|
-
return
|
|
17310
|
+
return config5;
|
|
16634
17311
|
}
|
|
16635
17312
|
async function oauth2TokenService(request) {
|
|
16636
17313
|
if (request.grant_type === "authorization_code") {
|
|
@@ -16639,14 +17316,14 @@ async function oauth2TokenService(request) {
|
|
|
16639
17316
|
if (request.grant_type === "refresh_token") {
|
|
16640
17317
|
return await refreshTokens(request);
|
|
16641
17318
|
}
|
|
16642
|
-
return
|
|
17319
|
+
return refuse3(
|
|
16643
17320
|
"unsupported_grant_type",
|
|
16644
17321
|
"grant_type must be authorization_code or refresh_token."
|
|
16645
17322
|
);
|
|
16646
17323
|
}
|
|
16647
17324
|
async function exchangeAuthorizationCode(request) {
|
|
16648
17325
|
if (!request.code || !request.code_verifier || !request.client_id || !request.redirect_uri) {
|
|
16649
|
-
return
|
|
17326
|
+
return refuse3(
|
|
16650
17327
|
"invalid_request",
|
|
16651
17328
|
"authorization_code requires code, code_verifier, client_id and redirect_uri."
|
|
16652
17329
|
);
|
|
@@ -16680,13 +17357,13 @@ async function spendBoundCode(request, record, pair) {
|
|
|
16680
17357
|
return invalidGrant();
|
|
16681
17358
|
}
|
|
16682
17359
|
if (resolveResource(request.resource, pair.grant) === null) {
|
|
16683
|
-
return
|
|
17360
|
+
return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
16684
17361
|
}
|
|
16685
17362
|
return { ok: true, tokens: await issueTokenPair(pair.grant, pair.grant.scopes) };
|
|
16686
17363
|
}
|
|
16687
17364
|
async function refreshTokens(request) {
|
|
16688
17365
|
if (!request.refresh_token || !request.client_id) {
|
|
16689
|
-
return
|
|
17366
|
+
return refuse3("invalid_request", "refresh_token requires refresh_token and client_id.");
|
|
16690
17367
|
}
|
|
16691
17368
|
const tokenHash = hashOAuth2Secret(request.refresh_token);
|
|
16692
17369
|
const presented = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
@@ -16708,11 +17385,11 @@ async function rotateRefresh(request, tokenHash, grantId, presentedScopes) {
|
|
|
16708
17385
|
return invalidGrant();
|
|
16709
17386
|
}
|
|
16710
17387
|
if (resolveResource(request.resource, pair.grant) === null) {
|
|
16711
|
-
return
|
|
17388
|
+
return refuse3("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
16712
17389
|
}
|
|
16713
17390
|
const scopes = resolveRefreshScopes(request.scope, pair.grant, presentedScopes);
|
|
16714
17391
|
if (!scopes) {
|
|
16715
|
-
return
|
|
17392
|
+
return refuse3("invalid_scope", "A refresh may ask for a subset of the granted scopes, never more.");
|
|
16716
17393
|
}
|
|
16717
17394
|
if (!await oauth2TokensRepository.rotate(tokenHash)) {
|
|
16718
17395
|
return await refuseLostRotation(tokenHash, grantId);
|
|
@@ -16741,10 +17418,10 @@ function resolveResource(requested, grant) {
|
|
|
16741
17418
|
return sameResource(requested, grant.resource) ? grant.resource : null;
|
|
16742
17419
|
}
|
|
16743
17420
|
async function issueTokenPair(grant, scopes) {
|
|
16744
|
-
const
|
|
17421
|
+
const config5 = requireConfig3();
|
|
16745
17422
|
const accessToken = generateAccessToken();
|
|
16746
17423
|
const refreshToken = generateRefreshToken();
|
|
16747
|
-
const expiresAt = new Date(Date.now() +
|
|
17424
|
+
const expiresAt = new Date(Date.now() + config5.accessTokenTtlMs);
|
|
16748
17425
|
await runInTransaction6(async () => {
|
|
16749
17426
|
await storeToken(accessToken, "access", grant.id, scopes, expiresAt);
|
|
16750
17427
|
await storeToken(
|
|
@@ -16752,7 +17429,7 @@ async function issueTokenPair(grant, scopes) {
|
|
|
16752
17429
|
"refresh",
|
|
16753
17430
|
grant.id,
|
|
16754
17431
|
scopes,
|
|
16755
|
-
new Date(Date.now() +
|
|
17432
|
+
new Date(Date.now() + config5.refreshTokenTtlMs)
|
|
16756
17433
|
);
|
|
16757
17434
|
});
|
|
16758
17435
|
oauth2ClientsRepository.updateLastUsedById(grant.client).catch((err) => authLogger.service.error("Failed to update OAuth2 client lastUsedAt", err));
|
|
@@ -17100,6 +17777,47 @@ function waitTarget(body) {
|
|
|
17100
17777
|
return { deviceCode, waitMillis };
|
|
17101
17778
|
}
|
|
17102
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
|
+
|
|
17103
17821
|
// src/server/routes/auth/index.ts
|
|
17104
17822
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
17105
17823
|
body: Type.Object({
|
|
@@ -17311,6 +18029,101 @@ var denyDeviceAuth = route.post("/_auth/device/deny").input({
|
|
|
17311
18029
|
await denyDeviceAuthService(body);
|
|
17312
18030
|
return c.noContent();
|
|
17313
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
|
+
});
|
|
17314
18127
|
var logout = route.post("/_auth/logout").handler(async (c) => {
|
|
17315
18128
|
const auth = getAuth(c);
|
|
17316
18129
|
if (!auth) {
|
|
@@ -17426,6 +18239,13 @@ var authRouter = defineRouter({
|
|
|
17426
18239
|
getDeviceAuthInfo,
|
|
17427
18240
|
approveDeviceAuth,
|
|
17428
18241
|
denyDeviceAuth,
|
|
18242
|
+
issueDeviceLink,
|
|
18243
|
+
redeemDeviceLink,
|
|
18244
|
+
getDeviceLinkStatus,
|
|
18245
|
+
confirmDeviceLink,
|
|
18246
|
+
denyDeviceLink,
|
|
18247
|
+
cancelDeviceLink,
|
|
18248
|
+
pollDeviceLink,
|
|
17429
18249
|
logout,
|
|
17430
18250
|
rotateKey,
|
|
17431
18251
|
listKeys,
|
|
@@ -17733,13 +18553,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
17733
18553
|
var INT64_MIN = -(2n ** 63n);
|
|
17734
18554
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
17735
18555
|
function parseCanonicalJson(bytes) {
|
|
17736
|
-
let
|
|
18556
|
+
let text28;
|
|
17737
18557
|
try {
|
|
17738
|
-
|
|
18558
|
+
text28 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
17739
18559
|
} catch {
|
|
17740
18560
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
17741
18561
|
}
|
|
17742
|
-
const parser = new Parser(
|
|
18562
|
+
const parser = new Parser(text28);
|
|
17743
18563
|
const value = parser.parseValue();
|
|
17744
18564
|
parser.skipWhitespace();
|
|
17745
18565
|
if (!parser.atEnd()) {
|
|
@@ -17760,8 +18580,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
17760
18580
|
return true;
|
|
17761
18581
|
}
|
|
17762
18582
|
var Parser = class {
|
|
17763
|
-
constructor(
|
|
17764
|
-
this.text =
|
|
18583
|
+
constructor(text28) {
|
|
18584
|
+
this.text = text28;
|
|
17765
18585
|
}
|
|
17766
18586
|
pos = 0;
|
|
17767
18587
|
atEnd() {
|
|
@@ -18351,7 +19171,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
|
|
|
18351
19171
|
|
|
18352
19172
|
// src/server/client-proof/contract-bundle.ts
|
|
18353
19173
|
init_wire_headers();
|
|
18354
|
-
var CONTRACT_VERSION = "0.13.
|
|
19174
|
+
var CONTRACT_VERSION = "0.13.2";
|
|
18355
19175
|
var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
|
|
18356
19176
|
function required(name, type) {
|
|
18357
19177
|
return { name, type, optional: false };
|
|
@@ -18747,6 +19567,39 @@ var CONTRACT_TYPES = [
|
|
|
18747
19567
|
fields: [
|
|
18748
19568
|
required("userCode", "string")
|
|
18749
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
|
+
]
|
|
18750
19603
|
}
|
|
18751
19604
|
];
|
|
18752
19605
|
var CONTRACT_ENUMS = [
|
|
@@ -19814,16 +20667,16 @@ function extractBearer2(header) {
|
|
|
19814
20667
|
}
|
|
19815
20668
|
|
|
19816
20669
|
// src/server/middleware/ops-or-user.ts
|
|
19817
|
-
function opsOrUser(
|
|
19818
|
-
const roles2 =
|
|
19819
|
-
const permissions2 =
|
|
19820
|
-
if (!
|
|
20670
|
+
function opsOrUser(config5) {
|
|
20671
|
+
const roles2 = config5.roles ?? [];
|
|
20672
|
+
const permissions2 = config5.permissions ?? [];
|
|
20673
|
+
if (!config5.opsScopes || config5.opsScopes.length === 0) {
|
|
19821
20674
|
throw new Error("opsOrUser: opsScopes must name at least one scope \u2014 an ops token would otherwise be admitted unchecked.");
|
|
19822
20675
|
}
|
|
19823
20676
|
if (roles2.length === 0 && permissions2.length === 0) {
|
|
19824
20677
|
throw new Error("opsOrUser: give roles, permissions, or both \u2014 a user session would otherwise be admitted unchecked.");
|
|
19825
20678
|
}
|
|
19826
|
-
const ops = chain([opsTokenAuth.handler, requireOpsScope(...
|
|
20679
|
+
const ops = chain([opsTokenAuth.handler, requireOpsScope(...config5.opsScopes)]);
|
|
19827
20680
|
const user = chain([
|
|
19828
20681
|
authenticate.handler,
|
|
19829
20682
|
...roles2.length > 0 ? [requireRole(...roles2)] : [],
|
|
@@ -19838,8 +20691,8 @@ function bearerOf(header) {
|
|
|
19838
20691
|
function chain(handlers) {
|
|
19839
20692
|
return async (c, next) => {
|
|
19840
20693
|
let answer;
|
|
19841
|
-
const runFrom = async (
|
|
19842
|
-
const produced =
|
|
20694
|
+
const runFrom = async (index22) => {
|
|
20695
|
+
const produced = index22 < handlers.length ? await handlers[index22](c, () => runFrom(index22 + 1)) : await next();
|
|
19843
20696
|
if (produced instanceof Response) {
|
|
19844
20697
|
answer = produced;
|
|
19845
20698
|
}
|
|
@@ -20866,13 +21719,13 @@ var NO_STORE_HEADERS = {
|
|
|
20866
21719
|
Pragma: "no-cache"
|
|
20867
21720
|
};
|
|
20868
21721
|
function requireAuthorizationServer() {
|
|
20869
|
-
const
|
|
20870
|
-
if (!
|
|
21722
|
+
const config5 = getAuthorizationServerConfig();
|
|
21723
|
+
if (!config5) {
|
|
20871
21724
|
throw new NotFoundError6({
|
|
20872
21725
|
message: "This application does not run an OAuth 2.1 authorization server. Pass `authorizationServer` to createAuthLifecycle() to enable one."
|
|
20873
21726
|
});
|
|
20874
21727
|
}
|
|
20875
|
-
return
|
|
21728
|
+
return config5;
|
|
20876
21729
|
}
|
|
20877
21730
|
function oauth2ErrorResponse(c, status, error, description) {
|
|
20878
21731
|
return c.json({ error, error_description: description }, status, NO_STORE_HEADERS);
|
|
@@ -20943,18 +21796,18 @@ var revokeOAuth2Grant = route14.delete("/_auth/oauth2/grants/:id").input({ param
|
|
|
20943
21796
|
return { revoked: true };
|
|
20944
21797
|
});
|
|
20945
21798
|
var oauth2AuthorizationServerMetadata = route14.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
|
|
20946
|
-
const
|
|
21799
|
+
const config5 = requireAuthorizationServer();
|
|
20947
21800
|
return c.json({
|
|
20948
|
-
issuer:
|
|
20949
|
-
authorization_endpoint:
|
|
20950
|
-
token_endpoint: new URL("/_auth/oauth2/token",
|
|
20951
|
-
registration_endpoint: new URL("/_auth/oauth2/register",
|
|
20952
|
-
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(),
|
|
20953
21806
|
response_types_supported: SUPPORTED_RESPONSE_TYPES,
|
|
20954
21807
|
grant_types_supported: SUPPORTED_GRANT_TYPES,
|
|
20955
21808
|
token_endpoint_auth_methods_supported: ["none"],
|
|
20956
21809
|
code_challenge_methods_supported: ["S256"],
|
|
20957
|
-
scopes_supported: Object.keys(
|
|
21810
|
+
scopes_supported: Object.keys(config5.scopes)
|
|
20958
21811
|
});
|
|
20959
21812
|
});
|
|
20960
21813
|
|
|
@@ -21054,6 +21907,14 @@ var mainAuthRouter = defineRouter6({
|
|
|
21054
21907
|
getDeviceAuthInfo,
|
|
21055
21908
|
approveDeviceAuth,
|
|
21056
21909
|
denyDeviceAuth,
|
|
21910
|
+
// Device link routes
|
|
21911
|
+
issueDeviceLink,
|
|
21912
|
+
redeemDeviceLink,
|
|
21913
|
+
getDeviceLinkStatus,
|
|
21914
|
+
confirmDeviceLink,
|
|
21915
|
+
denyDeviceLink,
|
|
21916
|
+
cancelDeviceLink,
|
|
21917
|
+
pollDeviceLink,
|
|
21057
21918
|
// Passkey routes (WebAuthn)
|
|
21058
21919
|
passkeyRegisterOptions,
|
|
21059
21920
|
passkeyRegisterVerify,
|
|
@@ -21541,6 +22402,7 @@ function assertOAuthRedirectUris(env21 = process.env) {
|
|
|
21541
22402
|
function createAuthLifecycle(options = {}) {
|
|
21542
22403
|
configureDeletion(options.deletion);
|
|
21543
22404
|
configureDeviceAuth(options.deviceAuth);
|
|
22405
|
+
configureDeviceLink(options.deviceLink);
|
|
21544
22406
|
configureAuthorizationServer(options.authorizationServer);
|
|
21545
22407
|
return {
|
|
21546
22408
|
/**
|
|
@@ -21657,11 +22519,14 @@ export {
|
|
|
21657
22519
|
DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
21658
22520
|
DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
|
|
21659
22521
|
DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
22522
|
+
DEFAULT_DEVICE_LINK_TTL_MS,
|
|
21660
22523
|
DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
21661
22524
|
DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,
|
|
21662
22525
|
DEVICE_AUTH_STATUSES,
|
|
22526
|
+
DEVICE_LINK_STATUSES,
|
|
21663
22527
|
DeviceAuthPollResponseSchema,
|
|
21664
22528
|
DeviceAuthorizationsRepository,
|
|
22529
|
+
DeviceLinksRepository,
|
|
21665
22530
|
DeviceNameSchema,
|
|
21666
22531
|
EmailSchema,
|
|
21667
22532
|
EnvironmentKeyringTokenCipher,
|
|
@@ -21675,11 +22540,13 @@ export {
|
|
|
21675
22540
|
KeyIdSchema,
|
|
21676
22541
|
KeyRevokeAllTokensRepository,
|
|
21677
22542
|
KeysRepository,
|
|
22543
|
+
LinkIdSchema,
|
|
21678
22544
|
MAX_UNGRANTED_CLIENTS_PER_IP,
|
|
21679
22545
|
MFA_CHALLENGE_ATTEMPT_LIMIT,
|
|
21680
22546
|
MFA_CHALLENGE_CHANNELS,
|
|
21681
22547
|
MFA_CONFIRM_ATTEMPT_LIMIT,
|
|
21682
22548
|
MFA_VERIFICATION_METHODS,
|
|
22549
|
+
MatchChoiceSchema,
|
|
21683
22550
|
MfaChallengesRepository,
|
|
21684
22551
|
MfaEnrolmentRepository,
|
|
21685
22552
|
MfaRecoveryCodesRepository,
|
|
@@ -21764,6 +22631,7 @@ export {
|
|
|
21764
22631
|
bearerAuthContext,
|
|
21765
22632
|
buildOAuthErrorUrl,
|
|
21766
22633
|
cancelAccountDeletionService,
|
|
22634
|
+
cancelDeviceLinkService,
|
|
21767
22635
|
cancelInvitation,
|
|
21768
22636
|
carryStepUpVerification,
|
|
21769
22637
|
changePasswordService,
|
|
@@ -21774,7 +22642,9 @@ export {
|
|
|
21774
22642
|
configureAuthorizationServer,
|
|
21775
22643
|
configureDeletion,
|
|
21776
22644
|
configureDeviceAuth,
|
|
22645
|
+
configureDeviceLink,
|
|
21777
22646
|
configureOAuthTokenCipher,
|
|
22647
|
+
confirmDeviceLinkService,
|
|
21778
22648
|
confirmPasswordResetService,
|
|
21779
22649
|
confirmSignupLinkService,
|
|
21780
22650
|
confirmTotpEnrolmentService,
|
|
@@ -21793,12 +22663,15 @@ export {
|
|
|
21793
22663
|
deleteInvitation,
|
|
21794
22664
|
deleteRole,
|
|
21795
22665
|
denyDeviceAuthService,
|
|
22666
|
+
denyDeviceLinkService,
|
|
21796
22667
|
denyOAuth2AuthorizeService,
|
|
21797
22668
|
deriveCsrfToken,
|
|
21798
22669
|
describeOAuth2AuthorizeRequestService,
|
|
21799
22670
|
describeRevokeAllLink,
|
|
21800
22671
|
deviceAuthorizations,
|
|
21801
22672
|
deviceAuthorizationsRepository,
|
|
22673
|
+
deviceLinks,
|
|
22674
|
+
deviceLinksRepository,
|
|
21802
22675
|
disableMfaService,
|
|
21803
22676
|
disableSessionBindingService,
|
|
21804
22677
|
enableSessionBindingService,
|
|
@@ -21833,6 +22706,8 @@ export {
|
|
|
21833
22706
|
getDeletionConfig,
|
|
21834
22707
|
getDeviceAuthConfig,
|
|
21835
22708
|
getDeviceAuthInfoService,
|
|
22709
|
+
getDeviceLinkConfig,
|
|
22710
|
+
getDeviceLinkStatusService,
|
|
21836
22711
|
getDummyPasswordHash,
|
|
21837
22712
|
getEnabledOAuthProviders,
|
|
21838
22713
|
getEncryptionKeyring,
|
|
@@ -21893,6 +22768,7 @@ export {
|
|
|
21893
22768
|
isPkceS256ChallengeShaped,
|
|
21894
22769
|
isPkceVerifierShaped,
|
|
21895
22770
|
isSafeReturnPath,
|
|
22771
|
+
issueDeviceLinkService,
|
|
21896
22772
|
issueOneTimeTokenService,
|
|
21897
22773
|
issueOpsTokenService,
|
|
21898
22774
|
kakaoProvider,
|
|
@@ -21960,9 +22836,11 @@ export {
|
|
|
21960
22836
|
permissionsRepository,
|
|
21961
22837
|
pkceChallengeFor,
|
|
21962
22838
|
pollDeviceAuthService,
|
|
22839
|
+
pollDeviceLinkService,
|
|
21963
22840
|
purgeRevokeAllTokensService,
|
|
21964
22841
|
purgeStaleOAuth2ClientsService,
|
|
21965
22842
|
purgeUserService,
|
|
22843
|
+
redeemDeviceLinkService,
|
|
21966
22844
|
redirectHostOf,
|
|
21967
22845
|
refreshAccessToken,
|
|
21968
22846
|
refuseRedirectUriRegistration,
|