@spfn/auth 0.3.0-beta.18 → 0.3.0-beta.19
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 +134 -0
- package/dist/errors.d.ts +74 -2
- package/dist/errors.js +51 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +39 -2
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-nrpFSvvB.d.ts → machine-principals-BD4tnASp.d.ts} +171 -1
- package/dist/nextjs/api.js +6 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +1466 -51
- package/dist/server.js +1862 -398
- package/dist/server.js.map +1 -1
- package/migrations/20260918083158_foamy_roughhouse/migration.sql +55 -0
- package/migrations/20260918083158_foamy_roughhouse/snapshot.json +5271 -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(index18) {
|
|
901
|
+
return CreateType({ [Kind]: "Argument", index: index18 });
|
|
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, index18, char) {
|
|
1140
|
+
return pattern[index18] === char && pattern.charCodeAt(index18 - 1) !== 92;
|
|
1141
1141
|
}
|
|
1142
|
-
function IsOpenParen(pattern,
|
|
1143
|
-
return IsNonEscaped(pattern,
|
|
1142
|
+
function IsOpenParen(pattern, index18) {
|
|
1143
|
+
return IsNonEscaped(pattern, index18, "(");
|
|
1144
1144
|
}
|
|
1145
|
-
function IsCloseParen(pattern,
|
|
1146
|
-
return IsNonEscaped(pattern,
|
|
1145
|
+
function IsCloseParen(pattern, index18) {
|
|
1146
|
+
return IsNonEscaped(pattern, index18, ")");
|
|
1147
1147
|
}
|
|
1148
|
-
function IsSeparator(pattern,
|
|
1149
|
-
return IsNonEscaped(pattern,
|
|
1148
|
+
function IsSeparator(pattern, index18) {
|
|
1149
|
+
return IsNonEscaped(pattern, index18, "|");
|
|
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 count = 0;
|
|
1155
|
-
for (let
|
|
1156
|
-
if (IsOpenParen(pattern,
|
|
1155
|
+
for (let index18 = 0; index18 < pattern.length; index18++) {
|
|
1156
|
+
if (IsOpenParen(pattern, index18))
|
|
1157
1157
|
count += 1;
|
|
1158
|
-
if (IsCloseParen(pattern,
|
|
1158
|
+
if (IsCloseParen(pattern, index18))
|
|
1159
1159
|
count -= 1;
|
|
1160
|
-
if (count === 0 &&
|
|
1160
|
+
if (count === 0 && index18 !== 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 count = 0;
|
|
1170
|
-
for (let
|
|
1171
|
-
if (IsOpenParen(pattern,
|
|
1170
|
+
for (let index18 = 0; index18 < pattern.length; index18++) {
|
|
1171
|
+
if (IsOpenParen(pattern, index18))
|
|
1172
1172
|
count += 1;
|
|
1173
|
-
if (IsCloseParen(pattern,
|
|
1173
|
+
if (IsCloseParen(pattern, index18))
|
|
1174
1174
|
count -= 1;
|
|
1175
|
-
if (IsSeparator(pattern,
|
|
1175
|
+
if (IsSeparator(pattern, index18) && count === 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 index18 = 0; index18 < pattern.length; index18++) {
|
|
1182
|
+
if (IsOpenParen(pattern, index18))
|
|
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 [count, start] = [0, 0];
|
|
1189
1189
|
const expressions = [];
|
|
1190
|
-
for (let
|
|
1191
|
-
if (IsOpenParen(pattern,
|
|
1190
|
+
for (let index18 = 0; index18 < pattern.length; index18++) {
|
|
1191
|
+
if (IsOpenParen(pattern, index18))
|
|
1192
1192
|
count += 1;
|
|
1193
|
-
if (IsCloseParen(pattern,
|
|
1193
|
+
if (IsCloseParen(pattern, index18))
|
|
1194
1194
|
count -= 1;
|
|
1195
|
-
if (IsSeparator(pattern,
|
|
1196
|
-
const range2 = pattern.slice(start,
|
|
1195
|
+
if (IsSeparator(pattern, index18) && count === 0) {
|
|
1196
|
+
const range2 = pattern.slice(start, index18);
|
|
1197
1197
|
if (range2.length > 0)
|
|
1198
1198
|
expressions.push(TemplateLiteralParse(range2));
|
|
1199
|
-
start =
|
|
1199
|
+
start = index18 + 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, index18) {
|
|
1213
|
+
if (!IsOpenParen(value, index18))
|
|
1214
1214
|
throw new TemplateLiteralParserError(`TemplateLiteralParser: Index must point to open parens`);
|
|
1215
1215
|
let count = 0;
|
|
1216
|
-
for (let scan =
|
|
1216
|
+
for (let scan = index18; scan < value.length; scan++) {
|
|
1217
1217
|
if (IsOpenParen(value, scan))
|
|
1218
1218
|
count += 1;
|
|
1219
1219
|
if (IsCloseParen(value, scan))
|
|
1220
1220
|
count -= 1;
|
|
1221
1221
|
if (count === 0)
|
|
1222
|
-
return [
|
|
1222
|
+
return [index18, 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, index18) {
|
|
1227
|
+
for (let scan = index18; scan < pattern2.length; scan++) {
|
|
1228
1228
|
if (IsOpenParen(pattern2, scan))
|
|
1229
|
-
return [
|
|
1229
|
+
return [index18, scan];
|
|
1230
1230
|
}
|
|
1231
|
-
return [
|
|
1231
|
+
return [index18, pattern2.length];
|
|
1232
1232
|
}
|
|
1233
1233
|
const expressions = [];
|
|
1234
|
-
for (let
|
|
1235
|
-
if (IsOpenParen(pattern,
|
|
1236
|
-
const [start, end] = Group(pattern,
|
|
1234
|
+
for (let index18 = 0; index18 < pattern.length; index18++) {
|
|
1235
|
+
if (IsOpenParen(pattern, index18)) {
|
|
1236
|
+
const [start, end] = Group(pattern, index18);
|
|
1237
1237
|
const range = pattern.slice(start, end + 1);
|
|
1238
1238
|
expressions.push(TemplateLiteralParse(range));
|
|
1239
|
-
|
|
1239
|
+
index18 = end;
|
|
1240
1240
|
} else {
|
|
1241
|
-
const [start, end] = Range(pattern,
|
|
1241
|
+
const [start, end] = Range(pattern, index18);
|
|
1242
1242
|
const range = pattern.slice(start, end);
|
|
1243
1243
|
if (range.length > 0)
|
|
1244
1244
|
expressions.push(TemplateLiteralParse(range));
|
|
1245
|
-
|
|
1245
|
+
index18 = 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, index18) => IntoBooleanResult(Visit3(right.parameters[index18], 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, index18) => IntoBooleanResult(Visit3(right.parameters[index18], 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, index18) => Visit3(schema, right.items[index18]) === 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;
|
|
@@ -5740,9 +5740,167 @@ var init_user_permissions = __esm({
|
|
|
5740
5740
|
}
|
|
5741
5741
|
});
|
|
5742
5742
|
|
|
5743
|
+
// src/server/entities/oauth2-clients.ts
|
|
5744
|
+
import { text as text16 } from "drizzle-orm/pg-core";
|
|
5745
|
+
import { id as id17, timestamps as timestamps15, utcTimestamp as utcTimestamp13 } from "@spfn/core/db";
|
|
5746
|
+
var oauth2Clients;
|
|
5747
|
+
var init_oauth2_clients = __esm({
|
|
5748
|
+
"src/server/entities/oauth2-clients.ts"() {
|
|
5749
|
+
"use strict";
|
|
5750
|
+
init_schema4();
|
|
5751
|
+
oauth2Clients = authSchema.table(
|
|
5752
|
+
"oauth2_clients",
|
|
5753
|
+
{
|
|
5754
|
+
id: id17(),
|
|
5755
|
+
// The `client_id` the client sends on every later request. Random, opaque,
|
|
5756
|
+
// and the unique constraint doubles as the lookup index.
|
|
5757
|
+
clientId: text16("client_id").notNull().unique(),
|
|
5758
|
+
// Self-declared label, shown on the consent screen and nowhere else.
|
|
5759
|
+
// A client that lies about it gains one wrong line on that screen.
|
|
5760
|
+
clientName: text16("client_name").notNull(),
|
|
5761
|
+
// Registered redirect URIs, stored exactly as the client wrote them.
|
|
5762
|
+
// The registration answer echoes them back verbatim, so normalising here
|
|
5763
|
+
// would answer a client with a URI it did not register. Matching
|
|
5764
|
+
// normalises both sides instead — see lib/oauth2/redirect-uri.ts.
|
|
5765
|
+
redirectUris: text16("redirect_uris").array().notNull(),
|
|
5766
|
+
// Client IP the registration arrived from, so the per-IP cap on clients
|
|
5767
|
+
// nobody has approved yet can be counted. Nullable: an IP is not always
|
|
5768
|
+
// knowable behind a proxy that forwards none, and a registration is not
|
|
5769
|
+
// worth refusing over that.
|
|
5770
|
+
createdIp: text16("created_ip"),
|
|
5771
|
+
// Last token issuance or refresh under this client, updated
|
|
5772
|
+
// fire-and-forget. Operator-facing only; nothing is authorized by it.
|
|
5773
|
+
lastUsedAt: utcTimestamp13("last_used_at"),
|
|
5774
|
+
...timestamps15()
|
|
5775
|
+
}
|
|
5776
|
+
);
|
|
5777
|
+
}
|
|
5778
|
+
});
|
|
5779
|
+
|
|
5780
|
+
// src/server/entities/oauth2-grants.ts
|
|
5781
|
+
import { uniqueIndex as uniqueIndex7, index as index16, text as text17 } from "drizzle-orm/pg-core";
|
|
5782
|
+
import { id as id18, timestamps as timestamps16, utcTimestamp as utcTimestamp14, foreignKey as foreignKey10 } from "@spfn/core/db";
|
|
5783
|
+
var oauth2Grants;
|
|
5784
|
+
var init_oauth2_grants = __esm({
|
|
5785
|
+
"src/server/entities/oauth2-grants.ts"() {
|
|
5786
|
+
"use strict";
|
|
5787
|
+
init_users();
|
|
5788
|
+
init_oauth2_clients();
|
|
5789
|
+
init_schema4();
|
|
5790
|
+
oauth2Grants = authSchema.table(
|
|
5791
|
+
"oauth2_grants",
|
|
5792
|
+
{
|
|
5793
|
+
id: id18(),
|
|
5794
|
+
// `oauth2_client_id` and not `client_id`: the opaque string a client
|
|
5795
|
+
// sends is called `client_id` everywhere in the protocol, and a bigint
|
|
5796
|
+
// foreign key under that name in this schema would read as that value.
|
|
5797
|
+
client: foreignKey10("oauth2_client", () => oauth2Clients.id, { onDelete: "cascade" }),
|
|
5798
|
+
// Whose consent this is. Read from the approving session at authorize
|
|
5799
|
+
// time, never from a request body — that is the whole authorization.
|
|
5800
|
+
user: foreignKey10("user", () => users.id, { onDelete: "cascade" }),
|
|
5801
|
+
// The RFC 8707 target this consent is for, normalised (see lib/oauth2).
|
|
5802
|
+
// An access token is only good against the resource its grant names.
|
|
5803
|
+
resource: text17("resource").notNull(),
|
|
5804
|
+
// Scope names the user approved. A refresh may ask for a subset of these
|
|
5805
|
+
// and never for more.
|
|
5806
|
+
scopes: text17("scopes").array().notNull(),
|
|
5807
|
+
// null = live; a timestamp cuts off every code and token beneath it
|
|
5808
|
+
revokedAt: utcTimestamp14("revoked_at"),
|
|
5809
|
+
...timestamps16()
|
|
5810
|
+
},
|
|
5811
|
+
(table) => [
|
|
5812
|
+
// One consent per (client, user, resource) — the re-consent path updates
|
|
5813
|
+
// this row rather than inserting beside it.
|
|
5814
|
+
uniqueIndex7("oauth2_grant_client_user_resource_idx").on(table.client, table.user, table.resource),
|
|
5815
|
+
// The global-revocation path and the user's own grant list both address
|
|
5816
|
+
// rows by user alone.
|
|
5817
|
+
index16("oauth2_grant_user_idx").on(table.user)
|
|
5818
|
+
]
|
|
5819
|
+
);
|
|
5820
|
+
}
|
|
5821
|
+
});
|
|
5822
|
+
|
|
5823
|
+
// src/server/entities/oauth2-authorization-codes.ts
|
|
5824
|
+
import { text as text18 } from "drizzle-orm/pg-core";
|
|
5825
|
+
import { id as id19, timestamps as timestamps17, utcTimestamp as utcTimestamp15, foreignKey as foreignKey11 } from "@spfn/core/db";
|
|
5826
|
+
var oauth2AuthorizationCodes;
|
|
5827
|
+
var init_oauth2_authorization_codes = __esm({
|
|
5828
|
+
"src/server/entities/oauth2-authorization-codes.ts"() {
|
|
5829
|
+
"use strict";
|
|
5830
|
+
init_oauth2_grants();
|
|
5831
|
+
init_schema4();
|
|
5832
|
+
oauth2AuthorizationCodes = authSchema.table(
|
|
5833
|
+
"oauth2_authorization_codes",
|
|
5834
|
+
{
|
|
5835
|
+
id: id19(),
|
|
5836
|
+
// SHA-256 hex of the code. The code itself (32 random bytes, base64url)
|
|
5837
|
+
// is in the redirect and nowhere else.
|
|
5838
|
+
codeHash: text18("code_hash").notNull().unique(),
|
|
5839
|
+
grant: foreignKey11("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
5840
|
+
// The exact redirect_uri string the authorize request carried. The token
|
|
5841
|
+
// request must repeat it character for character (RFC 6749 §4.1.3).
|
|
5842
|
+
redirectUri: text18("redirect_uri").notNull(),
|
|
5843
|
+
// The PKCE S256 challenge. `code_verifier` at the token endpoint is
|
|
5844
|
+
// hashed and compared against this; `plain` is not accepted anywhere,
|
|
5845
|
+
// so no method column is needed.
|
|
5846
|
+
codeChallenge: text18("code_challenge").notNull(),
|
|
5847
|
+
// 60 seconds from issuance. Judged by the database in the statement that
|
|
5848
|
+
// spends the row, not only in a read before it.
|
|
5849
|
+
expiresAt: utcTimestamp15("expires_at").notNull(),
|
|
5850
|
+
// null = unspent. Non-null means spent, and a second presentation of the
|
|
5851
|
+
// same code revokes the grant.
|
|
5852
|
+
usedAt: utcTimestamp15("used_at"),
|
|
5853
|
+
...timestamps17()
|
|
5854
|
+
}
|
|
5855
|
+
);
|
|
5856
|
+
}
|
|
5857
|
+
});
|
|
5858
|
+
|
|
5859
|
+
// src/server/entities/oauth2-tokens.ts
|
|
5860
|
+
import { index as index17, text as text19 } from "drizzle-orm/pg-core";
|
|
5861
|
+
import { id as id20, timestamps as timestamps18, enumText as enumText11, utcTimestamp as utcTimestamp16, foreignKey as foreignKey12 } from "@spfn/core/db";
|
|
5862
|
+
var OAUTH2_TOKEN_KINDS, oauth2Tokens;
|
|
5863
|
+
var init_oauth2_tokens = __esm({
|
|
5864
|
+
"src/server/entities/oauth2-tokens.ts"() {
|
|
5865
|
+
"use strict";
|
|
5866
|
+
init_oauth2_grants();
|
|
5867
|
+
init_schema4();
|
|
5868
|
+
OAUTH2_TOKEN_KINDS = ["access", "refresh"];
|
|
5869
|
+
oauth2Tokens = authSchema.table(
|
|
5870
|
+
"oauth2_tokens",
|
|
5871
|
+
{
|
|
5872
|
+
id: id20(),
|
|
5873
|
+
// SHA-256 hex of the token value. Lookup key; the unique constraint
|
|
5874
|
+
// doubles as the index.
|
|
5875
|
+
tokenHash: text19("token_hash").notNull().unique(),
|
|
5876
|
+
kind: enumText11("kind", OAUTH2_TOKEN_KINDS).notNull(),
|
|
5877
|
+
grant: foreignKey12("oauth2_grant", () => oauth2Grants.id, { onDelete: "cascade" }),
|
|
5878
|
+
// What this particular token carries, which may be narrower than its
|
|
5879
|
+
// grant's scopes: a refresh request is allowed to ask for a subset.
|
|
5880
|
+
// Never wider — that is `invalid_scope`.
|
|
5881
|
+
scopes: text19("scopes").array().notNull(),
|
|
5882
|
+
expiresAt: utcTimestamp16("expires_at").notNull(),
|
|
5883
|
+
// null = live. Set by revoke, by a grant revocation, and by the two
|
|
5884
|
+
// replay detections (code reuse, refresh reuse).
|
|
5885
|
+
revokedAt: utcTimestamp16("revoked_at"),
|
|
5886
|
+
// Refresh only: set when a rotation issued the successor. A row with
|
|
5887
|
+
// this set is spent, and presenting it revokes the grant.
|
|
5888
|
+
replacedAt: utcTimestamp16("replaced_at"),
|
|
5889
|
+
// Last successful verification, updated fire-and-forget like ops tokens
|
|
5890
|
+
lastUsedAt: utcTimestamp16("last_used_at"),
|
|
5891
|
+
...timestamps18()
|
|
5892
|
+
},
|
|
5893
|
+
(table) => [
|
|
5894
|
+
// Revocation addresses every token under one grant.
|
|
5895
|
+
index17("oauth2_token_grant_idx").on(table.grant)
|
|
5896
|
+
]
|
|
5897
|
+
);
|
|
5898
|
+
}
|
|
5899
|
+
});
|
|
5900
|
+
|
|
5743
5901
|
// src/server/entities/auth-metadata.ts
|
|
5744
5902
|
import { sql as sql3 } from "drizzle-orm";
|
|
5745
|
-
import { text as
|
|
5903
|
+
import { text as text20, timestamp } from "drizzle-orm/pg-core";
|
|
5746
5904
|
var authMetadata;
|
|
5747
5905
|
var init_auth_metadata = __esm({
|
|
5748
5906
|
"src/server/entities/auth-metadata.ts"() {
|
|
@@ -5752,9 +5910,9 @@ var init_auth_metadata = __esm({
|
|
|
5752
5910
|
"auth_metadata",
|
|
5753
5911
|
{
|
|
5754
5912
|
// Metadata key (primary key)
|
|
5755
|
-
key:
|
|
5913
|
+
key: text20("key").primaryKey(),
|
|
5756
5914
|
// Metadata value
|
|
5757
|
-
value:
|
|
5915
|
+
value: text20("value").notNull(),
|
|
5758
5916
|
// Last updated timestamp — stamped by the database on insert and on update
|
|
5759
5917
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => sql3`now()`)
|
|
5760
5918
|
}
|
|
@@ -5763,8 +5921,8 @@ var init_auth_metadata = __esm({
|
|
|
5763
5921
|
});
|
|
5764
5922
|
|
|
5765
5923
|
// src/server/entities/ops-tokens.ts
|
|
5766
|
-
import { text as
|
|
5767
|
-
import { id as
|
|
5924
|
+
import { text as text21 } from "drizzle-orm/pg-core";
|
|
5925
|
+
import { id as id21, timestamps as timestamps19, utcTimestamp as utcTimestamp17 } from "@spfn/core/db";
|
|
5768
5926
|
var opsTokens;
|
|
5769
5927
|
var init_ops_tokens = __esm({
|
|
5770
5928
|
"src/server/entities/ops-tokens.ts"() {
|
|
@@ -5773,22 +5931,22 @@ var init_ops_tokens = __esm({
|
|
|
5773
5931
|
opsTokens = authSchema.table(
|
|
5774
5932
|
"ops_tokens",
|
|
5775
5933
|
{
|
|
5776
|
-
id:
|
|
5934
|
+
id: id21(),
|
|
5777
5935
|
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
5778
|
-
name:
|
|
5936
|
+
name: text21("name").notNull(),
|
|
5779
5937
|
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
5780
5938
|
// here, and the unique constraint doubles as the lookup index.
|
|
5781
|
-
tokenHash:
|
|
5939
|
+
tokenHash: text21("token_hash").notNull().unique(),
|
|
5782
5940
|
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
5783
5941
|
// '*' grants every scope.
|
|
5784
|
-
scopes:
|
|
5942
|
+
scopes: text21("scopes").array().notNull(),
|
|
5785
5943
|
// null = the token does not expire
|
|
5786
|
-
expiresAt:
|
|
5944
|
+
expiresAt: utcTimestamp17("expires_at"),
|
|
5787
5945
|
// null = active; a timestamp revokes the token permanently
|
|
5788
|
-
revokedAt:
|
|
5946
|
+
revokedAt: utcTimestamp17("revoked_at"),
|
|
5789
5947
|
// Last successful verification, updated fire-and-forget
|
|
5790
|
-
lastUsedAt:
|
|
5791
|
-
...
|
|
5948
|
+
lastUsedAt: utcTimestamp17("last_used_at"),
|
|
5949
|
+
...timestamps19()
|
|
5792
5950
|
}
|
|
5793
5951
|
);
|
|
5794
5952
|
}
|
|
@@ -5815,6 +5973,10 @@ var init_entities = __esm({
|
|
|
5815
5973
|
init_permissions();
|
|
5816
5974
|
init_role_permissions();
|
|
5817
5975
|
init_user_permissions();
|
|
5976
|
+
init_oauth2_clients();
|
|
5977
|
+
init_oauth2_grants();
|
|
5978
|
+
init_oauth2_authorization_codes();
|
|
5979
|
+
init_oauth2_tokens();
|
|
5818
5980
|
init_auth_metadata();
|
|
5819
5981
|
init_ops_tokens();
|
|
5820
5982
|
}
|
|
@@ -5835,8 +5997,8 @@ var init_users_repository = __esm({
|
|
|
5835
5997
|
* ID로 사용자 조회
|
|
5836
5998
|
* Read replica 사용
|
|
5837
5999
|
*/
|
|
5838
|
-
async findById(
|
|
5839
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
6000
|
+
async findById(id22) {
|
|
6001
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id22)).limit(1);
|
|
5840
6002
|
return result[0] ?? null;
|
|
5841
6003
|
}
|
|
5842
6004
|
/**
|
|
@@ -5846,8 +6008,8 @@ var init_users_repository = __esm({
|
|
|
5846
6008
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
5847
6009
|
* 계속 사용할 것.
|
|
5848
6010
|
*/
|
|
5849
|
-
async findByIdOnPrimary(
|
|
5850
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
6011
|
+
async findByIdOnPrimary(id22) {
|
|
6012
|
+
const result = await this.db.select().from(users).where(eq(users.id, id22)).limit(1);
|
|
5851
6013
|
return result[0] ?? null;
|
|
5852
6014
|
}
|
|
5853
6015
|
/**
|
|
@@ -5863,8 +6025,8 @@ var init_users_repository = __esm({
|
|
|
5863
6025
|
* Only meaningful inside a transaction — the lock is released at commit.
|
|
5864
6026
|
* Write primary.
|
|
5865
6027
|
*/
|
|
5866
|
-
async lockById(
|
|
5867
|
-
await this.db.select({ id: users.id }).from(users).where(eq(users.id,
|
|
6028
|
+
async lockById(id22) {
|
|
6029
|
+
await this.db.select({ id: users.id }).from(users).where(eq(users.id, id22)).for("update");
|
|
5868
6030
|
}
|
|
5869
6031
|
/**
|
|
5870
6032
|
* 이메일로 사용자 조회
|
|
@@ -5934,13 +6096,13 @@ var init_users_repository = __esm({
|
|
|
5934
6096
|
*
|
|
5935
6097
|
* roleId가 null인 유저는 role: null 반환
|
|
5936
6098
|
*/
|
|
5937
|
-
async findByIdWithRole(
|
|
6099
|
+
async findByIdWithRole(id22) {
|
|
5938
6100
|
const result = await this.readDb.select({
|
|
5939
6101
|
user: users,
|
|
5940
6102
|
roleName: roles.name,
|
|
5941
6103
|
roleDisplayName: roles.displayName,
|
|
5942
6104
|
rolePriority: roles.priority
|
|
5943
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
6105
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id22)).limit(1);
|
|
5944
6106
|
const row = result[0];
|
|
5945
6107
|
if (!row) {
|
|
5946
6108
|
return null;
|
|
@@ -6015,9 +6177,9 @@ var init_users_repository = __esm({
|
|
|
6015
6177
|
* 사용자 정보 업데이트
|
|
6016
6178
|
* Write primary 사용
|
|
6017
6179
|
*/
|
|
6018
|
-
async updateById(
|
|
6180
|
+
async updateById(id22, data) {
|
|
6019
6181
|
const patch = "email" in data ? { ...data, email: normalizeOptionalEmail(data.email) } : data;
|
|
6020
|
-
const result = await this.db.update(users).set(patch).where(eq(users.id,
|
|
6182
|
+
const result = await this.db.update(users).set(patch).where(eq(users.id, id22)).returning();
|
|
6021
6183
|
return result[0] ?? null;
|
|
6022
6184
|
}
|
|
6023
6185
|
/**
|
|
@@ -6029,10 +6191,10 @@ var init_users_repository = __esm({
|
|
|
6029
6191
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
6030
6192
|
* Write primary 사용
|
|
6031
6193
|
*/
|
|
6032
|
-
async reactivateFromPendingDeletion(
|
|
6194
|
+
async reactivateFromPendingDeletion(id22) {
|
|
6033
6195
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
6034
6196
|
and(
|
|
6035
|
-
eq(users.id,
|
|
6197
|
+
eq(users.id, id22),
|
|
6036
6198
|
eq(users.status, "pending_deletion")
|
|
6037
6199
|
)
|
|
6038
6200
|
).returning();
|
|
@@ -6042,32 +6204,32 @@ var init_users_repository = __esm({
|
|
|
6042
6204
|
* 비밀번호 업데이트
|
|
6043
6205
|
* Write primary 사용
|
|
6044
6206
|
*/
|
|
6045
|
-
async updatePassword(
|
|
6207
|
+
async updatePassword(id22, passwordHash, clearPasswordChangeRequired = true) {
|
|
6046
6208
|
const updateData = {
|
|
6047
6209
|
passwordHash
|
|
6048
6210
|
};
|
|
6049
6211
|
if (clearPasswordChangeRequired) {
|
|
6050
6212
|
updateData.passwordChangeRequired = false;
|
|
6051
6213
|
}
|
|
6052
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
6214
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id22)).returning();
|
|
6053
6215
|
return result[0] ?? null;
|
|
6054
6216
|
}
|
|
6055
6217
|
/**
|
|
6056
6218
|
* 마지막 로그인 시간 업데이트
|
|
6057
6219
|
* Write primary 사용
|
|
6058
6220
|
*/
|
|
6059
|
-
async updateLastLogin(
|
|
6221
|
+
async updateLastLogin(id22) {
|
|
6060
6222
|
const result = await this.db.update(users).set({
|
|
6061
6223
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
6062
|
-
}).where(eq(users.id,
|
|
6224
|
+
}).where(eq(users.id, id22)).returning();
|
|
6063
6225
|
return result[0] ?? null;
|
|
6064
6226
|
}
|
|
6065
6227
|
/**
|
|
6066
6228
|
* 사용자 삭제
|
|
6067
6229
|
* Write primary 사용
|
|
6068
6230
|
*/
|
|
6069
|
-
async deleteById(
|
|
6070
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
6231
|
+
async deleteById(id22) {
|
|
6232
|
+
const result = await this.db.delete(users).where(eq(users.id, id22)).returning();
|
|
6071
6233
|
return result[0] ?? null;
|
|
6072
6234
|
}
|
|
6073
6235
|
/**
|
|
@@ -6415,14 +6577,14 @@ var init_keys_repository = __esm({
|
|
|
6415
6577
|
* stored, so it answers "since when has this device been on this release"
|
|
6416
6578
|
* rather than "when was it last seen", which lastUsedAt already answers.
|
|
6417
6579
|
*/
|
|
6418
|
-
async updateLastUsedById(
|
|
6580
|
+
async updateLastUsedById(id22, identity) {
|
|
6419
6581
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
6420
6582
|
const lastUsedIsStale = or(
|
|
6421
6583
|
isNull(userPublicKeys.lastUsedAt),
|
|
6422
6584
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
6423
6585
|
);
|
|
6424
6586
|
if (!identity) {
|
|
6425
|
-
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id,
|
|
6587
|
+
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id22), lastUsedIsStale));
|
|
6426
6588
|
return;
|
|
6427
6589
|
}
|
|
6428
6590
|
const identityChanged = sql5`(
|
|
@@ -6439,7 +6601,7 @@ var init_keys_repository = __esm({
|
|
|
6439
6601
|
clientContractVersion: identity.contractVersion,
|
|
6440
6602
|
clientSeenAt: sql5`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
6441
6603
|
}).where(and2(
|
|
6442
|
-
eq2(userPublicKeys.id,
|
|
6604
|
+
eq2(userPublicKeys.id, id22),
|
|
6443
6605
|
or(lastUsedIsStale, identityChanged)
|
|
6444
6606
|
));
|
|
6445
6607
|
}
|
|
@@ -6478,8 +6640,8 @@ var init_verification_codes_repository = __esm({
|
|
|
6478
6640
|
* ID로 인증 코드 조회
|
|
6479
6641
|
* Read replica 사용
|
|
6480
6642
|
*/
|
|
6481
|
-
async findById(
|
|
6482
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
6643
|
+
async findById(id22) {
|
|
6644
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id22)).limit(1);
|
|
6483
6645
|
return result[0] ?? null;
|
|
6484
6646
|
}
|
|
6485
6647
|
/**
|
|
@@ -6493,22 +6655,22 @@ var init_verification_codes_repository = __esm({
|
|
|
6493
6655
|
* 인증 코드 사용 처리
|
|
6494
6656
|
* Write primary 사용
|
|
6495
6657
|
*/
|
|
6496
|
-
async markAsUsed(
|
|
6658
|
+
async markAsUsed(id22) {
|
|
6497
6659
|
const result = await this.db.update(verificationCodes).set({
|
|
6498
6660
|
usedAt: /* @__PURE__ */ new Date()
|
|
6499
|
-
}).where(eq3(verificationCodes.id,
|
|
6661
|
+
}).where(eq3(verificationCodes.id, id22)).returning();
|
|
6500
6662
|
return result[0] ?? null;
|
|
6501
6663
|
}
|
|
6502
6664
|
/**
|
|
6503
6665
|
* 시도 횟수 증가
|
|
6504
6666
|
* Write primary 사용
|
|
6505
6667
|
*/
|
|
6506
|
-
async incrementAttempts(
|
|
6507
|
-
const code = await this.findById(
|
|
6668
|
+
async incrementAttempts(id22) {
|
|
6669
|
+
const code = await this.findById(id22);
|
|
6508
6670
|
if (!code) return null;
|
|
6509
6671
|
const result = await this.db.update(verificationCodes).set({
|
|
6510
6672
|
attempts: code.attempts + 1
|
|
6511
|
-
}).where(eq3(verificationCodes.id,
|
|
6673
|
+
}).where(eq3(verificationCodes.id, id22)).returning();
|
|
6512
6674
|
return result[0] ?? null;
|
|
6513
6675
|
}
|
|
6514
6676
|
/**
|
|
@@ -6592,10 +6754,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6592
6754
|
*
|
|
6593
6755
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
6594
6756
|
*/
|
|
6595
|
-
async issue(
|
|
6757
|
+
async issue(id22, tokenHash) {
|
|
6596
6758
|
const result = await this.db.update(signupLinkTokens).set({ tokenHash }).where(
|
|
6597
6759
|
and4(
|
|
6598
|
-
eq4(signupLinkTokens.id,
|
|
6760
|
+
eq4(signupLinkTokens.id, id22),
|
|
6599
6761
|
isNull3(signupLinkTokens.consumedAt),
|
|
6600
6762
|
isNull3(signupLinkTokens.supersededAt),
|
|
6601
6763
|
isNull3(signupLinkTokens.completedAt),
|
|
@@ -6631,14 +6793,14 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6631
6793
|
*
|
|
6632
6794
|
* @returns the updated row, or null if another request claimed it first
|
|
6633
6795
|
*/
|
|
6634
|
-
async claimLink(
|
|
6796
|
+
async claimLink(id22, setupSecretHash, setupExpiresAt) {
|
|
6635
6797
|
const result = await this.db.update(signupLinkTokens).set({
|
|
6636
6798
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
6637
6799
|
setupSecretHash,
|
|
6638
6800
|
setupExpiresAt
|
|
6639
6801
|
}).where(
|
|
6640
6802
|
and4(
|
|
6641
|
-
eq4(signupLinkTokens.id,
|
|
6803
|
+
eq4(signupLinkTokens.id, id22),
|
|
6642
6804
|
isNull3(signupLinkTokens.consumedAt),
|
|
6643
6805
|
isNull3(signupLinkTokens.supersededAt)
|
|
6644
6806
|
)
|
|
@@ -6651,10 +6813,10 @@ var init_signup_link_tokens_repository = __esm({
|
|
|
6651
6813
|
*
|
|
6652
6814
|
* @returns the updated row, or null if another request completed it first
|
|
6653
6815
|
*/
|
|
6654
|
-
async claimSetupSession(
|
|
6816
|
+
async claimSetupSession(id22) {
|
|
6655
6817
|
const result = await this.db.update(signupLinkTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
6656
6818
|
and4(
|
|
6657
|
-
eq4(signupLinkTokens.id,
|
|
6819
|
+
eq4(signupLinkTokens.id, id22),
|
|
6658
6820
|
isNull3(signupLinkTokens.completedAt),
|
|
6659
6821
|
isNull3(signupLinkTokens.supersededAt)
|
|
6660
6822
|
)
|
|
@@ -6736,10 +6898,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
6736
6898
|
*
|
|
6737
6899
|
* @returns the issued row, or null if the row is no longer deliverable
|
|
6738
6900
|
*/
|
|
6739
|
-
async issue(
|
|
6901
|
+
async issue(id22, tokenHash) {
|
|
6740
6902
|
const result = await this.db.update(passwordResetTokens).set({ tokenHash }).where(
|
|
6741
6903
|
and5(
|
|
6742
|
-
eq5(passwordResetTokens.id,
|
|
6904
|
+
eq5(passwordResetTokens.id, id22),
|
|
6743
6905
|
isNull4(passwordResetTokens.consumedAt),
|
|
6744
6906
|
isNull4(passwordResetTokens.supersededAt),
|
|
6745
6907
|
isNull4(passwordResetTokens.completedAt),
|
|
@@ -6787,14 +6949,14 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
6787
6949
|
*
|
|
6788
6950
|
* @returns the updated row, or null if another request claimed it first
|
|
6789
6951
|
*/
|
|
6790
|
-
async consume(
|
|
6952
|
+
async consume(id22, setupSecretHash, setupExpiresAt) {
|
|
6791
6953
|
const result = await this.db.update(passwordResetTokens).set({
|
|
6792
6954
|
consumedAt: /* @__PURE__ */ new Date(),
|
|
6793
6955
|
setupSecretHash,
|
|
6794
6956
|
setupExpiresAt
|
|
6795
6957
|
}).where(
|
|
6796
6958
|
and5(
|
|
6797
|
-
eq5(passwordResetTokens.id,
|
|
6959
|
+
eq5(passwordResetTokens.id, id22),
|
|
6798
6960
|
isNull4(passwordResetTokens.consumedAt),
|
|
6799
6961
|
isNull4(passwordResetTokens.supersededAt)
|
|
6800
6962
|
)
|
|
@@ -6807,10 +6969,10 @@ var init_password_reset_tokens_repository = __esm({
|
|
|
6807
6969
|
*
|
|
6808
6970
|
* @returns the updated row, or null if another request completed it first
|
|
6809
6971
|
*/
|
|
6810
|
-
async complete(
|
|
6972
|
+
async complete(id22) {
|
|
6811
6973
|
const result = await this.db.update(passwordResetTokens).set({ completedAt: /* @__PURE__ */ new Date() }).where(
|
|
6812
6974
|
and5(
|
|
6813
|
-
eq5(passwordResetTokens.id,
|
|
6975
|
+
eq5(passwordResetTokens.id, id22),
|
|
6814
6976
|
isNull4(passwordResetTokens.completedAt),
|
|
6815
6977
|
isNull4(passwordResetTokens.supersededAt)
|
|
6816
6978
|
)
|
|
@@ -6891,9 +7053,9 @@ var init_passkeys_repository = __esm({
|
|
|
6891
7053
|
* Owner-scoped, so an id belonging to someone else answers null rather than
|
|
6892
7054
|
* a row — a management route can only ever say "not yours".
|
|
6893
7055
|
*/
|
|
6894
|
-
async findLiveByIdAndUserId(
|
|
7056
|
+
async findLiveByIdAndUserId(id22, userId) {
|
|
6895
7057
|
const result = await this.db.select().from(passkeys).where(and6(
|
|
6896
|
-
eq6(passkeys.id,
|
|
7058
|
+
eq6(passkeys.id, id22),
|
|
6897
7059
|
eq6(passkeys.userId, userId),
|
|
6898
7060
|
isNull5(passkeys.revokedAt)
|
|
6899
7061
|
)).limit(1);
|
|
@@ -6903,17 +7065,17 @@ var init_passkeys_repository = __esm({
|
|
|
6903
7065
|
* Record a successful assertion.
|
|
6904
7066
|
* Write primary.
|
|
6905
7067
|
*/
|
|
6906
|
-
async recordUse(
|
|
6907
|
-
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq6(passkeys.id,
|
|
7068
|
+
async recordUse(id22, counter) {
|
|
7069
|
+
await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq6(passkeys.id, id22));
|
|
6908
7070
|
}
|
|
6909
7071
|
/**
|
|
6910
7072
|
* Rename, but only a credential this user still owns and has not revoked.
|
|
6911
7073
|
*
|
|
6912
7074
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
6913
7075
|
*/
|
|
6914
|
-
async renameByIdAndUserId(
|
|
7076
|
+
async renameByIdAndUserId(id22, userId, label) {
|
|
6915
7077
|
const result = await this.db.update(passkeys).set({ label }).where(and6(
|
|
6916
|
-
eq6(passkeys.id,
|
|
7078
|
+
eq6(passkeys.id, id22),
|
|
6917
7079
|
eq6(passkeys.userId, userId),
|
|
6918
7080
|
isNull5(passkeys.revokedAt)
|
|
6919
7081
|
)).returning();
|
|
@@ -6928,9 +7090,9 @@ var init_passkeys_repository = __esm({
|
|
|
6928
7090
|
*
|
|
6929
7091
|
* @returns the updated row, or null if it is not theirs or already revoked
|
|
6930
7092
|
*/
|
|
6931
|
-
async revokeByIdAndUserId(
|
|
7093
|
+
async revokeByIdAndUserId(id22, userId, reason) {
|
|
6932
7094
|
const result = await this.db.update(passkeys).set({ revokedAt: /* @__PURE__ */ new Date(), revokedReason: reason }).where(and6(
|
|
6933
|
-
eq6(passkeys.id,
|
|
7095
|
+
eq6(passkeys.id, id22),
|
|
6934
7096
|
eq6(passkeys.userId, userId),
|
|
6935
7097
|
isNull5(passkeys.revokedAt)
|
|
6936
7098
|
)).returning();
|
|
@@ -7063,10 +7225,10 @@ var init_device_authorizations_repository = __esm({
|
|
|
7063
7225
|
*
|
|
7064
7226
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
7065
7227
|
*/
|
|
7066
|
-
async approve(
|
|
7228
|
+
async approve(id22, userId) {
|
|
7067
7229
|
const result = await this.db.update(deviceAuthorizations).set({ status: "approved", userId, approvedAt: /* @__PURE__ */ new Date() }).where(
|
|
7068
7230
|
and8(
|
|
7069
|
-
eq8(deviceAuthorizations.id,
|
|
7231
|
+
eq8(deviceAuthorizations.id, id22),
|
|
7070
7232
|
eq8(deviceAuthorizations.status, "pending"),
|
|
7071
7233
|
notExpired()
|
|
7072
7234
|
)
|
|
@@ -7080,10 +7242,10 @@ var init_device_authorizations_repository = __esm({
|
|
|
7080
7242
|
*
|
|
7081
7243
|
* @returns the updated row, or null if it was no longer pending, or expired
|
|
7082
7244
|
*/
|
|
7083
|
-
async deny(
|
|
7245
|
+
async deny(id22) {
|
|
7084
7246
|
const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
|
|
7085
7247
|
and8(
|
|
7086
|
-
eq8(deviceAuthorizations.id,
|
|
7248
|
+
eq8(deviceAuthorizations.id, id22),
|
|
7087
7249
|
eq8(deviceAuthorizations.status, "pending"),
|
|
7088
7250
|
notExpired()
|
|
7089
7251
|
)
|
|
@@ -7161,8 +7323,8 @@ var init_roles_repository = __esm({
|
|
|
7161
7323
|
/**
|
|
7162
7324
|
* ID로 역할 조회
|
|
7163
7325
|
*/
|
|
7164
|
-
async findById(
|
|
7165
|
-
const result = await this.readDb.select().from(roles).where(eq9(roles.id,
|
|
7326
|
+
async findById(id22) {
|
|
7327
|
+
const result = await this.readDb.select().from(roles).where(eq9(roles.id, id22)).limit(1);
|
|
7166
7328
|
return result[0] ?? null;
|
|
7167
7329
|
}
|
|
7168
7330
|
/**
|
|
@@ -7193,15 +7355,15 @@ var init_roles_repository = __esm({
|
|
|
7193
7355
|
/**
|
|
7194
7356
|
* 역할 업데이트
|
|
7195
7357
|
*/
|
|
7196
|
-
async updateById(
|
|
7197
|
-
const result = await this.db.update(roles).set(data).where(eq9(roles.id,
|
|
7358
|
+
async updateById(id22, data) {
|
|
7359
|
+
const result = await this.db.update(roles).set(data).where(eq9(roles.id, id22)).returning();
|
|
7198
7360
|
return result[0] ?? null;
|
|
7199
7361
|
}
|
|
7200
7362
|
/**
|
|
7201
7363
|
* 역할 삭제
|
|
7202
7364
|
*/
|
|
7203
|
-
async deleteById(
|
|
7204
|
-
const result = await this.db.delete(roles).where(eq9(roles.id,
|
|
7365
|
+
async deleteById(id22) {
|
|
7366
|
+
const result = await this.db.delete(roles).where(eq9(roles.id, id22)).returning();
|
|
7205
7367
|
return result[0] ?? null;
|
|
7206
7368
|
}
|
|
7207
7369
|
};
|
|
@@ -7221,8 +7383,8 @@ var init_permissions_repository = __esm({
|
|
|
7221
7383
|
/**
|
|
7222
7384
|
* ID로 권한 조회
|
|
7223
7385
|
*/
|
|
7224
|
-
async findById(
|
|
7225
|
-
const result = await this.readDb.select().from(permissions).where(eq10(permissions.id,
|
|
7386
|
+
async findById(id22) {
|
|
7387
|
+
const result = await this.readDb.select().from(permissions).where(eq10(permissions.id, id22)).limit(1);
|
|
7226
7388
|
return result[0] ?? null;
|
|
7227
7389
|
}
|
|
7228
7390
|
/**
|
|
@@ -7273,15 +7435,15 @@ var init_permissions_repository = __esm({
|
|
|
7273
7435
|
/**
|
|
7274
7436
|
* 권한 업데이트
|
|
7275
7437
|
*/
|
|
7276
|
-
async updateById(
|
|
7277
|
-
const result = await this.db.update(permissions).set(data).where(eq10(permissions.id,
|
|
7438
|
+
async updateById(id22, data) {
|
|
7439
|
+
const result = await this.db.update(permissions).set(data).where(eq10(permissions.id, id22)).returning();
|
|
7278
7440
|
return result[0] ?? null;
|
|
7279
7441
|
}
|
|
7280
7442
|
/**
|
|
7281
7443
|
* 권한 삭제
|
|
7282
7444
|
*/
|
|
7283
|
-
async deleteById(
|
|
7284
|
-
const result = await this.db.delete(permissions).where(eq10(permissions.id,
|
|
7445
|
+
async deleteById(id22) {
|
|
7446
|
+
const result = await this.db.delete(permissions).where(eq10(permissions.id, id22)).returning();
|
|
7285
7447
|
return result[0] ?? null;
|
|
7286
7448
|
}
|
|
7287
7449
|
};
|
|
@@ -7411,8 +7573,8 @@ var init_user_permissions_repository = __esm({
|
|
|
7411
7573
|
/**
|
|
7412
7574
|
* 사용자 권한 오버라이드 업데이트
|
|
7413
7575
|
*/
|
|
7414
|
-
async updateById(
|
|
7415
|
-
const result = await this.db.update(userPermissions).set(data).where(eq12(userPermissions.id,
|
|
7576
|
+
async updateById(id22, data) {
|
|
7577
|
+
const result = await this.db.update(userPermissions).set(data).where(eq12(userPermissions.id, id22)).returning();
|
|
7416
7578
|
return result[0] ?? null;
|
|
7417
7579
|
}
|
|
7418
7580
|
/**
|
|
@@ -7464,8 +7626,8 @@ var init_user_profiles_repository = __esm({
|
|
|
7464
7626
|
/**
|
|
7465
7627
|
* ID로 프로필 조회
|
|
7466
7628
|
*/
|
|
7467
|
-
async findById(
|
|
7468
|
-
const result = await this.readDb.select().from(userProfiles).where(eq13(userProfiles.id,
|
|
7629
|
+
async findById(id22) {
|
|
7630
|
+
const result = await this.readDb.select().from(userProfiles).where(eq13(userProfiles.id, id22)).limit(1);
|
|
7469
7631
|
return result[0] ?? null;
|
|
7470
7632
|
}
|
|
7471
7633
|
/**
|
|
@@ -7491,8 +7653,8 @@ var init_user_profiles_repository = __esm({
|
|
|
7491
7653
|
/**
|
|
7492
7654
|
* 프로필 업데이트 (by ID)
|
|
7493
7655
|
*/
|
|
7494
|
-
async updateById(
|
|
7495
|
-
const result = await this.db.update(userProfiles).set(data).where(eq13(userProfiles.id,
|
|
7656
|
+
async updateById(id22, data) {
|
|
7657
|
+
const result = await this.db.update(userProfiles).set(data).where(eq13(userProfiles.id, id22)).returning();
|
|
7496
7658
|
return result[0] ?? null;
|
|
7497
7659
|
}
|
|
7498
7660
|
/**
|
|
@@ -7505,8 +7667,8 @@ var init_user_profiles_repository = __esm({
|
|
|
7505
7667
|
/**
|
|
7506
7668
|
* 프로필 삭제 (by ID)
|
|
7507
7669
|
*/
|
|
7508
|
-
async deleteById(
|
|
7509
|
-
const result = await this.db.delete(userProfiles).where(eq13(userProfiles.id,
|
|
7670
|
+
async deleteById(id22) {
|
|
7671
|
+
const result = await this.db.delete(userProfiles).where(eq13(userProfiles.id, id22)).returning();
|
|
7510
7672
|
return result[0] ?? null;
|
|
7511
7673
|
}
|
|
7512
7674
|
/**
|
|
@@ -7597,8 +7759,8 @@ var init_invitations_repository = __esm({
|
|
|
7597
7759
|
/**
|
|
7598
7760
|
* ID로 초대 조회
|
|
7599
7761
|
*/
|
|
7600
|
-
async findById(
|
|
7601
|
-
const result = await this.readDb.select().from(userInvitations).where(eq14(userInvitations.id,
|
|
7762
|
+
async findById(id22) {
|
|
7763
|
+
const result = await this.readDb.select().from(userInvitations).where(eq14(userInvitations.id, id22)).limit(1);
|
|
7602
7764
|
return result[0] ?? null;
|
|
7603
7765
|
}
|
|
7604
7766
|
/**
|
|
@@ -7641,7 +7803,7 @@ var init_invitations_repository = __esm({
|
|
|
7641
7803
|
/**
|
|
7642
7804
|
* 초대 상태 업데이트
|
|
7643
7805
|
*/
|
|
7644
|
-
async updateStatus(
|
|
7806
|
+
async updateStatus(id22, status, timestamp2) {
|
|
7645
7807
|
const updates = {
|
|
7646
7808
|
status
|
|
7647
7809
|
};
|
|
@@ -7652,14 +7814,14 @@ var init_invitations_repository = __esm({
|
|
|
7652
7814
|
updates.cancelledAt = timestamp2;
|
|
7653
7815
|
}
|
|
7654
7816
|
}
|
|
7655
|
-
const result = await this.db.update(userInvitations).set(updates).where(eq14(userInvitations.id,
|
|
7817
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq14(userInvitations.id, id22)).returning();
|
|
7656
7818
|
return result[0] ?? null;
|
|
7657
7819
|
}
|
|
7658
7820
|
/**
|
|
7659
7821
|
* 초대 삭제
|
|
7660
7822
|
*/
|
|
7661
|
-
async deleteById(
|
|
7662
|
-
const result = await this.db.delete(userInvitations).where(eq14(userInvitations.id,
|
|
7823
|
+
async deleteById(id22) {
|
|
7824
|
+
const result = await this.db.delete(userInvitations).where(eq14(userInvitations.id, id22)).returning();
|
|
7663
7825
|
return result[0] ?? null;
|
|
7664
7826
|
}
|
|
7665
7827
|
/**
|
|
@@ -7754,31 +7916,31 @@ var init_invitations_repository = __esm({
|
|
|
7754
7916
|
/**
|
|
7755
7917
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
7756
7918
|
*/
|
|
7757
|
-
async updateById(
|
|
7919
|
+
async updateById(id22, data) {
|
|
7758
7920
|
const patch = "email" in data && typeof data.email === "string" ? { ...data, email: normalizeEmail(data.email) } : data;
|
|
7759
|
-
const result = await this.db.update(userInvitations).set(patch).where(eq14(userInvitations.id,
|
|
7921
|
+
const result = await this.db.update(userInvitations).set(patch).where(eq14(userInvitations.id, id22)).returning();
|
|
7760
7922
|
return result[0] ?? null;
|
|
7761
7923
|
}
|
|
7762
7924
|
/**
|
|
7763
7925
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
7764
7926
|
*/
|
|
7765
|
-
async resend(
|
|
7927
|
+
async resend(id22, newExpiresAt) {
|
|
7766
7928
|
const result = await this.db.update(userInvitations).set({
|
|
7767
7929
|
status: "pending",
|
|
7768
7930
|
expiresAt: newExpiresAt
|
|
7769
|
-
}).where(eq14(userInvitations.id,
|
|
7931
|
+
}).where(eq14(userInvitations.id, id22)).returning();
|
|
7770
7932
|
return result[0] ?? null;
|
|
7771
7933
|
}
|
|
7772
7934
|
/**
|
|
7773
7935
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
7774
7936
|
*/
|
|
7775
|
-
async cancel(
|
|
7937
|
+
async cancel(id22, cancelledBy, reason, currentMetadata) {
|
|
7776
7938
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
7777
7939
|
const result = await this.db.update(userInvitations).set({
|
|
7778
7940
|
status: "cancelled",
|
|
7779
7941
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
7780
7942
|
metadata: newMetadata
|
|
7781
|
-
}).where(eq14(userInvitations.id,
|
|
7943
|
+
}).where(eq14(userInvitations.id, id22)).returning();
|
|
7782
7944
|
return result[0] ?? null;
|
|
7783
7945
|
}
|
|
7784
7946
|
};
|
|
@@ -8633,11 +8795,11 @@ var init_social_accounts_repository = __esm({
|
|
|
8633
8795
|
* 토큰 정보 업데이트
|
|
8634
8796
|
* Write primary 사용
|
|
8635
8797
|
*/
|
|
8636
|
-
async updateTokens(
|
|
8798
|
+
async updateTokens(id22, data) {
|
|
8637
8799
|
const accounts = await this.db.select({
|
|
8638
8800
|
provider: userSocialAccounts.provider,
|
|
8639
8801
|
providerUserId: userSocialAccounts.providerUserId
|
|
8640
|
-
}).from(userSocialAccounts).where(eq15(userSocialAccounts.id,
|
|
8802
|
+
}).from(userSocialAccounts).where(eq15(userSocialAccounts.id, id22)).limit(1);
|
|
8641
8803
|
const account = accounts[0];
|
|
8642
8804
|
if (!account) {
|
|
8643
8805
|
return null;
|
|
@@ -8651,15 +8813,15 @@ var init_social_accounts_repository = __esm({
|
|
|
8651
8813
|
...data,
|
|
8652
8814
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
8653
8815
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
8654
|
-
}).where(eq15(userSocialAccounts.id,
|
|
8816
|
+
}).where(eq15(userSocialAccounts.id, id22)).returning();
|
|
8655
8817
|
return this.decryptAccount(result[0] ?? null);
|
|
8656
8818
|
}
|
|
8657
8819
|
/**
|
|
8658
8820
|
* 소셜 계정 삭제
|
|
8659
8821
|
* Write primary 사용
|
|
8660
8822
|
*/
|
|
8661
|
-
async deleteById(
|
|
8662
|
-
const result = await this.db.delete(userSocialAccounts).where(eq15(userSocialAccounts.id,
|
|
8823
|
+
async deleteById(id22) {
|
|
8824
|
+
const result = await this.db.delete(userSocialAccounts).where(eq15(userSocialAccounts.id, id22)).returning();
|
|
8663
8825
|
return result[0] ?? null;
|
|
8664
8826
|
}
|
|
8665
8827
|
/**
|
|
@@ -8739,8 +8901,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
8739
8901
|
* ID로 요청 조회
|
|
8740
8902
|
* Read replica 사용
|
|
8741
8903
|
*/
|
|
8742
|
-
async findById(
|
|
8743
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(eq17(accountDeletionRequests.id,
|
|
8904
|
+
async findById(id22) {
|
|
8905
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq17(accountDeletionRequests.id, id22)).limit(1);
|
|
8744
8906
|
return result[0] ?? null;
|
|
8745
8907
|
}
|
|
8746
8908
|
/**
|
|
@@ -8799,13 +8961,13 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
8799
8961
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
8800
8962
|
* Write primary 사용
|
|
8801
8963
|
*/
|
|
8802
|
-
async markCancelled(
|
|
8964
|
+
async markCancelled(id22) {
|
|
8803
8965
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
8804
8966
|
status: "cancelled",
|
|
8805
8967
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
8806
8968
|
}).where(
|
|
8807
8969
|
and13(
|
|
8808
|
-
eq17(accountDeletionRequests.id,
|
|
8970
|
+
eq17(accountDeletionRequests.id, id22),
|
|
8809
8971
|
eq17(accountDeletionRequests.status, "pending")
|
|
8810
8972
|
)
|
|
8811
8973
|
).returning();
|
|
@@ -8821,14 +8983,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
8821
8983
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
8822
8984
|
* Write primary 사용
|
|
8823
8985
|
*/
|
|
8824
|
-
async markCompleted(
|
|
8986
|
+
async markCompleted(id22, purgeStrategy) {
|
|
8825
8987
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
8826
8988
|
status: "completed",
|
|
8827
8989
|
completedAt: /* @__PURE__ */ new Date(),
|
|
8828
8990
|
purgeStrategy
|
|
8829
8991
|
}).where(
|
|
8830
8992
|
and13(
|
|
8831
|
-
eq17(accountDeletionRequests.id,
|
|
8993
|
+
eq17(accountDeletionRequests.id, id22),
|
|
8832
8994
|
eq17(accountDeletionRequests.status, "pending")
|
|
8833
8995
|
)
|
|
8834
8996
|
).returning();
|
|
@@ -8872,19 +9034,332 @@ var init_ops_tokens_repository = __esm({
|
|
|
8872
9034
|
* token is already revoked — the first revocation's timestamp is never
|
|
8873
9035
|
* overwritten.
|
|
8874
9036
|
*/
|
|
8875
|
-
async revokeById(
|
|
8876
|
-
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and14(eq18(opsTokens.id,
|
|
9037
|
+
async revokeById(id22) {
|
|
9038
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and14(eq18(opsTokens.id, id22), isNull8(opsTokens.revokedAt))).returning();
|
|
8877
9039
|
return result[0] ?? null;
|
|
8878
9040
|
}
|
|
8879
9041
|
/** Fire-and-forget from the verification path. */
|
|
8880
|
-
async updateLastUsedById(
|
|
8881
|
-
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq18(opsTokens.id,
|
|
9042
|
+
async updateLastUsedById(id22) {
|
|
9043
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq18(opsTokens.id, id22));
|
|
8882
9044
|
}
|
|
8883
9045
|
};
|
|
8884
9046
|
opsTokensRepository = new OpsTokensRepository();
|
|
8885
9047
|
}
|
|
8886
9048
|
});
|
|
8887
9049
|
|
|
9050
|
+
// src/server/repositories/oauth2-clients.repository.ts
|
|
9051
|
+
import { and as and15, eq as eq19, gt as gt7, lt as lt6, sql as sql8 } from "drizzle-orm";
|
|
9052
|
+
import { BaseRepository as BaseRepository19, runInTransaction } from "@spfn/core/db";
|
|
9053
|
+
var OAuth2ClientsRepository, oauth2ClientsRepository;
|
|
9054
|
+
var init_oauth2_clients_repository = __esm({
|
|
9055
|
+
"src/server/repositories/oauth2-clients.repository.ts"() {
|
|
9056
|
+
"use strict";
|
|
9057
|
+
init_oauth2_clients();
|
|
9058
|
+
init_oauth2_grants();
|
|
9059
|
+
OAuth2ClientsRepository = class extends BaseRepository19 {
|
|
9060
|
+
async create(data) {
|
|
9061
|
+
const result = await this.db.insert(oauth2Clients).values(data).returning();
|
|
9062
|
+
return result[0];
|
|
9063
|
+
}
|
|
9064
|
+
/**
|
|
9065
|
+
* Lookup by the public `client_id` — the authorize and token paths.
|
|
9066
|
+
*
|
|
9067
|
+
* Reads the primary, not the replica, for the reason the ops-token lookup
|
|
9068
|
+
* does: a client registered a moment ago is immediately used, and a replica
|
|
9069
|
+
* read would answer "unknown client" for the length of the replication lag,
|
|
9070
|
+
* at the one moment a CLI is being connected.
|
|
9071
|
+
*/
|
|
9072
|
+
async findByClientId(clientId) {
|
|
9073
|
+
const result = await this.db.select().from(oauth2Clients).where(eq19(oauth2Clients.clientId, clientId)).limit(1);
|
|
9074
|
+
return result[0] ?? null;
|
|
9075
|
+
}
|
|
9076
|
+
/** Fire-and-forget from the token-issuing path. */
|
|
9077
|
+
async updateLastUsedById(id22) {
|
|
9078
|
+
await this.db.update(oauth2Clients).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq19(oauth2Clients.id, id22));
|
|
9079
|
+
}
|
|
9080
|
+
/**
|
|
9081
|
+
* Register a client unless this address is already at its standing cap.
|
|
9082
|
+
*
|
|
9083
|
+
* The count and the insert are one transaction whose first statement takes
|
|
9084
|
+
* an advisory lock on the address, and all three parts are load-bearing.
|
|
9085
|
+
* Counting on the replica lets replication lag hand out slots that are
|
|
9086
|
+
* already taken. Counting and inserting in two statements lets a second
|
|
9087
|
+
* registration read the same total between them. And a transaction alone
|
|
9088
|
+
* does not close that: under READ COMMITTED each concurrent transaction
|
|
9089
|
+
* counts the rows the others have not committed yet, so twenty requests
|
|
9090
|
+
* arriving together all see zero. The lock is the only part that makes the
|
|
9091
|
+
* cap a number rather than an average, and it is per address, so it costs
|
|
9092
|
+
* nobody else anything.
|
|
9093
|
+
*
|
|
9094
|
+
* @param data - The row to write when there is room for it
|
|
9095
|
+
* @param limit - The address, its cap, and how far back the cap looks
|
|
9096
|
+
* @returns the registered client, or null when the address is at its cap
|
|
9097
|
+
*/
|
|
9098
|
+
async createWithinStandingCap(data, limit) {
|
|
9099
|
+
return await runInTransaction(async () => {
|
|
9100
|
+
await this.db.execute(sql8`select pg_advisory_xact_lock(hashtext(${limit.ip}))`);
|
|
9101
|
+
const standing = await this.countRecentUngrantedByIp(limit.ip, limit.windowMs);
|
|
9102
|
+
return standing < limit.max ? await this.create(data) : null;
|
|
9103
|
+
});
|
|
9104
|
+
}
|
|
9105
|
+
/**
|
|
9106
|
+
* How many clients this IP registered within the window that nobody has
|
|
9107
|
+
* approved yet.
|
|
9108
|
+
*
|
|
9109
|
+
* The window is what makes this a standing-population cap rather than a
|
|
9110
|
+
* permanent quota. Rows are only freed by the purge job, which sweeps once
|
|
9111
|
+
* a day against a 24-hour threshold — so without a window a row registered
|
|
9112
|
+
* a minute after one sweep holds its slot for nearly two days, and twenty
|
|
9113
|
+
* developers behind one NAT lock their whole office out of registering.
|
|
9114
|
+
* A grant against the client takes it out of the count at any age: a client
|
|
9115
|
+
* somebody approved is not junk.
|
|
9116
|
+
*
|
|
9117
|
+
* Reads the primary, inside the caller's transaction, for the reason
|
|
9118
|
+
* `findByClientId` does — a row written a moment ago is exactly the row this
|
|
9119
|
+
* count exists to see.
|
|
9120
|
+
*/
|
|
9121
|
+
async countRecentUngrantedByIp(ip, windowMs) {
|
|
9122
|
+
const result = await this.db.select({ count: sql8`count(*)::int` }).from(oauth2Clients).where(
|
|
9123
|
+
and15(
|
|
9124
|
+
eq19(oauth2Clients.createdIp, ip),
|
|
9125
|
+
gt7(oauth2Clients.createdAt, new Date(Date.now() - windowMs)),
|
|
9126
|
+
sql8`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
9127
|
+
)
|
|
9128
|
+
);
|
|
9129
|
+
return result[0]?.count ?? 0;
|
|
9130
|
+
}
|
|
9131
|
+
/**
|
|
9132
|
+
* Delete clients older than `before` that no user ever approved.
|
|
9133
|
+
*
|
|
9134
|
+
* A registration is an unauthenticated write, so the table fills with rows
|
|
9135
|
+
* from installs that were abandoned at the consent screen. A row with a
|
|
9136
|
+
* grant against it is never touched, whatever its age — that is somebody's
|
|
9137
|
+
* connected CLI.
|
|
9138
|
+
*
|
|
9139
|
+
* @returns how many rows this sweep removed
|
|
9140
|
+
*/
|
|
9141
|
+
async deleteStaleUngranted(before) {
|
|
9142
|
+
const deleted = await this.db.delete(oauth2Clients).where(
|
|
9143
|
+
and15(
|
|
9144
|
+
lt6(oauth2Clients.createdAt, before),
|
|
9145
|
+
sql8`not exists (select 1 from ${oauth2Grants} where ${oauth2Grants.client} = ${oauth2Clients.id})`
|
|
9146
|
+
)
|
|
9147
|
+
).returning({ id: oauth2Clients.id });
|
|
9148
|
+
return deleted.length;
|
|
9149
|
+
}
|
|
9150
|
+
};
|
|
9151
|
+
oauth2ClientsRepository = new OAuth2ClientsRepository();
|
|
9152
|
+
}
|
|
9153
|
+
});
|
|
9154
|
+
|
|
9155
|
+
// src/server/repositories/oauth2-grants.repository.ts
|
|
9156
|
+
import { and as and16, desc as desc5, eq as eq20, inArray as inArray3, isNull as isNull9, sql as sql9 } from "drizzle-orm";
|
|
9157
|
+
import { BaseRepository as BaseRepository20 } from "@spfn/core/db";
|
|
9158
|
+
var OAuth2GrantsRepository, oauth2GrantsRepository;
|
|
9159
|
+
var init_oauth2_grants_repository = __esm({
|
|
9160
|
+
"src/server/repositories/oauth2-grants.repository.ts"() {
|
|
9161
|
+
"use strict";
|
|
9162
|
+
init_oauth2_grants();
|
|
9163
|
+
init_oauth2_clients();
|
|
9164
|
+
init_oauth2_tokens();
|
|
9165
|
+
OAuth2GrantsRepository = class extends BaseRepository20 {
|
|
9166
|
+
/**
|
|
9167
|
+
* Record a consent, or refresh the scopes of the one already there.
|
|
9168
|
+
*
|
|
9169
|
+
* Re-consenting is an upsert and not an insert: the unique index on
|
|
9170
|
+
* (client, user, resource) says one consent per triple, and a user who
|
|
9171
|
+
* approves a wider scope set is amending the consent they already gave, not
|
|
9172
|
+
* giving a second one. Revoking the entry they can see must revoke
|
|
9173
|
+
* everything the client holds, which a second row would break.
|
|
9174
|
+
*
|
|
9175
|
+
* `revokedAt: null` in the update is deliberate — approving again after
|
|
9176
|
+
* revoking is how a user reconnects a CLI they cut off.
|
|
9177
|
+
*/
|
|
9178
|
+
async upsert(data) {
|
|
9179
|
+
const result = await this.db.insert(oauth2Grants).values(data).onConflictDoUpdate({
|
|
9180
|
+
target: [oauth2Grants.client, oauth2Grants.user, oauth2Grants.resource],
|
|
9181
|
+
set: { scopes: data.scopes, revokedAt: null, updatedAt: /* @__PURE__ */ new Date() }
|
|
9182
|
+
}).returning();
|
|
9183
|
+
return result[0];
|
|
9184
|
+
}
|
|
9185
|
+
/**
|
|
9186
|
+
* A grant and its client in one read — the shape the token and verification
|
|
9187
|
+
* paths need, since both have to check the client the caller claims and the
|
|
9188
|
+
* grant's revocation at once.
|
|
9189
|
+
*
|
|
9190
|
+
* Read primary: revocation is documented as taking effect immediately.
|
|
9191
|
+
*/
|
|
9192
|
+
async findWithClientById(id22) {
|
|
9193
|
+
const result = await this.db.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq20(oauth2Grants.client, oauth2Clients.id)).where(eq20(oauth2Grants.id, id22)).limit(1);
|
|
9194
|
+
return result[0] ?? null;
|
|
9195
|
+
}
|
|
9196
|
+
/** What the user's "connected apps" screen lists. Read replica. */
|
|
9197
|
+
async listActiveByUserId(userId) {
|
|
9198
|
+
return await this.readDb.select({ grant: oauth2Grants, client: oauth2Clients }).from(oauth2Grants).innerJoin(oauth2Clients, eq20(oauth2Grants.client, oauth2Clients.id)).where(and16(eq20(oauth2Grants.user, userId), isNull9(oauth2Grants.revokedAt))).orderBy(desc5(oauth2Grants.createdAt));
|
|
9199
|
+
}
|
|
9200
|
+
/**
|
|
9201
|
+
* Revoke one live grant belonging to one user.
|
|
9202
|
+
*
|
|
9203
|
+
* The user id is part of the condition rather than checked beforehand: the
|
|
9204
|
+
* id in the URL comes from a caller, and a grant that is not theirs must not
|
|
9205
|
+
* be revocable by guessing a number. A miss is indistinguishable from an id
|
|
9206
|
+
* that does not exist, which is the answer the route gives.
|
|
9207
|
+
*
|
|
9208
|
+
* @returns the revoked row, or null if there was no live grant of that id
|
|
9209
|
+
* for that user
|
|
9210
|
+
*/
|
|
9211
|
+
async revokeByIdForUser(id22, userId) {
|
|
9212
|
+
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(
|
|
9213
|
+
and16(
|
|
9214
|
+
eq20(oauth2Grants.id, id22),
|
|
9215
|
+
eq20(oauth2Grants.user, userId),
|
|
9216
|
+
isNull9(oauth2Grants.revokedAt)
|
|
9217
|
+
)
|
|
9218
|
+
).returning();
|
|
9219
|
+
return result[0] ?? null;
|
|
9220
|
+
}
|
|
9221
|
+
/** Revoke one grant by id, whoever it belongs to — the replay detections. */
|
|
9222
|
+
async revokeById(id22) {
|
|
9223
|
+
const result = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and16(eq20(oauth2Grants.id, id22), isNull9(oauth2Grants.revokedAt))).returning();
|
|
9224
|
+
return result[0] ?? null;
|
|
9225
|
+
}
|
|
9226
|
+
/**
|
|
9227
|
+
* Revoke every live grant a user has — the authorization-server half of a
|
|
9228
|
+
* global revocation, beside `denyAllActiveByUserId`.
|
|
9229
|
+
*
|
|
9230
|
+
* @returns the grant ids this call revoked
|
|
9231
|
+
*/
|
|
9232
|
+
async revokeAllActiveByUserId(userId) {
|
|
9233
|
+
const revoked = await this.db.update(oauth2Grants).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and16(eq20(oauth2Grants.user, userId), isNull9(oauth2Grants.revokedAt))).returning({ id: oauth2Grants.id });
|
|
9234
|
+
return revoked.map((row) => row.id);
|
|
9235
|
+
}
|
|
9236
|
+
/**
|
|
9237
|
+
* Revoke every live token under the given grants.
|
|
9238
|
+
*
|
|
9239
|
+
* Verification already refuses a token whose grant is revoked, so this is
|
|
9240
|
+
* belt and braces — but the belt is what a `SELECT` against this table shows
|
|
9241
|
+
* an operator, and a live-looking row under a dead grant reads as a hole.
|
|
9242
|
+
*
|
|
9243
|
+
* @returns how many tokens this call revoked
|
|
9244
|
+
*/
|
|
9245
|
+
async revokeTokensOfGrants(grantIds) {
|
|
9246
|
+
if (grantIds.length === 0) {
|
|
9247
|
+
return 0;
|
|
9248
|
+
}
|
|
9249
|
+
const revoked = await this.db.update(oauth2Tokens).set({ revokedAt: sql9`now()` }).where(and16(inArray3(oauth2Tokens.grant, grantIds), isNull9(oauth2Tokens.revokedAt))).returning({ id: oauth2Tokens.id });
|
|
9250
|
+
return revoked.length;
|
|
9251
|
+
}
|
|
9252
|
+
};
|
|
9253
|
+
oauth2GrantsRepository = new OAuth2GrantsRepository();
|
|
9254
|
+
}
|
|
9255
|
+
});
|
|
9256
|
+
|
|
9257
|
+
// src/server/repositories/oauth2-authorization-codes.repository.ts
|
|
9258
|
+
import { and as and17, eq as eq21, gt as gt8, isNull as isNull10, sql as sql10 } from "drizzle-orm";
|
|
9259
|
+
import { BaseRepository as BaseRepository21 } from "@spfn/core/db";
|
|
9260
|
+
var OAuth2AuthorizationCodesRepository, oauth2AuthorizationCodesRepository;
|
|
9261
|
+
var init_oauth2_authorization_codes_repository = __esm({
|
|
9262
|
+
"src/server/repositories/oauth2-authorization-codes.repository.ts"() {
|
|
9263
|
+
"use strict";
|
|
9264
|
+
init_oauth2_authorization_codes();
|
|
9265
|
+
OAuth2AuthorizationCodesRepository = class extends BaseRepository21 {
|
|
9266
|
+
async create(data) {
|
|
9267
|
+
const result = await this.db.insert(oauth2AuthorizationCodes).values(data).returning();
|
|
9268
|
+
return result[0];
|
|
9269
|
+
}
|
|
9270
|
+
/**
|
|
9271
|
+
* Spend a code, addressed by the hash the caller actually presented.
|
|
9272
|
+
*
|
|
9273
|
+
* @returns the row this call spent, or null when it was unknown, already
|
|
9274
|
+
* spent, or past its 60 seconds
|
|
9275
|
+
*/
|
|
9276
|
+
async consume(codeHash) {
|
|
9277
|
+
const result = await this.db.update(oauth2AuthorizationCodes).set({ usedAt: sql10`now()` }).where(
|
|
9278
|
+
and17(
|
|
9279
|
+
eq21(oauth2AuthorizationCodes.codeHash, codeHash),
|
|
9280
|
+
isNull10(oauth2AuthorizationCodes.usedAt),
|
|
9281
|
+
gt8(oauth2AuthorizationCodes.expiresAt, sql10`now()`)
|
|
9282
|
+
)
|
|
9283
|
+
).returning();
|
|
9284
|
+
return result[0] ?? null;
|
|
9285
|
+
}
|
|
9286
|
+
/**
|
|
9287
|
+
* The row behind a miss, in any state. Deliberately unfiltered — which
|
|
9288
|
+
* refusal is owed is the service's decision, and a row filtered out here
|
|
9289
|
+
* would be indistinguishable from a code that never existed.
|
|
9290
|
+
*/
|
|
9291
|
+
async findByCodeHash(codeHash) {
|
|
9292
|
+
const result = await this.db.select().from(oauth2AuthorizationCodes).where(eq21(oauth2AuthorizationCodes.codeHash, codeHash)).limit(1);
|
|
9293
|
+
return result[0] ?? null;
|
|
9294
|
+
}
|
|
9295
|
+
};
|
|
9296
|
+
oauth2AuthorizationCodesRepository = new OAuth2AuthorizationCodesRepository();
|
|
9297
|
+
}
|
|
9298
|
+
});
|
|
9299
|
+
|
|
9300
|
+
// src/server/repositories/oauth2-tokens.repository.ts
|
|
9301
|
+
import { and as and18, eq as eq22, gt as gt9, isNull as isNull11, sql as sql11 } from "drizzle-orm";
|
|
9302
|
+
import { BaseRepository as BaseRepository22 } from "@spfn/core/db";
|
|
9303
|
+
var OAuth2TokensRepository, oauth2TokensRepository;
|
|
9304
|
+
var init_oauth2_tokens_repository = __esm({
|
|
9305
|
+
"src/server/repositories/oauth2-tokens.repository.ts"() {
|
|
9306
|
+
"use strict";
|
|
9307
|
+
init_oauth2_tokens();
|
|
9308
|
+
OAuth2TokensRepository = class extends BaseRepository22 {
|
|
9309
|
+
async create(data) {
|
|
9310
|
+
const result = await this.db.insert(oauth2Tokens).values(data).returning();
|
|
9311
|
+
return result[0];
|
|
9312
|
+
}
|
|
9313
|
+
/**
|
|
9314
|
+
* Lookup by the token's hash — the verification and refresh paths.
|
|
9315
|
+
*
|
|
9316
|
+
* Primary, not replica: revocation is a button the user presses and is
|
|
9317
|
+
* documented as taking effect immediately, so a replica read would keep
|
|
9318
|
+
* authenticating a revoked token for the length of the replication lag.
|
|
9319
|
+
* Unfiltered, so the service can tell revoked from expired from unknown.
|
|
9320
|
+
*/
|
|
9321
|
+
async findByTokenHash(tokenHash) {
|
|
9322
|
+
const result = await this.db.select().from(oauth2Tokens).where(eq22(oauth2Tokens.tokenHash, tokenHash)).limit(1);
|
|
9323
|
+
return result[0] ?? null;
|
|
9324
|
+
}
|
|
9325
|
+
/**
|
|
9326
|
+
* Mark a live refresh token as replaced, addressed by its hash.
|
|
9327
|
+
*
|
|
9328
|
+
* @returns the row this call replaced, or null when it was unknown, already
|
|
9329
|
+
* rotated, revoked, or expired
|
|
9330
|
+
*/
|
|
9331
|
+
async rotate(tokenHash) {
|
|
9332
|
+
const result = await this.db.update(oauth2Tokens).set({ replacedAt: sql11`now()` }).where(
|
|
9333
|
+
and18(
|
|
9334
|
+
eq22(oauth2Tokens.tokenHash, tokenHash),
|
|
9335
|
+
eq22(oauth2Tokens.kind, "refresh"),
|
|
9336
|
+
isNull11(oauth2Tokens.replacedAt),
|
|
9337
|
+
isNull11(oauth2Tokens.revokedAt),
|
|
9338
|
+
gt9(oauth2Tokens.expiresAt, sql11`now()`)
|
|
9339
|
+
)
|
|
9340
|
+
).returning();
|
|
9341
|
+
return result[0] ?? null;
|
|
9342
|
+
}
|
|
9343
|
+
/**
|
|
9344
|
+
* Revoke a live token by hash — RFC 7009.
|
|
9345
|
+
*
|
|
9346
|
+
* @returns the revoked row, or null when there was nothing live to revoke.
|
|
9347
|
+
* The endpoint answers 200 either way; the caller learns nothing
|
|
9348
|
+
* about whether the value it presented ever existed.
|
|
9349
|
+
*/
|
|
9350
|
+
async revokeByTokenHash(tokenHash) {
|
|
9351
|
+
const result = await this.db.update(oauth2Tokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and18(eq22(oauth2Tokens.tokenHash, tokenHash), isNull11(oauth2Tokens.revokedAt))).returning();
|
|
9352
|
+
return result[0] ?? null;
|
|
9353
|
+
}
|
|
9354
|
+
/** Fire-and-forget from the verification path, as ops tokens do. */
|
|
9355
|
+
async updateLastUsedById(id22) {
|
|
9356
|
+
await this.db.update(oauth2Tokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq22(oauth2Tokens.id, id22));
|
|
9357
|
+
}
|
|
9358
|
+
};
|
|
9359
|
+
oauth2TokensRepository = new OAuth2TokensRepository();
|
|
9360
|
+
}
|
|
9361
|
+
});
|
|
9362
|
+
|
|
8888
9363
|
// src/server/repositories/index.ts
|
|
8889
9364
|
var init_repositories = __esm({
|
|
8890
9365
|
"src/server/repositories/index.ts"() {
|
|
@@ -8907,6 +9382,10 @@ var init_repositories = __esm({
|
|
|
8907
9382
|
init_auth_metadata_repository();
|
|
8908
9383
|
init_account_deletion_requests_repository();
|
|
8909
9384
|
init_ops_tokens_repository();
|
|
9385
|
+
init_oauth2_clients_repository();
|
|
9386
|
+
init_oauth2_grants_repository();
|
|
9387
|
+
init_oauth2_authorization_codes_repository();
|
|
9388
|
+
init_oauth2_tokens_repository();
|
|
8910
9389
|
}
|
|
8911
9390
|
});
|
|
8912
9391
|
|
|
@@ -8998,7 +9477,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
8998
9477
|
}
|
|
8999
9478
|
async function setRolePermissions(roleId, permissionIds) {
|
|
9000
9479
|
const roleIdNum = Number(roleId);
|
|
9001
|
-
const permissionIdNums = permissionIds.map((
|
|
9480
|
+
const permissionIdNums = permissionIds.map((id22) => Number(id22));
|
|
9002
9481
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
9003
9482
|
}
|
|
9004
9483
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -9018,7 +9497,7 @@ async function getRolePermissions(roleId) {
|
|
|
9018
9497
|
}
|
|
9019
9498
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
9020
9499
|
const perms = await Promise.all(
|
|
9021
|
-
permissionIds.map((
|
|
9500
|
+
permissionIds.map((id22) => permissionsRepository.findById(id22))
|
|
9022
9501
|
);
|
|
9023
9502
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
9024
9503
|
}
|
|
@@ -9266,10 +9745,9 @@ import {
|
|
|
9266
9745
|
VerificationTokenTargetMismatchError as VerificationTokenTargetMismatchError2
|
|
9267
9746
|
} from "@spfn/auth/errors";
|
|
9268
9747
|
|
|
9269
|
-
// src/server/
|
|
9270
|
-
|
|
9271
|
-
import {
|
|
9272
|
-
import { PasskeyConfigError } from "@spfn/auth/errors";
|
|
9748
|
+
// src/server/services/oauth2-grant.service.ts
|
|
9749
|
+
init_oauth2_grants_repository();
|
|
9750
|
+
import { OAuth2GrantNotFoundError } from "@spfn/auth/errors";
|
|
9273
9751
|
|
|
9274
9752
|
// src/server/logger.ts
|
|
9275
9753
|
import { logger as rootLogger } from "@spfn/core/logger";
|
|
@@ -9290,7 +9768,43 @@ var authLogger = {
|
|
|
9290
9768
|
sms: rootLogger.child("@spfn/auth:sms")
|
|
9291
9769
|
};
|
|
9292
9770
|
|
|
9771
|
+
// src/server/services/oauth2-grant.service.ts
|
|
9772
|
+
async function listOAuth2GrantsService(userId) {
|
|
9773
|
+
const rows = await oauth2GrantsRepository.listActiveByUserId(userId);
|
|
9774
|
+
return rows.map(({ grant, client }) => ({
|
|
9775
|
+
id: Number(grant.id),
|
|
9776
|
+
clientId: client.clientId,
|
|
9777
|
+
clientName: client.clientName,
|
|
9778
|
+
resource: grant.resource,
|
|
9779
|
+
scopes: grant.scopes,
|
|
9780
|
+
createdAtMillis: grant.createdAt.getTime(),
|
|
9781
|
+
lastUsedAtMillis: client.lastUsedAt?.getTime()
|
|
9782
|
+
}));
|
|
9783
|
+
}
|
|
9784
|
+
async function revokeOAuth2GrantService(id22, userId) {
|
|
9785
|
+
const revoked = await oauth2GrantsRepository.revokeByIdForUser(id22, userId);
|
|
9786
|
+
if (!revoked) {
|
|
9787
|
+
throw new OAuth2GrantNotFoundError();
|
|
9788
|
+
}
|
|
9789
|
+
await oauth2GrantsRepository.revokeTokensOfGrants([revoked.id]);
|
|
9790
|
+
}
|
|
9791
|
+
async function revokeAllOAuth2GrantsForUser(userId) {
|
|
9792
|
+
const grantIds = await oauth2GrantsRepository.revokeAllActiveByUserId(userId);
|
|
9793
|
+
if (grantIds.length === 0) {
|
|
9794
|
+
return;
|
|
9795
|
+
}
|
|
9796
|
+
const tokens = await oauth2GrantsRepository.revokeTokensOfGrants(grantIds);
|
|
9797
|
+
authLogger.service.info("OAuth2 grants revoked for a user", {
|
|
9798
|
+
userId,
|
|
9799
|
+
grants: grantIds.length,
|
|
9800
|
+
tokens
|
|
9801
|
+
});
|
|
9802
|
+
}
|
|
9803
|
+
|
|
9293
9804
|
// src/server/lib/config.ts
|
|
9805
|
+
init_email();
|
|
9806
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
9807
|
+
import { PasskeyConfigError } from "@spfn/auth/errors";
|
|
9294
9808
|
function getCookieSuffix() {
|
|
9295
9809
|
const port = process.env.SPFN_PORT;
|
|
9296
9810
|
return port ? `_${port}` : "";
|
|
@@ -9355,10 +9869,10 @@ var globalConfig = {
|
|
|
9355
9869
|
sessionTtl: "7d"
|
|
9356
9870
|
// Default: 7 days
|
|
9357
9871
|
};
|
|
9358
|
-
function configureAuth(
|
|
9872
|
+
function configureAuth(config4) {
|
|
9359
9873
|
globalConfig = {
|
|
9360
9874
|
...globalConfig,
|
|
9361
|
-
...
|
|
9875
|
+
...config4
|
|
9362
9876
|
};
|
|
9363
9877
|
}
|
|
9364
9878
|
function getAuthConfig() {
|
|
@@ -9402,8 +9916,13 @@ function getCsrfMode() {
|
|
|
9402
9916
|
}
|
|
9403
9917
|
return normalized;
|
|
9404
9918
|
}
|
|
9919
|
+
var PACKAGE_CSRF_EXEMPT_PATHS = [
|
|
9920
|
+
"/_auth/oauth2/register",
|
|
9921
|
+
"/_auth/oauth2/token",
|
|
9922
|
+
"/_auth/oauth2/revoke"
|
|
9923
|
+
];
|
|
9405
9924
|
function getCsrfExemptPaths() {
|
|
9406
|
-
return globalConfig.csrf?.exemptPaths ?? [];
|
|
9925
|
+
return [...PACKAGE_CSRF_EXEMPT_PATHS, ...globalConfig.csrf?.exemptPaths ?? []];
|
|
9407
9926
|
}
|
|
9408
9927
|
var PASSKEY_USER_VERIFICATIONS = ["preferred", "required"];
|
|
9409
9928
|
var DEFAULT_CHALLENGE_TTL_SECONDS = 300;
|
|
@@ -9963,6 +10482,7 @@ async function revokeAllKeysService(params) {
|
|
|
9963
10482
|
const { userId, currentKeyId, includeCurrent = false, reason } = params;
|
|
9964
10483
|
const revoked = includeCurrent ? await keysRepository.revokeAllActiveByUserId(userId, reason) : await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason);
|
|
9965
10484
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
10485
|
+
await revokeAllOAuth2GrantsForUser(userId);
|
|
9966
10486
|
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
9967
10487
|
}
|
|
9968
10488
|
|
|
@@ -10040,7 +10560,7 @@ async function updateUsernameService(userId, username) {
|
|
|
10040
10560
|
// src/server/services/account-deletion.service.ts
|
|
10041
10561
|
init_repositories();
|
|
10042
10562
|
import { ValidationError as ValidationError2, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
|
|
10043
|
-
import { runInTransaction, onAfterCommit } from "@spfn/core/db";
|
|
10563
|
+
import { runInTransaction as runInTransaction2, onAfterCommit } from "@spfn/core/db";
|
|
10044
10564
|
import { sendEmail as sendEmail3 } from "@spfn/notification/server";
|
|
10045
10565
|
import {
|
|
10046
10566
|
InvalidCredentialsError,
|
|
@@ -10230,8 +10750,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
10230
10750
|
throw new VerificationTokenTargetMismatchError();
|
|
10231
10751
|
}
|
|
10232
10752
|
}
|
|
10233
|
-
async function sendDeletionEmail(to, subject,
|
|
10234
|
-
const result = await sendEmail3({ to, subject, text:
|
|
10753
|
+
async function sendDeletionEmail(to, subject, text22) {
|
|
10754
|
+
const result = await sendEmail3({ to, subject, text: text22 });
|
|
10235
10755
|
if (!result.success) {
|
|
10236
10756
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
10237
10757
|
}
|
|
@@ -10283,12 +10803,12 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
10283
10803
|
if (requestedBy === "self") {
|
|
10284
10804
|
await verifyReauthCredential(user, { password, verificationToken });
|
|
10285
10805
|
}
|
|
10286
|
-
const
|
|
10806
|
+
const config4 = getDeletionConfig();
|
|
10287
10807
|
const wantsImmediate = immediate === true;
|
|
10288
|
-
if (wantsImmediate && requestedBy === "self" && !
|
|
10808
|
+
if (wantsImmediate && requestedBy === "self" && !config4.allowSelfImmediate) {
|
|
10289
10809
|
throw new ImmediateDeletionNotAllowedError();
|
|
10290
10810
|
}
|
|
10291
|
-
const gracePeriodDays = wantsImmediate ? 0 :
|
|
10811
|
+
const gracePeriodDays = wantsImmediate ? 0 : config4.gracePeriodDays;
|
|
10292
10812
|
const requestedAt = /* @__PURE__ */ new Date();
|
|
10293
10813
|
const purgeScheduledAt = addDays(requestedAt, gracePeriodDays);
|
|
10294
10814
|
await usersRepository.updateById(user.id, { status: "pending_deletion" });
|
|
@@ -10311,6 +10831,7 @@ async function requestAccountDeletionService(userId, params) {
|
|
|
10311
10831
|
}
|
|
10312
10832
|
await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
|
|
10313
10833
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
10834
|
+
await revokeAllOAuth2GrantsForUser(user.id);
|
|
10314
10835
|
onAfterCommit(() => authDeletionRequestedEvent.emit({
|
|
10315
10836
|
userId: String(user.id),
|
|
10316
10837
|
userPublicId: user.publicId,
|
|
@@ -10394,10 +10915,10 @@ async function purgePendingRequest(request) {
|
|
|
10394
10915
|
if (!precheckUser || precheckUser.status !== "pending_deletion") {
|
|
10395
10916
|
return { outcome: "skipped" };
|
|
10396
10917
|
}
|
|
10397
|
-
const
|
|
10398
|
-
if (
|
|
10918
|
+
const config4 = getDeletionConfig();
|
|
10919
|
+
if (config4.onBeforePurge) {
|
|
10399
10920
|
try {
|
|
10400
|
-
await
|
|
10921
|
+
await config4.onBeforePurge({
|
|
10401
10922
|
id: precheckUser.id,
|
|
10402
10923
|
publicId: precheckUser.publicId,
|
|
10403
10924
|
email: precheckUser.email,
|
|
@@ -10411,9 +10932,9 @@ async function purgePendingRequest(request) {
|
|
|
10411
10932
|
return { outcome: "skipped" };
|
|
10412
10933
|
}
|
|
10413
10934
|
}
|
|
10414
|
-
const purgeStrategy =
|
|
10935
|
+
const purgeStrategy = config4.purgeStrategy;
|
|
10415
10936
|
let purgedUser = null;
|
|
10416
|
-
await
|
|
10937
|
+
await runInTransaction2(async () => {
|
|
10417
10938
|
const user = await usersRepository.findById(userId);
|
|
10418
10939
|
if (!user || user.status !== "pending_deletion") {
|
|
10419
10940
|
return;
|
|
@@ -10634,6 +11155,7 @@ async function changePasswordService(params) {
|
|
|
10634
11155
|
const newPasswordHash = await hashPassword(newPassword);
|
|
10635
11156
|
await usersRepository.updatePassword(userId, newPasswordHash, true);
|
|
10636
11157
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(userId);
|
|
11158
|
+
await revokeAllOAuth2GrantsForUser(userId);
|
|
10637
11159
|
await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
|
|
10638
11160
|
}
|
|
10639
11161
|
|
|
@@ -10829,6 +11351,7 @@ async function replaceCredentials(row, user, params) {
|
|
|
10829
11351
|
...user.emailVerifiedAt ? {} : { emailVerifiedAt: /* @__PURE__ */ new Date() }
|
|
10830
11352
|
});
|
|
10831
11353
|
await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
|
|
11354
|
+
await revokeAllOAuth2GrantsForUser(user.id);
|
|
10832
11355
|
await keysRepository.revokeAllActiveByUserId(user.id, "Revoked by password reset");
|
|
10833
11356
|
await registerPublicKeyService({
|
|
10834
11357
|
userId: user.id,
|
|
@@ -11106,7 +11629,7 @@ async function completeDeviceLogin(record) {
|
|
|
11106
11629
|
|
|
11107
11630
|
// src/server/services/passkey.service.ts
|
|
11108
11631
|
import crypto6 from "crypto";
|
|
11109
|
-
import { onAfterCommit as onAfterCommit4, runInTransaction as
|
|
11632
|
+
import { onAfterCommit as onAfterCommit4, runInTransaction as runInTransaction3 } from "@spfn/core/db";
|
|
11110
11633
|
import { ValidationError as ValidationError5 } from "@spfn/core/errors";
|
|
11111
11634
|
import {
|
|
11112
11635
|
AccountDisabledError as AccountDisabledError3,
|
|
@@ -11258,9 +11781,9 @@ function presentedChallenge(clientDataJSON) {
|
|
|
11258
11781
|
const decoded = parseJson(Buffer.from(clientDataJSON, "base64url").toString("utf8"));
|
|
11259
11782
|
return typeof decoded?.challenge === "string" ? decoded.challenge : "";
|
|
11260
11783
|
}
|
|
11261
|
-
function parseJson(
|
|
11784
|
+
function parseJson(text22) {
|
|
11262
11785
|
try {
|
|
11263
|
-
return JSON.parse(
|
|
11786
|
+
return JSON.parse(text22);
|
|
11264
11787
|
} catch {
|
|
11265
11788
|
return null;
|
|
11266
11789
|
}
|
|
@@ -11364,7 +11887,7 @@ async function startPasskeyLoginService() {
|
|
|
11364
11887
|
}
|
|
11365
11888
|
async function finishPasskeyLoginService(params) {
|
|
11366
11889
|
assertDeviceKeyPresent(params);
|
|
11367
|
-
return
|
|
11890
|
+
return runInTransaction3(async () => {
|
|
11368
11891
|
const challenge = presentedChallenge(params.response.response.clientDataJSON);
|
|
11369
11892
|
await consumeChallenge(challenge, "authentication", null);
|
|
11370
11893
|
const passkey = await passkeysRepository.findLiveByCredentialId(params.response.id);
|
|
@@ -11563,51 +12086,51 @@ async function initializeAuth(options = {}) {
|
|
|
11563
12086
|
authLogger.service.info("\u{1F512} Built-in roles: user, admin, superadmin");
|
|
11564
12087
|
}
|
|
11565
12088
|
async function syncRoles(configs, existingByName) {
|
|
11566
|
-
for (const
|
|
11567
|
-
const existing = existingByName.get(
|
|
12089
|
+
for (const config4 of configs) {
|
|
12090
|
+
const existing = existingByName.get(config4.name);
|
|
11568
12091
|
if (!existing) {
|
|
11569
12092
|
await rolesRepository.create({
|
|
11570
|
-
name:
|
|
11571
|
-
displayName:
|
|
11572
|
-
description:
|
|
11573
|
-
priority:
|
|
11574
|
-
isSystem:
|
|
11575
|
-
isBuiltin:
|
|
12093
|
+
name: config4.name,
|
|
12094
|
+
displayName: config4.displayName,
|
|
12095
|
+
description: config4.description || null,
|
|
12096
|
+
priority: config4.priority ?? 10,
|
|
12097
|
+
isSystem: config4.isSystem ?? false,
|
|
12098
|
+
isBuiltin: config4.isBuiltin ?? false,
|
|
11576
12099
|
isActive: true
|
|
11577
12100
|
});
|
|
11578
|
-
authLogger.service.info(` \u2705 Created role: ${
|
|
12101
|
+
authLogger.service.info(` \u2705 Created role: ${config4.name}`);
|
|
11579
12102
|
} else {
|
|
11580
12103
|
const updateData = {
|
|
11581
|
-
displayName:
|
|
11582
|
-
description:
|
|
12104
|
+
displayName: config4.displayName,
|
|
12105
|
+
description: config4.description || null
|
|
11583
12106
|
};
|
|
11584
12107
|
if (!existing.isBuiltin) {
|
|
11585
|
-
updateData.priority =
|
|
12108
|
+
updateData.priority = config4.priority ?? existing.priority;
|
|
11586
12109
|
}
|
|
11587
12110
|
await rolesRepository.updateById(existing.id, updateData);
|
|
11588
12111
|
}
|
|
11589
12112
|
}
|
|
11590
12113
|
}
|
|
11591
12114
|
async function syncPermissions(configs, existingByName) {
|
|
11592
|
-
for (const
|
|
11593
|
-
const existing = existingByName.get(
|
|
12115
|
+
for (const config4 of configs) {
|
|
12116
|
+
const existing = existingByName.get(config4.name);
|
|
11594
12117
|
if (!existing) {
|
|
11595
12118
|
await permissionsRepository.create({
|
|
11596
|
-
name:
|
|
11597
|
-
displayName:
|
|
11598
|
-
description:
|
|
11599
|
-
category:
|
|
11600
|
-
isSystem:
|
|
11601
|
-
isBuiltin:
|
|
12119
|
+
name: config4.name,
|
|
12120
|
+
displayName: config4.displayName,
|
|
12121
|
+
description: config4.description || null,
|
|
12122
|
+
category: config4.category || null,
|
|
12123
|
+
isSystem: config4.isSystem ?? false,
|
|
12124
|
+
isBuiltin: config4.isBuiltin ?? false,
|
|
11602
12125
|
isActive: true,
|
|
11603
12126
|
metadata: null
|
|
11604
12127
|
});
|
|
11605
|
-
authLogger.service.info(` \u2705 Created permission: ${
|
|
12128
|
+
authLogger.service.info(` \u2705 Created permission: ${config4.name}`);
|
|
11606
12129
|
} else {
|
|
11607
12130
|
await permissionsRepository.updateById(existing.id, {
|
|
11608
|
-
displayName:
|
|
11609
|
-
description:
|
|
11610
|
-
category:
|
|
12131
|
+
displayName: config4.displayName,
|
|
12132
|
+
description: config4.description || null,
|
|
12133
|
+
category: config4.category || null
|
|
11611
12134
|
});
|
|
11612
12135
|
}
|
|
11613
12136
|
}
|
|
@@ -11668,7 +12191,7 @@ async function getUserPermissions(userId) {
|
|
|
11668
12191
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
11669
12192
|
if (permIds.length > 0) {
|
|
11670
12193
|
const rolePerms = await Promise.all(
|
|
11671
|
-
permIds.map((
|
|
12194
|
+
permIds.map((id22) => permissionsRepository.findById(id22))
|
|
11672
12195
|
);
|
|
11673
12196
|
for (const perm of rolePerms) {
|
|
11674
12197
|
if (perm && perm.isActive) {
|
|
@@ -11882,20 +12405,20 @@ async function acceptInvitation(params) {
|
|
|
11882
12405
|
async function listInvitations(params) {
|
|
11883
12406
|
return await invitationsRepository.list(params);
|
|
11884
12407
|
}
|
|
11885
|
-
async function cancelInvitation(
|
|
11886
|
-
const invitation = await invitationsRepository.findById(
|
|
12408
|
+
async function cancelInvitation(id22, cancelledBy, reason) {
|
|
12409
|
+
const invitation = await invitationsRepository.findById(id22);
|
|
11887
12410
|
if (!invitation) {
|
|
11888
12411
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
11889
12412
|
}
|
|
11890
12413
|
if (invitation.status !== "pending") {
|
|
11891
12414
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
11892
12415
|
}
|
|
11893
|
-
await invitationsRepository.cancel(
|
|
12416
|
+
await invitationsRepository.cancel(id22, cancelledBy, reason, invitation.metadata);
|
|
11894
12417
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
11895
12418
|
}
|
|
11896
|
-
async function deleteInvitation(
|
|
11897
|
-
await invitationsRepository.deleteById(
|
|
11898
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
12419
|
+
async function deleteInvitation(id22) {
|
|
12420
|
+
await invitationsRepository.deleteById(id22);
|
|
12421
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id22}`);
|
|
11899
12422
|
}
|
|
11900
12423
|
async function expireOldInvitations() {
|
|
11901
12424
|
const count = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -11904,8 +12427,8 @@ async function expireOldInvitations() {
|
|
|
11904
12427
|
}
|
|
11905
12428
|
return count;
|
|
11906
12429
|
}
|
|
11907
|
-
async function resendInvitation(
|
|
11908
|
-
const invitation = await invitationsRepository.findById(
|
|
12430
|
+
async function resendInvitation(id22, expiresInDays = 7) {
|
|
12431
|
+
const invitation = await invitationsRepository.findById(id22);
|
|
11909
12432
|
if (!invitation) {
|
|
11910
12433
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
11911
12434
|
}
|
|
@@ -11913,7 +12436,7 @@ async function resendInvitation(id18, expiresInDays = 7) {
|
|
|
11913
12436
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
11914
12437
|
}
|
|
11915
12438
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
11916
|
-
const updated = await invitationsRepository.resend(
|
|
12439
|
+
const updated = await invitationsRepository.resend(id22, newExpiresAt);
|
|
11917
12440
|
if (!updated) {
|
|
11918
12441
|
throw new Error("Failed to update invitation");
|
|
11919
12442
|
}
|
|
@@ -11952,13 +12475,13 @@ async function getAuthSessionService(userId) {
|
|
|
11952
12475
|
// src/server/lib/one-time-token.ts
|
|
11953
12476
|
import { SSETokenManager } from "@spfn/core/event/sse";
|
|
11954
12477
|
var manager = null;
|
|
11955
|
-
function initOneTimeTokenManager(
|
|
12478
|
+
function initOneTimeTokenManager(config4) {
|
|
11956
12479
|
if (manager) {
|
|
11957
12480
|
manager.destroy();
|
|
11958
12481
|
}
|
|
11959
12482
|
manager = new SSETokenManager({
|
|
11960
|
-
ttl:
|
|
11961
|
-
store:
|
|
12483
|
+
ttl: config4?.ttl,
|
|
12484
|
+
store: config4?.store
|
|
11962
12485
|
});
|
|
11963
12486
|
}
|
|
11964
12487
|
function getOneTimeTokenManager() {
|
|
@@ -12106,10 +12629,10 @@ function getDefaultScopes() {
|
|
|
12106
12629
|
}
|
|
12107
12630
|
function getGoogleAuthUrl(state, scopes) {
|
|
12108
12631
|
const resolvedScopes = scopes ?? getDefaultScopes();
|
|
12109
|
-
const
|
|
12632
|
+
const config4 = getGoogleOAuthConfig();
|
|
12110
12633
|
const params = new URLSearchParams({
|
|
12111
|
-
client_id:
|
|
12112
|
-
redirect_uri:
|
|
12634
|
+
client_id: config4.clientId,
|
|
12635
|
+
redirect_uri: config4.redirectUri,
|
|
12113
12636
|
response_type: "code",
|
|
12114
12637
|
scope: resolvedScopes.join(" "),
|
|
12115
12638
|
state,
|
|
@@ -12121,16 +12644,16 @@ function getGoogleAuthUrl(state, scopes) {
|
|
|
12121
12644
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
12122
12645
|
}
|
|
12123
12646
|
async function exchangeCodeForTokens(code) {
|
|
12124
|
-
const
|
|
12647
|
+
const config4 = getGoogleOAuthConfig();
|
|
12125
12648
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
12126
12649
|
method: "POST",
|
|
12127
12650
|
headers: {
|
|
12128
12651
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
12129
12652
|
},
|
|
12130
12653
|
body: new URLSearchParams({
|
|
12131
|
-
client_id:
|
|
12132
|
-
client_secret:
|
|
12133
|
-
redirect_uri:
|
|
12654
|
+
client_id: config4.clientId,
|
|
12655
|
+
client_secret: config4.clientSecret,
|
|
12656
|
+
redirect_uri: config4.redirectUri,
|
|
12134
12657
|
grant_type: "authorization_code",
|
|
12135
12658
|
code
|
|
12136
12659
|
})
|
|
@@ -12154,15 +12677,15 @@ async function getGoogleUserInfo(accessToken) {
|
|
|
12154
12677
|
return response.json();
|
|
12155
12678
|
}
|
|
12156
12679
|
async function refreshAccessToken(refreshToken) {
|
|
12157
|
-
const
|
|
12680
|
+
const config4 = getGoogleOAuthConfig();
|
|
12158
12681
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
12159
12682
|
method: "POST",
|
|
12160
12683
|
headers: {
|
|
12161
12684
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
12162
12685
|
},
|
|
12163
12686
|
body: new URLSearchParams({
|
|
12164
|
-
client_id:
|
|
12165
|
-
client_secret:
|
|
12687
|
+
client_id: config4.clientId,
|
|
12688
|
+
client_secret: config4.clientSecret,
|
|
12166
12689
|
refresh_token: refreshToken,
|
|
12167
12690
|
grant_type: "refresh_token"
|
|
12168
12691
|
})
|
|
@@ -12227,8 +12750,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
12227
12750
|
function registerOAuthProvider(provider) {
|
|
12228
12751
|
registry2.set(provider.id, provider);
|
|
12229
12752
|
}
|
|
12230
|
-
function getOAuthProvider(
|
|
12231
|
-
return registry2.get(
|
|
12753
|
+
function getOAuthProvider(id22) {
|
|
12754
|
+
return registry2.get(id22);
|
|
12232
12755
|
}
|
|
12233
12756
|
function getRegisteredProviders() {
|
|
12234
12757
|
return [...registry2.values()];
|
|
@@ -12482,21 +13005,21 @@ var githubProvider = {
|
|
|
12482
13005
|
return !!(env3.SPFN_AUTH_GITHUB_CLIENT_ID && env3.SPFN_AUTH_GITHUB_CLIENT_SECRET);
|
|
12483
13006
|
},
|
|
12484
13007
|
getAuthUrl(state, scopes) {
|
|
12485
|
-
const
|
|
13008
|
+
const config4 = getGithubConfig();
|
|
12486
13009
|
const params = new URLSearchParams({
|
|
12487
|
-
client_id:
|
|
12488
|
-
redirect_uri:
|
|
13010
|
+
client_id: config4.clientId,
|
|
13011
|
+
redirect_uri: config4.redirectUri,
|
|
12489
13012
|
state,
|
|
12490
13013
|
scope: (scopes ?? getGithubScopes()).join(" ")
|
|
12491
13014
|
});
|
|
12492
13015
|
return `${GITHUB_AUTH_URL}?${params.toString()}`;
|
|
12493
13016
|
},
|
|
12494
13017
|
async exchangeCodeForTokens(code) {
|
|
12495
|
-
const
|
|
13018
|
+
const config4 = getGithubConfig();
|
|
12496
13019
|
return requestGithubTokens(new URLSearchParams({
|
|
12497
|
-
client_id:
|
|
12498
|
-
client_secret:
|
|
12499
|
-
redirect_uri:
|
|
13020
|
+
client_id: config4.clientId,
|
|
13021
|
+
client_secret: config4.clientSecret,
|
|
13022
|
+
redirect_uri: config4.redirectUri,
|
|
12500
13023
|
code
|
|
12501
13024
|
}));
|
|
12502
13025
|
},
|
|
@@ -12526,11 +13049,11 @@ var githubProvider = {
|
|
|
12526
13049
|
};
|
|
12527
13050
|
},
|
|
12528
13051
|
async refreshTokens(refreshToken) {
|
|
12529
|
-
const
|
|
13052
|
+
const config4 = getGithubConfig();
|
|
12530
13053
|
return requestGithubTokens(new URLSearchParams({
|
|
12531
13054
|
grant_type: "refresh_token",
|
|
12532
|
-
client_id:
|
|
12533
|
-
client_secret:
|
|
13055
|
+
client_id: config4.clientId,
|
|
13056
|
+
client_secret: config4.clientSecret,
|
|
12534
13057
|
refresh_token: refreshToken
|
|
12535
13058
|
}));
|
|
12536
13059
|
}
|
|
@@ -12653,26 +13176,26 @@ var kakaoProvider = {
|
|
|
12653
13176
|
return !!env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
12654
13177
|
},
|
|
12655
13178
|
getAuthUrl(state, scopes) {
|
|
12656
|
-
const
|
|
13179
|
+
const config4 = getKakaoConfig();
|
|
12657
13180
|
const params = new URLSearchParams({
|
|
12658
13181
|
response_type: "code",
|
|
12659
|
-
client_id:
|
|
12660
|
-
redirect_uri:
|
|
13182
|
+
client_id: config4.clientId,
|
|
13183
|
+
redirect_uri: config4.redirectUri,
|
|
12661
13184
|
state,
|
|
12662
13185
|
scope: (scopes ?? getKakaoScopes()).join(",")
|
|
12663
13186
|
});
|
|
12664
13187
|
return `${KAKAO_AUTH_URL}?${params.toString()}`;
|
|
12665
13188
|
},
|
|
12666
13189
|
async exchangeCodeForTokens(code) {
|
|
12667
|
-
const
|
|
13190
|
+
const config4 = getKakaoConfig();
|
|
12668
13191
|
const params = new URLSearchParams({
|
|
12669
13192
|
grant_type: "authorization_code",
|
|
12670
|
-
client_id:
|
|
12671
|
-
redirect_uri:
|
|
13193
|
+
client_id: config4.clientId,
|
|
13194
|
+
redirect_uri: config4.redirectUri,
|
|
12672
13195
|
code
|
|
12673
13196
|
});
|
|
12674
|
-
if (
|
|
12675
|
-
params.set("client_secret",
|
|
13197
|
+
if (config4.clientSecret) {
|
|
13198
|
+
params.set("client_secret", config4.clientSecret);
|
|
12676
13199
|
}
|
|
12677
13200
|
return requestKakaoTokens(params);
|
|
12678
13201
|
},
|
|
@@ -12714,14 +13237,14 @@ var kakaoProvider = {
|
|
|
12714
13237
|
return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
|
|
12715
13238
|
},
|
|
12716
13239
|
async refreshTokens(refreshToken) {
|
|
12717
|
-
const
|
|
13240
|
+
const config4 = getKakaoConfig();
|
|
12718
13241
|
const params = new URLSearchParams({
|
|
12719
13242
|
grant_type: "refresh_token",
|
|
12720
|
-
client_id:
|
|
13243
|
+
client_id: config4.clientId,
|
|
12721
13244
|
refresh_token: refreshToken
|
|
12722
13245
|
});
|
|
12723
|
-
if (
|
|
12724
|
-
params.set("client_secret",
|
|
13246
|
+
if (config4.clientSecret) {
|
|
13247
|
+
params.set("client_secret", config4.clientSecret);
|
|
12725
13248
|
}
|
|
12726
13249
|
return requestKakaoTokens(params);
|
|
12727
13250
|
},
|
|
@@ -12879,22 +13402,22 @@ var naverProvider = {
|
|
|
12879
13402
|
return !!(env3.SPFN_AUTH_NAVER_CLIENT_ID && env3.SPFN_AUTH_NAVER_CLIENT_SECRET);
|
|
12880
13403
|
},
|
|
12881
13404
|
getAuthUrl(state) {
|
|
12882
|
-
const
|
|
13405
|
+
const config4 = getNaverConfig();
|
|
12883
13406
|
const params = new URLSearchParams({
|
|
12884
13407
|
response_type: "code",
|
|
12885
|
-
client_id:
|
|
12886
|
-
redirect_uri:
|
|
13408
|
+
client_id: config4.clientId,
|
|
13409
|
+
redirect_uri: config4.redirectUri,
|
|
12887
13410
|
state
|
|
12888
13411
|
});
|
|
12889
13412
|
return `${NAVER_AUTH_URL}?${params.toString()}`;
|
|
12890
13413
|
},
|
|
12891
13414
|
async exchangeCodeForTokens(code, options) {
|
|
12892
|
-
const
|
|
13415
|
+
const config4 = getNaverConfig();
|
|
12893
13416
|
return requestNaverTokens(new URLSearchParams({
|
|
12894
13417
|
grant_type: "authorization_code",
|
|
12895
|
-
client_id:
|
|
12896
|
-
client_secret:
|
|
12897
|
-
redirect_uri:
|
|
13418
|
+
client_id: config4.clientId,
|
|
13419
|
+
client_secret: config4.clientSecret,
|
|
13420
|
+
redirect_uri: config4.redirectUri,
|
|
12898
13421
|
code,
|
|
12899
13422
|
state: options.state
|
|
12900
13423
|
}));
|
|
@@ -12935,11 +13458,11 @@ var naverProvider = {
|
|
|
12935
13458
|
return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
|
|
12936
13459
|
},
|
|
12937
13460
|
async refreshTokens(refreshToken) {
|
|
12938
|
-
const
|
|
13461
|
+
const config4 = getNaverConfig();
|
|
12939
13462
|
return requestNaverTokens(new URLSearchParams({
|
|
12940
13463
|
grant_type: "refresh_token",
|
|
12941
|
-
client_id:
|
|
12942
|
-
client_secret:
|
|
13464
|
+
client_id: config4.clientId,
|
|
13465
|
+
client_secret: config4.clientSecret,
|
|
12943
13466
|
refresh_token: refreshToken
|
|
12944
13467
|
}));
|
|
12945
13468
|
},
|
|
@@ -13257,7 +13780,7 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
13257
13780
|
}
|
|
13258
13781
|
|
|
13259
13782
|
// src/server/services/oauth-native.service.ts
|
|
13260
|
-
import { runInTransaction as
|
|
13783
|
+
import { runInTransaction as runInTransaction4, onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
|
|
13261
13784
|
import {
|
|
13262
13785
|
InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
|
|
13263
13786
|
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
@@ -13290,7 +13813,7 @@ function assertNonceBindsPublicKey(params) {
|
|
|
13290
13813
|
}
|
|
13291
13814
|
}
|
|
13292
13815
|
async function persistNativeLogin(identity, params) {
|
|
13293
|
-
return
|
|
13816
|
+
return runInTransaction4(async () => {
|
|
13294
13817
|
const existing = await socialAccountsRepository.findByProviderAndProviderId(
|
|
13295
13818
|
params.provider,
|
|
13296
13819
|
identity.providerUserId
|
|
@@ -13371,89 +13894,818 @@ async function verifyOpsTokenService(token) {
|
|
|
13371
13894
|
scopes: record.scopes
|
|
13372
13895
|
};
|
|
13373
13896
|
}
|
|
13374
|
-
async function revokeOpsTokenService(
|
|
13375
|
-
return await opsTokensRepository.revokeById(
|
|
13897
|
+
async function revokeOpsTokenService(id22) {
|
|
13898
|
+
return await opsTokensRepository.revokeById(id22);
|
|
13376
13899
|
}
|
|
13377
13900
|
async function listOpsTokensService() {
|
|
13378
13901
|
return await opsTokensRepository.list();
|
|
13379
13902
|
}
|
|
13380
13903
|
|
|
13381
|
-
// src/server/
|
|
13382
|
-
|
|
13383
|
-
import {
|
|
13384
|
-
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
13904
|
+
// src/server/services/oauth2-client.service.ts
|
|
13905
|
+
init_oauth2_clients_repository();
|
|
13906
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
13385
13907
|
|
|
13386
|
-
// src/server/lib/
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
return {};
|
|
13395
|
-
}
|
|
13908
|
+
// src/server/lib/oauth2/config.ts
|
|
13909
|
+
var DEFAULT_ACCESS_TOKEN_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
13910
|
+
var DEFAULT_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
13911
|
+
var DEFAULT_CODE_TTL_MS = 60 * 1e3;
|
|
13912
|
+
var AUTHORIZE_PATH = "/oauth/authorize";
|
|
13913
|
+
var config3 = null;
|
|
13914
|
+
function resolveIssuerSource(env19 = process.env) {
|
|
13915
|
+
return { value: env19.SPFN_API_URL, variable: "SPFN_API_URL" };
|
|
13396
13916
|
}
|
|
13397
|
-
function
|
|
13398
|
-
|
|
13399
|
-
|
|
13917
|
+
function resolveAuthorizeUrl(env19) {
|
|
13918
|
+
const appUrl = env19.NEXT_PUBLIC_SPFN_APP_URL || env19.SPFN_APP_URL || "http://localhost:3000";
|
|
13919
|
+
return new URL(AUTHORIZE_PATH, appUrl).toString();
|
|
13920
|
+
}
|
|
13921
|
+
function configureAuthorizationServer(options, env19 = process.env) {
|
|
13922
|
+
if (!options) {
|
|
13923
|
+
config3 = null;
|
|
13924
|
+
return;
|
|
13400
13925
|
}
|
|
13401
|
-
|
|
13402
|
-
|
|
13926
|
+
const scopeNames = Object.keys(options.scopes ?? {});
|
|
13927
|
+
if (scopeNames.length === 0) {
|
|
13928
|
+
throw new Error(
|
|
13929
|
+
"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."
|
|
13930
|
+
);
|
|
13403
13931
|
}
|
|
13404
|
-
|
|
13932
|
+
config3 = {
|
|
13933
|
+
issuer: canonicalIssuer(options.issuer ?? resolveIssuerSource(env19).value ?? ""),
|
|
13934
|
+
issuerSource: options.issuer ? "authorizationServer.issuer" : resolveIssuerSource(env19).variable,
|
|
13935
|
+
authorizeUrl: options.authorizeUrl ?? resolveAuthorizeUrl(env19),
|
|
13936
|
+
scopes: { ...options.scopes },
|
|
13937
|
+
defaultScopes: options.defaultScopes ?? scopeNames,
|
|
13938
|
+
allowedRedirectOrigins: options.allowedRedirectOrigins ?? [],
|
|
13939
|
+
accessTokenTtlMs: options.accessTokenTtlMs ?? DEFAULT_ACCESS_TOKEN_TTL_MS,
|
|
13940
|
+
refreshTokenTtlMs: options.refreshTokenTtlMs ?? DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
13941
|
+
codeTtlMs: options.codeTtlMs ?? DEFAULT_CODE_TTL_MS
|
|
13942
|
+
};
|
|
13943
|
+
assertKnownDefaultScopes(config3);
|
|
13405
13944
|
}
|
|
13406
|
-
function
|
|
13407
|
-
|
|
13408
|
-
|
|
13945
|
+
function assertKnownDefaultScopes(resolved) {
|
|
13946
|
+
const unknown = resolved.defaultScopes.filter((scope) => !(scope in resolved.scopes));
|
|
13947
|
+
if (unknown.length > 0) {
|
|
13948
|
+
throw new Error(
|
|
13949
|
+
`authorizationServer.defaultScopes names ${unknown.join(", ")}, which authorizationServer.scopes does not describe. An authorize request with no scope would ask for something no consent screen can explain.`
|
|
13950
|
+
);
|
|
13409
13951
|
}
|
|
13410
|
-
const type = typeof body.targetType === "string" ? body.targetType : "target";
|
|
13411
|
-
const value = type === "email" ? normalizeEmail(body.target) : body.target.trim();
|
|
13412
|
-
return `${type}:${value}`;
|
|
13413
13952
|
}
|
|
13414
|
-
function
|
|
13415
|
-
return
|
|
13416
|
-
const body = await readJsonBody(c);
|
|
13417
|
-
const account = accountKey(body);
|
|
13418
|
-
return [
|
|
13419
|
-
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
13420
|
-
account ? `acct:${account}` : void 0
|
|
13421
|
-
];
|
|
13422
|
-
};
|
|
13953
|
+
function getAuthorizationServerConfig() {
|
|
13954
|
+
return config3;
|
|
13423
13955
|
}
|
|
13424
|
-
function
|
|
13425
|
-
return
|
|
13426
|
-
const auth = getOptionalAuth(c);
|
|
13427
|
-
return [
|
|
13428
|
-
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
13429
|
-
auth ? `caller:${auth.userId}` : void 0
|
|
13430
|
-
];
|
|
13431
|
-
};
|
|
13956
|
+
function isLoopbackHostname(hostname) {
|
|
13957
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
13432
13958
|
}
|
|
13433
|
-
function
|
|
13434
|
-
|
|
13435
|
-
|
|
13959
|
+
function assertAuthorizationServerIssuer() {
|
|
13960
|
+
const resolved = getAuthorizationServerConfig();
|
|
13961
|
+
if (!resolved) {
|
|
13962
|
+
return;
|
|
13436
13963
|
}
|
|
13437
|
-
|
|
13964
|
+
assertPublishableIssuer(resolved.issuer, resolved.issuerSource);
|
|
13965
|
+
authLogger.service.info(
|
|
13966
|
+
`OAuth 2.1 authorization server enabled. issuer=${resolved.issuer}, authorization_endpoint=${resolved.authorizeUrl}, scopes=${Object.keys(resolved.scopes).join(" ")}.`
|
|
13967
|
+
);
|
|
13438
13968
|
}
|
|
13439
|
-
function
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
return [
|
|
13443
|
-
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
13444
|
-
idTokenKey(body)
|
|
13445
|
-
];
|
|
13446
|
-
};
|
|
13969
|
+
function canonicalIssuer(raw) {
|
|
13970
|
+
const url = parseUrl(raw);
|
|
13971
|
+
return url && isOriginOnly(raw, url) ? url.origin : raw;
|
|
13447
13972
|
}
|
|
13448
|
-
function
|
|
13449
|
-
return
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
}
|
|
13973
|
+
function isOriginOnly(raw, url) {
|
|
13974
|
+
return raw === url.origin || raw === `${url.origin}/`;
|
|
13975
|
+
}
|
|
13976
|
+
function parseUrl(value) {
|
|
13977
|
+
try {
|
|
13978
|
+
return new URL(value);
|
|
13979
|
+
} catch {
|
|
13980
|
+
return null;
|
|
13981
|
+
}
|
|
13982
|
+
}
|
|
13983
|
+
function assertPublishableIssuer(issuer, source) {
|
|
13984
|
+
const url = parseUrl(issuer);
|
|
13985
|
+
if (!url) {
|
|
13986
|
+
throw new Error(
|
|
13987
|
+
`${source} must be an absolute URL for the OAuth 2.1 authorization server to issue tokens under; it is "${issuer}".`
|
|
13988
|
+
);
|
|
13989
|
+
}
|
|
13990
|
+
assertIssuerIdentifiesOneOrigin(url, issuer, source);
|
|
13991
|
+
assertIssuerTransportIsSafe(url, issuer, source);
|
|
13992
|
+
}
|
|
13993
|
+
function assertIssuerIdentifiesOneOrigin(url, issuer, source) {
|
|
13994
|
+
if (url.username !== "" || url.password !== "") {
|
|
13995
|
+
throw new Error(
|
|
13996
|
+
`${source} must not carry a username or a password for the OAuth 2.1 authorization server. Credentials in an issuer are dropped by every client that compares one, so the identifier published in the metadata would not be the value configured here.`
|
|
13997
|
+
);
|
|
13998
|
+
}
|
|
13999
|
+
if (!isOriginOnly(issuer, url)) {
|
|
14000
|
+
throw new Error(
|
|
14001
|
+
`${source} must be an origin with no path for the OAuth 2.1 authorization server; it is "${issuer}". /.well-known/oauth-authorization-server is served at an origin's root, so an issuer carrying a path publishes its metadata nowhere a client will look.`
|
|
14002
|
+
);
|
|
14003
|
+
}
|
|
14004
|
+
}
|
|
14005
|
+
function assertIssuerTransportIsSafe(url, issuer, source) {
|
|
14006
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) {
|
|
14007
|
+
throw new Error(
|
|
14008
|
+
`${source} must be https, or http on localhost / 127.0.0.1 / [::1] for development; it is "${issuer}". Anything else carries access tokens over the network in the clear.`
|
|
14009
|
+
);
|
|
14010
|
+
}
|
|
14011
|
+
}
|
|
14012
|
+
|
|
14013
|
+
// src/server/lib/oauth2/redirect-uri.ts
|
|
14014
|
+
function refuseRedirectUriRegistration(uri, allowedRedirectOrigins) {
|
|
14015
|
+
let url;
|
|
14016
|
+
try {
|
|
14017
|
+
url = new URL(uri);
|
|
14018
|
+
} catch {
|
|
14019
|
+
return `"${uri}" is not an absolute URI.`;
|
|
14020
|
+
}
|
|
14021
|
+
if (url.hash !== "") {
|
|
14022
|
+
return `"${uri}" carries a fragment. A fragment never survives the redirect, so a client that registered one would be waiting for something it can never be sent.`;
|
|
14023
|
+
}
|
|
14024
|
+
if (hasDotSegment(uri)) {
|
|
14025
|
+
return `"${uri}" has a "." or ".." path segment. Those resolve away before a path is compared, so the URI does not name one destination; register the path it resolves to.`;
|
|
14026
|
+
}
|
|
14027
|
+
return refuseRedirectOrigin(url, uri, allowedRedirectOrigins);
|
|
14028
|
+
}
|
|
14029
|
+
function refuseRedirectOrigin(url, uri, allowedRedirectOrigins) {
|
|
14030
|
+
if (url.protocol === "http:") {
|
|
14031
|
+
return isLoopbackHostname(url.hostname) ? null : `"${uri}" is plain http off loopback. The authorization code would cross the network in the clear; register an https URI, or a loopback one for a client on this machine.`;
|
|
14032
|
+
}
|
|
14033
|
+
if (url.protocol !== "https:") {
|
|
14034
|
+
return `"${uri}" is neither https nor loopback http. Custom-protocol redirects are not registered here.`;
|
|
14035
|
+
}
|
|
14036
|
+
return allowedRedirectOrigins.includes(url.origin) ? null : `"${uri}" is on an origin this application does not allow. Add ${url.origin} to authorizationServer.allowedRedirectOrigins, or register a loopback URI.`;
|
|
14037
|
+
}
|
|
14038
|
+
function hasDotSegment(uri) {
|
|
14039
|
+
const path = uri.replace(/^[^:]*:[/\\]{0,2}[^/\\?#]*/, "").split(/[?#]/)[0] ?? "";
|
|
14040
|
+
return path.split(/[/\\]/).some(isDotSegment);
|
|
14041
|
+
}
|
|
14042
|
+
function isDotSegment(segment) {
|
|
14043
|
+
const decoded = segment.replace(/%2e/gi, ".");
|
|
14044
|
+
return decoded === "." || decoded === "..";
|
|
14045
|
+
}
|
|
14046
|
+
function matchesRegisteredRedirectUri(presented, registered) {
|
|
14047
|
+
let request;
|
|
14048
|
+
try {
|
|
14049
|
+
request = new URL(presented);
|
|
14050
|
+
} catch {
|
|
14051
|
+
return false;
|
|
14052
|
+
}
|
|
14053
|
+
if (request.hash !== "" || hasDotSegment(presented)) {
|
|
14054
|
+
return false;
|
|
14055
|
+
}
|
|
14056
|
+
return registered.some((candidate) => sameRedirectTarget(request, candidate));
|
|
14057
|
+
}
|
|
14058
|
+
function sameRedirectTarget(request, registered) {
|
|
14059
|
+
let known;
|
|
14060
|
+
try {
|
|
14061
|
+
known = new URL(registered);
|
|
14062
|
+
} catch {
|
|
14063
|
+
return false;
|
|
14064
|
+
}
|
|
14065
|
+
if (request.protocol !== known.protocol || request.hostname !== known.hostname || request.pathname !== known.pathname || request.search !== known.search) {
|
|
14066
|
+
return false;
|
|
14067
|
+
}
|
|
14068
|
+
return portMayVary(known) || request.port === known.port;
|
|
14069
|
+
}
|
|
14070
|
+
function portMayVary(registered) {
|
|
14071
|
+
return registered.protocol === "http:" && isLoopbackHostname(registered.hostname);
|
|
14072
|
+
}
|
|
14073
|
+
function redirectHostOf(uri) {
|
|
14074
|
+
return new URL(uri).host;
|
|
14075
|
+
}
|
|
14076
|
+
|
|
14077
|
+
// src/server/lib/oauth2/tokens.ts
|
|
14078
|
+
import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
|
|
14079
|
+
|
|
14080
|
+
// src/server/lib/csrf.ts
|
|
14081
|
+
import { env as env16 } from "@spfn/auth/config";
|
|
14082
|
+
var CSRF_HEADER = "x-spfn-csrf";
|
|
14083
|
+
var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
|
|
14084
|
+
var MAX_CANDIDATES = 32;
|
|
14085
|
+
function sessionSecret() {
|
|
14086
|
+
const secret = env16.SPFN_AUTH_SESSION_SECRET;
|
|
14087
|
+
if (!secret) {
|
|
14088
|
+
throw new Error(
|
|
14089
|
+
"SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
|
|
14090
|
+
);
|
|
14091
|
+
}
|
|
14092
|
+
return secret;
|
|
14093
|
+
}
|
|
14094
|
+
async function hmacSha256(key, message) {
|
|
14095
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
14096
|
+
"raw",
|
|
14097
|
+
key.buffer,
|
|
14098
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
14099
|
+
false,
|
|
14100
|
+
["sign"]
|
|
14101
|
+
);
|
|
14102
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
14103
|
+
return new Uint8Array(signature);
|
|
14104
|
+
}
|
|
14105
|
+
function toHex(bytes) {
|
|
14106
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
14107
|
+
}
|
|
14108
|
+
async function deriveCsrfToken(keyId) {
|
|
14109
|
+
const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
|
|
14110
|
+
return toHex(await hmacSha256(subkey, keyId));
|
|
14111
|
+
}
|
|
14112
|
+
function timingSafeEqualString(a, b) {
|
|
14113
|
+
if (a.length !== b.length) {
|
|
14114
|
+
return false;
|
|
14115
|
+
}
|
|
14116
|
+
let difference = 0;
|
|
14117
|
+
for (let i = 0; i < a.length; i++) {
|
|
14118
|
+
difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
14119
|
+
}
|
|
14120
|
+
return difference === 0;
|
|
14121
|
+
}
|
|
14122
|
+
function matchesCsrfToken(expected, presented) {
|
|
14123
|
+
if (!presented) {
|
|
14124
|
+
return false;
|
|
14125
|
+
}
|
|
14126
|
+
return presented.split(",", MAX_CANDIDATES).some((candidate) => timingSafeEqualString(expected, candidate.trim()));
|
|
14127
|
+
}
|
|
14128
|
+
|
|
14129
|
+
// src/server/lib/oauth2/tokens.ts
|
|
14130
|
+
var OAUTH2_ACCESS_TOKEN_PREFIX = "spfn_at_";
|
|
14131
|
+
var OAUTH2_REFRESH_TOKEN_PREFIX = "spfn_rt_";
|
|
14132
|
+
var SECRET_BYTES = 32;
|
|
14133
|
+
function hashOAuth2Secret(secret) {
|
|
14134
|
+
return createHash6("sha256").update(secret).digest("hex");
|
|
14135
|
+
}
|
|
14136
|
+
function sameOAuth2Hash(a, b) {
|
|
14137
|
+
return timingSafeEqualString(a, b);
|
|
14138
|
+
}
|
|
14139
|
+
function generateAccessToken() {
|
|
14140
|
+
return OAUTH2_ACCESS_TOKEN_PREFIX + randomBytes3(SECRET_BYTES).toString("hex");
|
|
14141
|
+
}
|
|
14142
|
+
function generateRefreshToken() {
|
|
14143
|
+
return OAUTH2_REFRESH_TOKEN_PREFIX + randomBytes3(SECRET_BYTES).toString("hex");
|
|
14144
|
+
}
|
|
14145
|
+
function generateAuthorizationCode() {
|
|
14146
|
+
return randomBytes3(SECRET_BYTES).toString("base64url");
|
|
14147
|
+
}
|
|
14148
|
+
function isAccessTokenShaped(bearer) {
|
|
14149
|
+
return typeof bearer === "string" && bearer.startsWith(OAUTH2_ACCESS_TOKEN_PREFIX);
|
|
14150
|
+
}
|
|
14151
|
+
function pkceChallengeFor(codeVerifier) {
|
|
14152
|
+
return createHash6("sha256").update(codeVerifier).digest("base64url");
|
|
14153
|
+
}
|
|
14154
|
+
var PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
14155
|
+
var PKCE_S256_CHALLENGE = /^[A-Za-z0-9_-]{43}$/;
|
|
14156
|
+
function isPkceVerifierShaped(codeVerifier) {
|
|
14157
|
+
return PKCE_VERIFIER.test(codeVerifier);
|
|
14158
|
+
}
|
|
14159
|
+
function isPkceS256ChallengeShaped(codeChallenge) {
|
|
14160
|
+
return PKCE_S256_CHALLENGE.test(codeChallenge);
|
|
14161
|
+
}
|
|
14162
|
+
function toEpochSeconds(at) {
|
|
14163
|
+
return Math.floor(at.getTime() / 1e3);
|
|
14164
|
+
}
|
|
14165
|
+
function secondsUntil(at, from = /* @__PURE__ */ new Date()) {
|
|
14166
|
+
return Math.max(0, Math.floor((at.getTime() - from.getTime()) / 1e3));
|
|
14167
|
+
}
|
|
14168
|
+
|
|
14169
|
+
// src/server/services/oauth2-client.service.ts
|
|
14170
|
+
var MAX_UNGRANTED_CLIENTS_PER_IP = 20;
|
|
14171
|
+
var UNGRANTED_CLIENT_WINDOW_MS = 60 * 60 * 1e3;
|
|
14172
|
+
var STALE_CLIENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
14173
|
+
var SUPPORTED_AUTH_METHOD = "none";
|
|
14174
|
+
var SUPPORTED_GRANT_TYPES = ["authorization_code", "refresh_token"];
|
|
14175
|
+
var SUPPORTED_RESPONSE_TYPES = ["code"];
|
|
14176
|
+
function refuse(status, error, description) {
|
|
14177
|
+
return { ok: false, status, error, description };
|
|
14178
|
+
}
|
|
14179
|
+
function requireConfig() {
|
|
14180
|
+
const config4 = getAuthorizationServerConfig();
|
|
14181
|
+
if (!config4) {
|
|
14182
|
+
throw new Error("OAuth2 client service called with no authorization server configured.");
|
|
14183
|
+
}
|
|
14184
|
+
return config4;
|
|
14185
|
+
}
|
|
14186
|
+
function asStringArray(value) {
|
|
14187
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
14188
|
+
return null;
|
|
14189
|
+
}
|
|
14190
|
+
return value;
|
|
14191
|
+
}
|
|
14192
|
+
function refuseDeclaredMetadata(request) {
|
|
14193
|
+
const { token_endpoint_auth_method: authMethod } = request;
|
|
14194
|
+
if (authMethod !== void 0 && authMethod !== SUPPORTED_AUTH_METHOD) {
|
|
14195
|
+
return refuse(
|
|
14196
|
+
400,
|
|
14197
|
+
"invalid_client_metadata",
|
|
14198
|
+
`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.`
|
|
14199
|
+
);
|
|
14200
|
+
}
|
|
14201
|
+
return refuseListedMetadata("grant_types", request.grant_types, SUPPORTED_GRANT_TYPES) ?? refuseListedMetadata("response_types", request.response_types, SUPPORTED_RESPONSE_TYPES);
|
|
14202
|
+
}
|
|
14203
|
+
function refuseListedMetadata(field, value, supported) {
|
|
14204
|
+
if (value === void 0) {
|
|
14205
|
+
return null;
|
|
14206
|
+
}
|
|
14207
|
+
const declared = asStringArray(value);
|
|
14208
|
+
if (!declared || declared.length === 0 || declared.some((entry) => !supported.includes(entry))) {
|
|
14209
|
+
return refuse(
|
|
14210
|
+
400,
|
|
14211
|
+
"invalid_client_metadata",
|
|
14212
|
+
`${field} must be a non-empty subset of ${supported.join(", ")}.`
|
|
14213
|
+
);
|
|
14214
|
+
}
|
|
14215
|
+
return null;
|
|
14216
|
+
}
|
|
14217
|
+
function refuseRedirectUris(uris, allowedRedirectOrigins) {
|
|
14218
|
+
const declared = asStringArray(uris);
|
|
14219
|
+
if (!declared || declared.length === 0) {
|
|
14220
|
+
return refuse(
|
|
14221
|
+
400,
|
|
14222
|
+
"invalid_client_metadata",
|
|
14223
|
+
"redirect_uris must list at least one absolute URI. There is nowhere to send an authorization code without one."
|
|
14224
|
+
);
|
|
14225
|
+
}
|
|
14226
|
+
for (const uri of declared) {
|
|
14227
|
+
const detail = refuseRedirectUriRegistration(uri, allowedRedirectOrigins);
|
|
14228
|
+
if (detail) {
|
|
14229
|
+
return refuse(400, "invalid_redirect_uri", detail);
|
|
14230
|
+
}
|
|
14231
|
+
}
|
|
14232
|
+
return null;
|
|
14233
|
+
}
|
|
14234
|
+
async function registerOAuth2ClientService(request, clientIp) {
|
|
14235
|
+
const config4 = requireConfig();
|
|
14236
|
+
const refusal = refuseRedirectUris(request.redirect_uris, config4.allowedRedirectOrigins) ?? refuseDeclaredMetadata(request);
|
|
14237
|
+
if (refusal) {
|
|
14238
|
+
return refusal;
|
|
14239
|
+
}
|
|
14240
|
+
const record = await createUnderStandingCap(newClientRow(request, clientIp), clientIp);
|
|
14241
|
+
if (!record) {
|
|
14242
|
+
return refuse(429, "invalid_client_metadata", OVER_STANDING_CAP_MESSAGE);
|
|
14243
|
+
}
|
|
14244
|
+
authLogger.service.info("OAuth2 client registered", { clientName: record.clientName });
|
|
14245
|
+
return { ok: true, client: describeClient(record) };
|
|
14246
|
+
}
|
|
14247
|
+
var OVER_STANDING_CAP_MESSAGE = `This address has registered ${MAX_UNGRANTED_CLIENTS_PER_IP} clients in the last hour that nobody has approved. Complete or abandon one of those, or try again later.`;
|
|
14248
|
+
function newClientRow(request, clientIp) {
|
|
14249
|
+
const redirectUris = asStringArray(request.redirect_uris);
|
|
14250
|
+
return {
|
|
14251
|
+
clientId: `spfn_client_${randomBytes4(16).toString("hex")}`,
|
|
14252
|
+
clientName: clientNameOf(request.client_name, redirectUris[0]),
|
|
14253
|
+
redirectUris,
|
|
14254
|
+
createdIp: clientIp
|
|
14255
|
+
};
|
|
14256
|
+
}
|
|
14257
|
+
async function createUnderStandingCap(data, clientIp) {
|
|
14258
|
+
if (!clientIp) {
|
|
14259
|
+
return await oauth2ClientsRepository.create(data);
|
|
14260
|
+
}
|
|
14261
|
+
return await oauth2ClientsRepository.createWithinStandingCap(data, {
|
|
14262
|
+
ip: clientIp,
|
|
14263
|
+
max: MAX_UNGRANTED_CLIENTS_PER_IP,
|
|
14264
|
+
windowMs: UNGRANTED_CLIENT_WINDOW_MS
|
|
14265
|
+
});
|
|
14266
|
+
}
|
|
14267
|
+
function clientNameOf(declared, firstRedirectUri) {
|
|
14268
|
+
if (typeof declared === "string" && declared.trim()) {
|
|
14269
|
+
return declared.trim();
|
|
14270
|
+
}
|
|
14271
|
+
return new URL(firstRedirectUri).host;
|
|
14272
|
+
}
|
|
14273
|
+
function describeClient(record) {
|
|
14274
|
+
return {
|
|
14275
|
+
client_id: record.clientId,
|
|
14276
|
+
client_id_issued_at: toEpochSeconds(record.createdAt),
|
|
14277
|
+
client_name: record.clientName,
|
|
14278
|
+
redirect_uris: record.redirectUris,
|
|
14279
|
+
token_endpoint_auth_method: SUPPORTED_AUTH_METHOD,
|
|
14280
|
+
grant_types: SUPPORTED_GRANT_TYPES,
|
|
14281
|
+
response_types: SUPPORTED_RESPONSE_TYPES
|
|
14282
|
+
};
|
|
14283
|
+
}
|
|
14284
|
+
async function purgeStaleOAuth2ClientsService() {
|
|
14285
|
+
const deleted = await oauth2ClientsRepository.deleteStaleUngranted(
|
|
14286
|
+
new Date(Date.now() - STALE_CLIENT_MAX_AGE_MS)
|
|
14287
|
+
);
|
|
14288
|
+
return { deleted };
|
|
14289
|
+
}
|
|
14290
|
+
|
|
14291
|
+
// src/server/services/oauth2-authorize.service.ts
|
|
14292
|
+
init_oauth2_clients_repository();
|
|
14293
|
+
init_oauth2_grants_repository();
|
|
14294
|
+
init_oauth2_authorization_codes_repository();
|
|
14295
|
+
import {
|
|
14296
|
+
OAuth2AuthorizeRedirectError,
|
|
14297
|
+
OAuth2RedirectUriMismatchError,
|
|
14298
|
+
OAuth2UnknownClientError
|
|
14299
|
+
} from "@spfn/auth/errors";
|
|
14300
|
+
|
|
14301
|
+
// src/server/lib/oauth2/resource.ts
|
|
14302
|
+
function normalizeResource(value) {
|
|
14303
|
+
let url;
|
|
14304
|
+
try {
|
|
14305
|
+
url = new URL(value);
|
|
14306
|
+
} catch {
|
|
14307
|
+
return null;
|
|
14308
|
+
}
|
|
14309
|
+
if (url.hash !== "") {
|
|
14310
|
+
return null;
|
|
14311
|
+
}
|
|
14312
|
+
return url.toString();
|
|
14313
|
+
}
|
|
14314
|
+
function sameResource(presented, granted) {
|
|
14315
|
+
const left = normalizeResource(presented);
|
|
14316
|
+
return left !== null && left === normalizeResource(granted);
|
|
14317
|
+
}
|
|
14318
|
+
|
|
14319
|
+
// src/server/services/oauth2-authorize.service.ts
|
|
14320
|
+
function requireConfig2() {
|
|
14321
|
+
const config4 = getAuthorizationServerConfig();
|
|
14322
|
+
if (!config4) {
|
|
14323
|
+
throw new Error("OAuth2 authorize service called with no authorization server configured.");
|
|
14324
|
+
}
|
|
14325
|
+
return config4;
|
|
14326
|
+
}
|
|
14327
|
+
async function resolveRedirectTarget(params) {
|
|
14328
|
+
const client = await oauth2ClientsRepository.findByClientId(params.clientId);
|
|
14329
|
+
if (!client) {
|
|
14330
|
+
throw new OAuth2UnknownClientError();
|
|
14331
|
+
}
|
|
14332
|
+
if (!matchesRegisteredRedirectUri(params.redirectUri, client.redirectUris)) {
|
|
14333
|
+
throw new OAuth2RedirectUriMismatchError();
|
|
14334
|
+
}
|
|
14335
|
+
return { client, redirectUri: params.redirectUri };
|
|
14336
|
+
}
|
|
14337
|
+
function resolveScopes(params, config4) {
|
|
14338
|
+
const requested = params.scope?.trim();
|
|
14339
|
+
if (!requested) {
|
|
14340
|
+
return config4.defaultScopes;
|
|
14341
|
+
}
|
|
14342
|
+
return requested.split(/\s+/);
|
|
14343
|
+
}
|
|
14344
|
+
var PKCE_REQUIRED_MESSAGE = "code_challenge with code_challenge_method=S256 is required, and the challenge must be the 43 base64url characters that transform produces. Neither plain PKCE nor a request without PKCE is accepted, because a code intercepted on the loopback listener would otherwise be exchangeable by whoever intercepted it.";
|
|
14345
|
+
var RESOURCE_REQUIRED_MESSAGE = "resource is required and must be an absolute URI with no fragment (RFC 8707). A token issued without a stated target is a token good against everything.";
|
|
14346
|
+
function refuseRedirectable(params, redirectUri, error, message) {
|
|
14347
|
+
throw new OAuth2AuthorizeRedirectError({ error, redirectUri, state: params.state, message });
|
|
14348
|
+
}
|
|
14349
|
+
function hasUsableS256Challenge(params) {
|
|
14350
|
+
return params.codeChallengeMethod === "S256" && !!params.codeChallenge && isPkceS256ChallengeShaped(params.codeChallenge);
|
|
14351
|
+
}
|
|
14352
|
+
function assertRedirectableRules(params, redirectUri, config4) {
|
|
14353
|
+
if (!hasUsableS256Challenge(params)) {
|
|
14354
|
+
refuseRedirectable(params, redirectUri, "invalid_request", PKCE_REQUIRED_MESSAGE);
|
|
14355
|
+
}
|
|
14356
|
+
const resource = params.resource ? normalizeResource(params.resource) : null;
|
|
14357
|
+
if (!resource) {
|
|
14358
|
+
refuseRedirectable(params, redirectUri, "invalid_target", RESOURCE_REQUIRED_MESSAGE);
|
|
14359
|
+
}
|
|
14360
|
+
const scopes = resolveScopes(params, config4);
|
|
14361
|
+
const unknown = scopes.filter((scope) => !(scope in config4.scopes));
|
|
14362
|
+
if (unknown.length > 0) {
|
|
14363
|
+
refuseRedirectable(params, redirectUri, "invalid_scope", `Unknown scope: ${unknown.join(", ")}.`);
|
|
14364
|
+
}
|
|
14365
|
+
return { resource, scopes, codeChallenge: params.codeChallenge };
|
|
14366
|
+
}
|
|
14367
|
+
async function validate(params) {
|
|
14368
|
+
const config4 = requireConfig2();
|
|
14369
|
+
const { client, redirectUri } = await resolveRedirectTarget(params);
|
|
14370
|
+
const { resource, scopes, codeChallenge } = assertRedirectableRules(params, redirectUri, config4);
|
|
14371
|
+
return { client, redirectUri, resource, scopes, codeChallenge, state: params.state };
|
|
14372
|
+
}
|
|
14373
|
+
async function describeOAuth2AuthorizeRequestService(params) {
|
|
14374
|
+
const config4 = requireConfig2();
|
|
14375
|
+
const validated = await validate(params);
|
|
14376
|
+
return {
|
|
14377
|
+
clientName: validated.client.clientName,
|
|
14378
|
+
redirectHost: redirectHostOf(validated.redirectUri),
|
|
14379
|
+
scopes: validated.scopes.map((name) => ({ name, description: config4.scopes[name] })),
|
|
14380
|
+
resource: validated.resource
|
|
14381
|
+
};
|
|
14382
|
+
}
|
|
14383
|
+
async function approveOAuth2AuthorizeService(params, userId) {
|
|
14384
|
+
const config4 = requireConfig2();
|
|
14385
|
+
const validated = await validate(params);
|
|
14386
|
+
const grant = await oauth2GrantsRepository.upsert({
|
|
14387
|
+
client: validated.client.id,
|
|
14388
|
+
user: userId,
|
|
14389
|
+
resource: validated.resource,
|
|
14390
|
+
scopes: validated.scopes
|
|
14391
|
+
});
|
|
14392
|
+
const code = generateAuthorizationCode();
|
|
14393
|
+
await oauth2AuthorizationCodesRepository.create({
|
|
14394
|
+
codeHash: hashOAuth2Secret(code),
|
|
14395
|
+
grant: grant.id,
|
|
14396
|
+
redirectUri: validated.redirectUri,
|
|
14397
|
+
codeChallenge: validated.codeChallenge,
|
|
14398
|
+
expiresAt: new Date(Date.now() + config4.codeTtlMs)
|
|
14399
|
+
});
|
|
14400
|
+
return { code, redirectUri: validated.redirectUri, state: validated.state };
|
|
14401
|
+
}
|
|
14402
|
+
async function denyOAuth2AuthorizeService(params) {
|
|
14403
|
+
const validated = await validate(params);
|
|
14404
|
+
throw new OAuth2AuthorizeRedirectError({
|
|
14405
|
+
error: "access_denied",
|
|
14406
|
+
redirectUri: validated.redirectUri,
|
|
14407
|
+
state: validated.state,
|
|
14408
|
+
message: "The account owner refused this authorization request."
|
|
14409
|
+
});
|
|
14410
|
+
}
|
|
14411
|
+
|
|
14412
|
+
// src/server/services/oauth2-token.service.ts
|
|
14413
|
+
init_oauth2_clients_repository();
|
|
14414
|
+
init_oauth2_grants_repository();
|
|
14415
|
+
init_oauth2_authorization_codes_repository();
|
|
14416
|
+
init_oauth2_tokens_repository();
|
|
14417
|
+
import { runInTransaction as runInTransaction5 } from "@spfn/core/db";
|
|
14418
|
+
function invalidGrant() {
|
|
14419
|
+
return {
|
|
14420
|
+
ok: false,
|
|
14421
|
+
error: "invalid_grant",
|
|
14422
|
+
description: "The authorization code or refresh token is invalid, expired, already used, or was issued to another client."
|
|
14423
|
+
};
|
|
14424
|
+
}
|
|
14425
|
+
function refuse2(error, description) {
|
|
14426
|
+
return { ok: false, error, description };
|
|
14427
|
+
}
|
|
14428
|
+
function requireConfig3() {
|
|
14429
|
+
const config4 = getAuthorizationServerConfig();
|
|
14430
|
+
if (!config4) {
|
|
14431
|
+
throw new Error("OAuth2 token service called with no authorization server configured.");
|
|
14432
|
+
}
|
|
14433
|
+
return config4;
|
|
14434
|
+
}
|
|
14435
|
+
async function oauth2TokenService(request) {
|
|
14436
|
+
if (request.grant_type === "authorization_code") {
|
|
14437
|
+
return await exchangeAuthorizationCode(request);
|
|
14438
|
+
}
|
|
14439
|
+
if (request.grant_type === "refresh_token") {
|
|
14440
|
+
return await refreshTokens(request);
|
|
14441
|
+
}
|
|
14442
|
+
return refuse2(
|
|
14443
|
+
"unsupported_grant_type",
|
|
14444
|
+
"grant_type must be authorization_code or refresh_token."
|
|
14445
|
+
);
|
|
14446
|
+
}
|
|
14447
|
+
async function exchangeAuthorizationCode(request) {
|
|
14448
|
+
if (!request.code || !request.code_verifier || !request.client_id || !request.redirect_uri) {
|
|
14449
|
+
return refuse2(
|
|
14450
|
+
"invalid_request",
|
|
14451
|
+
"authorization_code requires code, code_verifier, client_id and redirect_uri."
|
|
14452
|
+
);
|
|
14453
|
+
}
|
|
14454
|
+
const codeHash = hashOAuth2Secret(request.code);
|
|
14455
|
+
const record = await oauth2AuthorizationCodesRepository.findByCodeHash(codeHash);
|
|
14456
|
+
if (!record || !sameOAuth2Hash(record.codeHash, codeHash)) {
|
|
14457
|
+
return invalidGrant();
|
|
14458
|
+
}
|
|
14459
|
+
const pair = await oauth2GrantsRepository.findWithClientById(record.grant);
|
|
14460
|
+
if (!pair || pair.grant.revokedAt !== null || !boundToRequest(request, record, pair.client)) {
|
|
14461
|
+
return invalidGrant();
|
|
14462
|
+
}
|
|
14463
|
+
return await spendBoundCode(request, record, pair);
|
|
14464
|
+
}
|
|
14465
|
+
function boundToRequest(request, record, client) {
|
|
14466
|
+
if (request.client_id !== client.clientId || request.redirect_uri !== record.redirectUri) {
|
|
14467
|
+
return false;
|
|
14468
|
+
}
|
|
14469
|
+
if (!isPkceVerifierShaped(request.code_verifier)) {
|
|
14470
|
+
return false;
|
|
14471
|
+
}
|
|
14472
|
+
return sameOAuth2Hash(pkceChallengeFor(request.code_verifier), record.codeChallenge);
|
|
14473
|
+
}
|
|
14474
|
+
async function spendBoundCode(request, record, pair) {
|
|
14475
|
+
if (record.usedAt !== null) {
|
|
14476
|
+
await revokeGrantAndTokens(record.grant, "authorization code presented twice");
|
|
14477
|
+
return invalidGrant();
|
|
14478
|
+
}
|
|
14479
|
+
if (!await oauth2AuthorizationCodesRepository.consume(record.codeHash)) {
|
|
14480
|
+
return invalidGrant();
|
|
14481
|
+
}
|
|
14482
|
+
if (resolveResource(request.resource, pair.grant) === null) {
|
|
14483
|
+
return refuse2("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
14484
|
+
}
|
|
14485
|
+
return { ok: true, tokens: await issueTokenPair(pair.grant, pair.grant.scopes) };
|
|
14486
|
+
}
|
|
14487
|
+
async function refreshTokens(request) {
|
|
14488
|
+
if (!request.refresh_token || !request.client_id) {
|
|
14489
|
+
return refuse2("invalid_request", "refresh_token requires refresh_token and client_id.");
|
|
14490
|
+
}
|
|
14491
|
+
const tokenHash = hashOAuth2Secret(request.refresh_token);
|
|
14492
|
+
const presented = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
14493
|
+
if (!presented || !sameOAuth2Hash(presented.tokenHash, tokenHash) || presented.kind !== "refresh") {
|
|
14494
|
+
return invalidGrant();
|
|
14495
|
+
}
|
|
14496
|
+
if (presented.replacedAt !== null) {
|
|
14497
|
+
await revokeGrantAndTokens(presented.grant, "rotated refresh token presented again");
|
|
14498
|
+
return invalidGrant();
|
|
14499
|
+
}
|
|
14500
|
+
if (presented.revokedAt !== null || presented.expiresAt.getTime() <= Date.now()) {
|
|
14501
|
+
return invalidGrant();
|
|
14502
|
+
}
|
|
14503
|
+
return await rotateRefresh(request, tokenHash, presented.grant, presented.scopes);
|
|
14504
|
+
}
|
|
14505
|
+
async function rotateRefresh(request, tokenHash, grantId, presentedScopes) {
|
|
14506
|
+
const pair = await oauth2GrantsRepository.findWithClientById(grantId);
|
|
14507
|
+
if (!pair || pair.grant.revokedAt !== null || request.client_id !== pair.client.clientId) {
|
|
14508
|
+
return invalidGrant();
|
|
14509
|
+
}
|
|
14510
|
+
if (resolveResource(request.resource, pair.grant) === null) {
|
|
14511
|
+
return refuse2("invalid_target", "resource does not match the resource this grant was issued for.");
|
|
14512
|
+
}
|
|
14513
|
+
const scopes = resolveRefreshScopes(request.scope, pair.grant, presentedScopes);
|
|
14514
|
+
if (!scopes) {
|
|
14515
|
+
return refuse2("invalid_scope", "A refresh may ask for a subset of the granted scopes, never more.");
|
|
14516
|
+
}
|
|
14517
|
+
if (!await oauth2TokensRepository.rotate(tokenHash)) {
|
|
14518
|
+
return await refuseLostRotation(tokenHash, grantId);
|
|
14519
|
+
}
|
|
14520
|
+
return { ok: true, tokens: await issueTokenPair(pair.grant, scopes) };
|
|
14521
|
+
}
|
|
14522
|
+
async function refuseLostRotation(tokenHash, grantId) {
|
|
14523
|
+
const current = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
14524
|
+
if (current && current.replacedAt !== null) {
|
|
14525
|
+
await revokeGrantAndTokens(grantId, "refresh token rotated twice concurrently");
|
|
14526
|
+
}
|
|
14527
|
+
return invalidGrant();
|
|
14528
|
+
}
|
|
14529
|
+
function resolveRefreshScopes(requested, grant, presentedScopes) {
|
|
14530
|
+
const asked = requested?.trim();
|
|
14531
|
+
if (!asked) {
|
|
14532
|
+
return presentedScopes;
|
|
14533
|
+
}
|
|
14534
|
+
const scopes = asked.split(/\s+/);
|
|
14535
|
+
return scopes.every((scope) => grant.scopes.includes(scope)) ? scopes : null;
|
|
14536
|
+
}
|
|
14537
|
+
function resolveResource(requested, grant) {
|
|
14538
|
+
if (!requested) {
|
|
14539
|
+
return grant.resource;
|
|
14540
|
+
}
|
|
14541
|
+
return sameResource(requested, grant.resource) ? grant.resource : null;
|
|
14542
|
+
}
|
|
14543
|
+
async function issueTokenPair(grant, scopes) {
|
|
14544
|
+
const config4 = requireConfig3();
|
|
14545
|
+
const accessToken = generateAccessToken();
|
|
14546
|
+
const refreshToken = generateRefreshToken();
|
|
14547
|
+
const expiresAt = new Date(Date.now() + config4.accessTokenTtlMs);
|
|
14548
|
+
await runInTransaction5(async () => {
|
|
14549
|
+
await storeToken(accessToken, "access", grant.id, scopes, expiresAt);
|
|
14550
|
+
await storeToken(
|
|
14551
|
+
refreshToken,
|
|
14552
|
+
"refresh",
|
|
14553
|
+
grant.id,
|
|
14554
|
+
scopes,
|
|
14555
|
+
new Date(Date.now() + config4.refreshTokenTtlMs)
|
|
14556
|
+
);
|
|
14557
|
+
});
|
|
14558
|
+
oauth2ClientsRepository.updateLastUsedById(grant.client).catch((err) => authLogger.service.error("Failed to update OAuth2 client lastUsedAt", err));
|
|
14559
|
+
return {
|
|
14560
|
+
access_token: accessToken,
|
|
14561
|
+
token_type: "Bearer",
|
|
14562
|
+
expires_in: secondsUntil(expiresAt),
|
|
14563
|
+
refresh_token: refreshToken,
|
|
14564
|
+
scope: scopes.join(" ")
|
|
14565
|
+
};
|
|
14566
|
+
}
|
|
14567
|
+
async function storeToken(token, kind, grantId, scopes, expiresAt) {
|
|
14568
|
+
await oauth2TokensRepository.create({
|
|
14569
|
+
tokenHash: hashOAuth2Secret(token),
|
|
14570
|
+
kind,
|
|
14571
|
+
grant: grantId,
|
|
14572
|
+
scopes,
|
|
14573
|
+
expiresAt
|
|
14574
|
+
});
|
|
14575
|
+
}
|
|
14576
|
+
async function revokeGrantAndTokens(grantId, reason) {
|
|
14577
|
+
await oauth2GrantsRepository.revokeById(grantId);
|
|
14578
|
+
await oauth2GrantsRepository.revokeTokensOfGrants([grantId]);
|
|
14579
|
+
authLogger.service.warn("OAuth2 grant revoked after a replay", { grantId, reason });
|
|
14580
|
+
}
|
|
14581
|
+
async function revokeOAuth2TokenService(token, clientId) {
|
|
14582
|
+
if (!token) {
|
|
14583
|
+
return;
|
|
14584
|
+
}
|
|
14585
|
+
const tokenHash = hashOAuth2Secret(token);
|
|
14586
|
+
const record = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
14587
|
+
if (!record || !sameOAuth2Hash(record.tokenHash, tokenHash)) {
|
|
14588
|
+
return;
|
|
14589
|
+
}
|
|
14590
|
+
if (!await issuedToClient(record.grant, clientId)) {
|
|
14591
|
+
return;
|
|
14592
|
+
}
|
|
14593
|
+
if (record.kind === "access") {
|
|
14594
|
+
await oauth2TokensRepository.revokeByTokenHash(tokenHash);
|
|
14595
|
+
return;
|
|
14596
|
+
}
|
|
14597
|
+
await oauth2GrantsRepository.revokeById(record.grant);
|
|
14598
|
+
await oauth2GrantsRepository.revokeTokensOfGrants([record.grant]);
|
|
14599
|
+
}
|
|
14600
|
+
async function issuedToClient(grantId, clientId) {
|
|
14601
|
+
const pair = await oauth2GrantsRepository.findWithClientById(grantId);
|
|
14602
|
+
return pair?.client.clientId === clientId;
|
|
14603
|
+
}
|
|
14604
|
+
|
|
14605
|
+
// src/server/services/oauth2-access-token.service.ts
|
|
14606
|
+
init_oauth2_grants_repository();
|
|
14607
|
+
init_oauth2_tokens_repository();
|
|
14608
|
+
async function verifyAccessToken(token, resource) {
|
|
14609
|
+
if (!isAccessTokenShaped(token)) {
|
|
14610
|
+
return null;
|
|
14611
|
+
}
|
|
14612
|
+
const tokenHash = hashOAuth2Secret(token);
|
|
14613
|
+
const record = await oauth2TokensRepository.findByTokenHash(tokenHash);
|
|
14614
|
+
if (!record || !sameOAuth2Hash(record.tokenHash, tokenHash) || !isUsable(record)) {
|
|
14615
|
+
return null;
|
|
14616
|
+
}
|
|
14617
|
+
const pair = await oauth2GrantsRepository.findWithClientById(record.grant);
|
|
14618
|
+
if (!pair || pair.grant.revokedAt !== null || !sameResource(resource, pair.grant.resource)) {
|
|
14619
|
+
return null;
|
|
14620
|
+
}
|
|
14621
|
+
oauth2TokensRepository.updateLastUsedById(record.id).catch((err) => authLogger.service.error("Failed to update OAuth2 token lastUsedAt", err));
|
|
14622
|
+
return {
|
|
14623
|
+
clientId: pair.client.clientId,
|
|
14624
|
+
scopes: record.scopes,
|
|
14625
|
+
expiresAt: toEpochSeconds(record.expiresAt),
|
|
14626
|
+
userId: String(pair.grant.user)
|
|
14627
|
+
};
|
|
14628
|
+
}
|
|
14629
|
+
function isUsable(record) {
|
|
14630
|
+
return record.kind === "access" && record.revokedAt === null && record.expiresAt.getTime() > Date.now();
|
|
14631
|
+
}
|
|
14632
|
+
|
|
14633
|
+
// src/server/routes/auth/index.ts
|
|
14634
|
+
init_esm();
|
|
14635
|
+
import { Transactional } from "@spfn/core/db";
|
|
14636
|
+
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
14637
|
+
|
|
14638
|
+
// src/server/lib/rate-limit-keys.ts
|
|
14639
|
+
init_email();
|
|
14640
|
+
import { createHash as createHash7 } from "crypto";
|
|
14641
|
+
import { getClientIp } from "@spfn/core/middleware";
|
|
14642
|
+
async function readJsonBody(c) {
|
|
14643
|
+
try {
|
|
14644
|
+
return await c.req.json();
|
|
14645
|
+
} catch {
|
|
14646
|
+
return {};
|
|
14647
|
+
}
|
|
14648
|
+
}
|
|
14649
|
+
function accountKey(body) {
|
|
14650
|
+
if (typeof body.email === "string" && body.email.trim()) {
|
|
14651
|
+
return `email:${normalizeEmail(body.email)}`;
|
|
14652
|
+
}
|
|
14653
|
+
if (typeof body.phone === "string" && body.phone.trim()) {
|
|
14654
|
+
return `phone:${body.phone.trim()}`;
|
|
14655
|
+
}
|
|
14656
|
+
return void 0;
|
|
14657
|
+
}
|
|
14658
|
+
function targetKey(body) {
|
|
14659
|
+
if (typeof body.target !== "string" || !body.target.trim()) {
|
|
14660
|
+
return void 0;
|
|
14661
|
+
}
|
|
14662
|
+
const type = typeof body.targetType === "string" ? body.targetType : "target";
|
|
14663
|
+
const value = type === "email" ? normalizeEmail(body.target) : body.target.trim();
|
|
14664
|
+
return `${type}:${value}`;
|
|
14665
|
+
}
|
|
14666
|
+
function byIpAndAccount(options = {}) {
|
|
14667
|
+
return async (c) => {
|
|
14668
|
+
const body = await readJsonBody(c);
|
|
14669
|
+
const account = accountKey(body);
|
|
14670
|
+
return [
|
|
14671
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
14672
|
+
account ? `acct:${account}` : void 0
|
|
14673
|
+
];
|
|
14674
|
+
};
|
|
14675
|
+
}
|
|
14676
|
+
function byIpAndCaller(options = {}) {
|
|
14677
|
+
return async (c) => {
|
|
14678
|
+
const auth = getOptionalAuth(c);
|
|
14679
|
+
return [
|
|
14680
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
14681
|
+
auth ? `caller:${auth.userId}` : void 0
|
|
14682
|
+
];
|
|
14683
|
+
};
|
|
14684
|
+
}
|
|
14685
|
+
function idTokenKey(body) {
|
|
14686
|
+
if (typeof body.idToken !== "string" || !body.idToken) {
|
|
14687
|
+
return void 0;
|
|
14688
|
+
}
|
|
14689
|
+
return `tok:${createHash7("sha256").update(body.idToken).digest("hex")}`;
|
|
14690
|
+
}
|
|
14691
|
+
function byIpAndIdToken(options = {}) {
|
|
14692
|
+
return async (c) => {
|
|
14693
|
+
const body = await readJsonBody(c);
|
|
14694
|
+
return [
|
|
14695
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
14696
|
+
idTokenKey(body)
|
|
14697
|
+
];
|
|
14698
|
+
};
|
|
14699
|
+
}
|
|
14700
|
+
function byIpAndTarget(options = {}) {
|
|
14701
|
+
return async (c) => {
|
|
14702
|
+
const body = await readJsonBody(c);
|
|
14703
|
+
const target = targetKey(body);
|
|
14704
|
+
return [
|
|
14705
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
14706
|
+
target ? `tgt:${target}` : void 0
|
|
14707
|
+
];
|
|
14708
|
+
};
|
|
13457
14709
|
}
|
|
13458
14710
|
|
|
13459
14711
|
// src/server/routes/auth/index.ts
|
|
@@ -13967,7 +15219,7 @@ import {
|
|
|
13967
15219
|
} from "@spfn/auth/errors";
|
|
13968
15220
|
|
|
13969
15221
|
// src/server/client-proof/refusal.ts
|
|
13970
|
-
import { randomBytes as
|
|
15222
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
13971
15223
|
|
|
13972
15224
|
// src/server/client-proof/canonical-json.ts
|
|
13973
15225
|
var CanonicalJsonError = class extends Error {
|
|
@@ -13980,13 +15232,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
13980
15232
|
var INT64_MIN = -(2n ** 63n);
|
|
13981
15233
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
13982
15234
|
function parseCanonicalJson(bytes) {
|
|
13983
|
-
let
|
|
15235
|
+
let text22;
|
|
13984
15236
|
try {
|
|
13985
|
-
|
|
15237
|
+
text22 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
13986
15238
|
} catch {
|
|
13987
15239
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
13988
15240
|
}
|
|
13989
|
-
const parser = new Parser(
|
|
15241
|
+
const parser = new Parser(text22);
|
|
13990
15242
|
const value = parser.parseValue();
|
|
13991
15243
|
parser.skipWhitespace();
|
|
13992
15244
|
if (!parser.atEnd()) {
|
|
@@ -14007,8 +15259,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
14007
15259
|
return true;
|
|
14008
15260
|
}
|
|
14009
15261
|
var Parser = class {
|
|
14010
|
-
constructor(
|
|
14011
|
-
this.text =
|
|
15262
|
+
constructor(text22) {
|
|
15263
|
+
this.text = text22;
|
|
14012
15264
|
}
|
|
14013
15265
|
pos = 0;
|
|
14014
15266
|
atEnd() {
|
|
@@ -14325,7 +15577,7 @@ var HTTP_STATUS = {
|
|
|
14325
15577
|
CONTRACT_UNSUPPORTED: 409
|
|
14326
15578
|
};
|
|
14327
15579
|
function newHexId() {
|
|
14328
|
-
return
|
|
15580
|
+
return randomBytes5(16).toString("hex");
|
|
14329
15581
|
}
|
|
14330
15582
|
var ClientProofRefusal = class _ClientProofRefusal {
|
|
14331
15583
|
constructor(code, message) {
|
|
@@ -14427,7 +15679,7 @@ function contractViolation(message) {
|
|
|
14427
15679
|
}
|
|
14428
15680
|
|
|
14429
15681
|
// src/server/client-proof/contract-bundle.ts
|
|
14430
|
-
import { createHash as
|
|
15682
|
+
import { createHash as createHash9 } from "crypto";
|
|
14431
15683
|
import {
|
|
14432
15684
|
CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
|
|
14433
15685
|
ServerTimeResponseSchema
|
|
@@ -14435,7 +15687,7 @@ import {
|
|
|
14435
15687
|
init_types();
|
|
14436
15688
|
|
|
14437
15689
|
// src/server/client-proof/proof.ts
|
|
14438
|
-
import { createHash as
|
|
15690
|
+
import { createHash as createHash8, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
|
|
14439
15691
|
var CLIENT_PROOF_PROFILE = "clientProofV1";
|
|
14440
15692
|
var ABSENT_BODY_SHA256 = "0".repeat(64);
|
|
14441
15693
|
var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
|
|
@@ -14505,7 +15757,7 @@ function verifyClientProof(input, presentedProof, publicKey) {
|
|
|
14505
15757
|
);
|
|
14506
15758
|
}
|
|
14507
15759
|
function sha256Hex(bytes) {
|
|
14508
|
-
return
|
|
15760
|
+
return createHash8("sha256").update(bytes).digest("hex");
|
|
14509
15761
|
}
|
|
14510
15762
|
|
|
14511
15763
|
// src/server/client-proof/admission.ts
|
|
@@ -15735,16 +16987,16 @@ function extractBearer2(header) {
|
|
|
15735
16987
|
}
|
|
15736
16988
|
|
|
15737
16989
|
// src/server/middleware/ops-or-user.ts
|
|
15738
|
-
function opsOrUser(
|
|
15739
|
-
const roles2 =
|
|
15740
|
-
const permissions2 =
|
|
15741
|
-
if (!
|
|
16990
|
+
function opsOrUser(config4) {
|
|
16991
|
+
const roles2 = config4.roles ?? [];
|
|
16992
|
+
const permissions2 = config4.permissions ?? [];
|
|
16993
|
+
if (!config4.opsScopes || config4.opsScopes.length === 0) {
|
|
15742
16994
|
throw new Error("opsOrUser: opsScopes must name at least one scope \u2014 an ops token would otherwise be admitted unchecked.");
|
|
15743
16995
|
}
|
|
15744
16996
|
if (roles2.length === 0 && permissions2.length === 0) {
|
|
15745
16997
|
throw new Error("opsOrUser: give roles, permissions, or both \u2014 a user session would otherwise be admitted unchecked.");
|
|
15746
16998
|
}
|
|
15747
|
-
const ops = chain([opsTokenAuth.handler, requireOpsScope(...
|
|
16999
|
+
const ops = chain([opsTokenAuth.handler, requireOpsScope(...config4.opsScopes)]);
|
|
15748
17000
|
const user = chain([
|
|
15749
17001
|
authenticate.handler,
|
|
15750
17002
|
...roles2.length > 0 ? [requireRole(...roles2)] : [],
|
|
@@ -15759,8 +17011,8 @@ function bearerOf(header) {
|
|
|
15759
17011
|
function chain(handlers) {
|
|
15760
17012
|
return async (c, next) => {
|
|
15761
17013
|
let answer;
|
|
15762
|
-
const runFrom = async (
|
|
15763
|
-
const produced =
|
|
17014
|
+
const runFrom = async (index18) => {
|
|
17015
|
+
const produced = index18 < handlers.length ? await handlers[index18](c, () => runFrom(index18 + 1)) : await next();
|
|
15764
17016
|
if (produced instanceof Response) {
|
|
15765
17017
|
answer = produced;
|
|
15766
17018
|
}
|
|
@@ -16751,6 +18003,186 @@ var revokeOpsToken = route9.delete("/_auth/ops-tokens/:id").input({
|
|
|
16751
18003
|
return { opsToken: toSummary2(record) };
|
|
16752
18004
|
});
|
|
16753
18005
|
|
|
18006
|
+
// src/server/routes/oauth2/index.ts
|
|
18007
|
+
init_esm();
|
|
18008
|
+
import { route as route10 } from "@spfn/core/route";
|
|
18009
|
+
import { getClientIp as getClientIp2, rateLimitPolicy as rateLimitPolicy8 } from "@spfn/core/middleware";
|
|
18010
|
+
|
|
18011
|
+
// src/server/routes/oauth2/http.ts
|
|
18012
|
+
import { NotFoundError as NotFoundError5 } from "@spfn/core/errors";
|
|
18013
|
+
var NO_STORE_HEADERS = {
|
|
18014
|
+
"Content-Type": "application/json",
|
|
18015
|
+
"Cache-Control": "no-store",
|
|
18016
|
+
Pragma: "no-cache"
|
|
18017
|
+
};
|
|
18018
|
+
function requireAuthorizationServer() {
|
|
18019
|
+
const config4 = getAuthorizationServerConfig();
|
|
18020
|
+
if (!config4) {
|
|
18021
|
+
throw new NotFoundError5({
|
|
18022
|
+
message: "This application does not run an OAuth 2.1 authorization server. Pass `authorizationServer` to createAuthLifecycle() to enable one."
|
|
18023
|
+
});
|
|
18024
|
+
}
|
|
18025
|
+
return config4;
|
|
18026
|
+
}
|
|
18027
|
+
function oauth2ErrorResponse(c, status, error, description) {
|
|
18028
|
+
return c.json({ error, error_description: description }, status, NO_STORE_HEADERS);
|
|
18029
|
+
}
|
|
18030
|
+
function oauth2JsonResponse(c, status, body) {
|
|
18031
|
+
return c.json(body, status, NO_STORE_HEADERS);
|
|
18032
|
+
}
|
|
18033
|
+
async function readOAuth2Body(c) {
|
|
18034
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
18035
|
+
if (contentType.includes("application/json")) {
|
|
18036
|
+
return await readJsonBody2(c);
|
|
18037
|
+
}
|
|
18038
|
+
return await readFormBody(c);
|
|
18039
|
+
}
|
|
18040
|
+
async function readJsonBody2(c) {
|
|
18041
|
+
try {
|
|
18042
|
+
return flatten(await c.req.json());
|
|
18043
|
+
} catch {
|
|
18044
|
+
return {};
|
|
18045
|
+
}
|
|
18046
|
+
}
|
|
18047
|
+
async function readFormBody(c) {
|
|
18048
|
+
try {
|
|
18049
|
+
return flatten(await c.req.parseBody());
|
|
18050
|
+
} catch {
|
|
18051
|
+
return {};
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
function flatten(parsed) {
|
|
18055
|
+
if (!parsed || typeof parsed !== "object") {
|
|
18056
|
+
return {};
|
|
18057
|
+
}
|
|
18058
|
+
const flat = {};
|
|
18059
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
18060
|
+
if (typeof value === "string") {
|
|
18061
|
+
flat[key] = value;
|
|
18062
|
+
}
|
|
18063
|
+
}
|
|
18064
|
+
return flat;
|
|
18065
|
+
}
|
|
18066
|
+
|
|
18067
|
+
// src/server/routes/oauth2/index.ts
|
|
18068
|
+
var registerOAuth2Client = route10.post("/_auth/oauth2/register").use([rateLimitPolicy8("auth-oauth2-register", { limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18069
|
+
requireAuthorizationServer();
|
|
18070
|
+
const body = await readRegistrationBody(c.raw);
|
|
18071
|
+
const result = await registerOAuth2ClientService(body, getClientIp2(c.raw) || null);
|
|
18072
|
+
if (!result.ok) {
|
|
18073
|
+
return oauth2ErrorResponse(c.raw, result.status, result.error, result.description);
|
|
18074
|
+
}
|
|
18075
|
+
return oauth2JsonResponse(c.raw, 201, result.client);
|
|
18076
|
+
});
|
|
18077
|
+
async function readRegistrationBody(c) {
|
|
18078
|
+
try {
|
|
18079
|
+
const parsed = await c.req.json();
|
|
18080
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
18081
|
+
} catch {
|
|
18082
|
+
return {};
|
|
18083
|
+
}
|
|
18084
|
+
}
|
|
18085
|
+
var listOAuth2Grants = route10.get("/_auth/oauth2/grants").use([authenticate]).handler(async (c) => {
|
|
18086
|
+
requireAuthorizationServer();
|
|
18087
|
+
return { grants: await listOAuth2GrantsService(Number(getAuth(c).userId)) };
|
|
18088
|
+
});
|
|
18089
|
+
var revokeOAuth2Grant = route10.delete("/_auth/oauth2/grants/:id").input({ params: Type.Object({ id: Type.Number({ description: "Grant id from the list" }) }) }).use([authenticate]).handler(async (c) => {
|
|
18090
|
+
requireAuthorizationServer();
|
|
18091
|
+
const { params } = await c.data();
|
|
18092
|
+
await revokeOAuth2GrantService(params.id, Number(getAuth(c).userId));
|
|
18093
|
+
return { revoked: true };
|
|
18094
|
+
});
|
|
18095
|
+
var oauth2AuthorizationServerMetadata = route10.get("/.well-known/oauth-authorization-server").skip(["auth"]).handler(async (c) => {
|
|
18096
|
+
const config4 = requireAuthorizationServer();
|
|
18097
|
+
return c.json({
|
|
18098
|
+
issuer: config4.issuer,
|
|
18099
|
+
authorization_endpoint: config4.authorizeUrl,
|
|
18100
|
+
token_endpoint: new URL("/_auth/oauth2/token", config4.issuer).toString(),
|
|
18101
|
+
registration_endpoint: new URL("/_auth/oauth2/register", config4.issuer).toString(),
|
|
18102
|
+
revocation_endpoint: new URL("/_auth/oauth2/revoke", config4.issuer).toString(),
|
|
18103
|
+
response_types_supported: SUPPORTED_RESPONSE_TYPES,
|
|
18104
|
+
grant_types_supported: SUPPORTED_GRANT_TYPES,
|
|
18105
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
18106
|
+
code_challenge_methods_supported: ["S256"],
|
|
18107
|
+
scopes_supported: Object.keys(config4.scopes)
|
|
18108
|
+
});
|
|
18109
|
+
});
|
|
18110
|
+
|
|
18111
|
+
// src/server/routes/oauth2/authorize.ts
|
|
18112
|
+
init_esm();
|
|
18113
|
+
import { route as route11 } from "@spfn/core/route";
|
|
18114
|
+
import { rateLimitPolicy as rateLimitPolicy9 } from "@spfn/core/middleware";
|
|
18115
|
+
var AUTHORIZE_FIELDS = {
|
|
18116
|
+
client_id: Type.String({ minLength: 1, description: "client_id from dynamic registration" }),
|
|
18117
|
+
redirect_uri: Type.String({ minLength: 1, description: "Where the code is sent; must be registered" }),
|
|
18118
|
+
code_challenge: Type.Optional(Type.String({ description: "PKCE S256 challenge" })),
|
|
18119
|
+
code_challenge_method: Type.Optional(Type.String({ description: "Must be S256" })),
|
|
18120
|
+
resource: Type.Optional(Type.String({ description: "RFC 8707 target the token will be good against" })),
|
|
18121
|
+
scope: Type.Optional(Type.String({ description: "Space-delimited scope names; absent asks for the default set" })),
|
|
18122
|
+
state: Type.Optional(Type.String({ description: "Client's opaque value, echoed back verbatim" }))
|
|
18123
|
+
};
|
|
18124
|
+
function toParams(input) {
|
|
18125
|
+
return {
|
|
18126
|
+
clientId: input.client_id,
|
|
18127
|
+
redirectUri: input.redirect_uri,
|
|
18128
|
+
codeChallenge: input.code_challenge,
|
|
18129
|
+
codeChallengeMethod: input.code_challenge_method,
|
|
18130
|
+
resource: input.resource,
|
|
18131
|
+
scope: input.scope,
|
|
18132
|
+
state: input.state
|
|
18133
|
+
};
|
|
18134
|
+
}
|
|
18135
|
+
var authorizeRateLimit = rateLimitPolicy9("auth-oauth2-authorize", {
|
|
18136
|
+
limit: 30,
|
|
18137
|
+
windowMs: 6e4,
|
|
18138
|
+
by: byIpAndCaller({ ipLimit: 120 })
|
|
18139
|
+
});
|
|
18140
|
+
var getOAuth2Authorize = route11.get("/_auth/oauth2/authorize").input({ query: Type.Object(AUTHORIZE_FIELDS) }).use([authenticate, authorizeRateLimit]).handler(async (c) => {
|
|
18141
|
+
requireAuthorizationServer();
|
|
18142
|
+
const { query } = await c.data();
|
|
18143
|
+
return await describeOAuth2AuthorizeRequestService(toParams(query));
|
|
18144
|
+
});
|
|
18145
|
+
var createOAuth2AuthorizationCode = route11.post("/_auth/oauth2/authorize").input({
|
|
18146
|
+
body: Type.Object({
|
|
18147
|
+
...AUTHORIZE_FIELDS,
|
|
18148
|
+
approve: Type.Boolean({ description: "What the account owner decided" })
|
|
18149
|
+
})
|
|
18150
|
+
}).use([authenticate, authorizeRateLimit]).handler(async (c) => {
|
|
18151
|
+
requireAuthorizationServer();
|
|
18152
|
+
const { body } = await c.data();
|
|
18153
|
+
const params = toParams(body);
|
|
18154
|
+
if (!body.approve) {
|
|
18155
|
+
return await denyOAuth2AuthorizeService(params);
|
|
18156
|
+
}
|
|
18157
|
+
return await approveOAuth2AuthorizeService(params, Number(getAuth(c).userId));
|
|
18158
|
+
});
|
|
18159
|
+
|
|
18160
|
+
// src/server/routes/oauth2/token.ts
|
|
18161
|
+
import { route as route12 } from "@spfn/core/route";
|
|
18162
|
+
import { rateLimitPolicy as rateLimitPolicy10 } from "@spfn/core/middleware";
|
|
18163
|
+
var oauth2Token = route12.post("/_auth/oauth2/token").use([rateLimitPolicy10("auth-oauth2-token", { limit: 60, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18164
|
+
requireAuthorizationServer();
|
|
18165
|
+
const result = await oauth2TokenService(await readOAuth2Body(c.raw));
|
|
18166
|
+
if (!result.ok) {
|
|
18167
|
+
return oauth2ErrorResponse(c.raw, 400, result.error, result.description);
|
|
18168
|
+
}
|
|
18169
|
+
return oauth2JsonResponse(c.raw, 200, result.tokens);
|
|
18170
|
+
});
|
|
18171
|
+
var oauth2Revoke = route12.post("/_auth/oauth2/revoke").use([rateLimitPolicy10("auth-oauth2-revoke", { limit: 60, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
18172
|
+
requireAuthorizationServer();
|
|
18173
|
+
const body = await readOAuth2Body(c.raw);
|
|
18174
|
+
if (!body.client_id) {
|
|
18175
|
+
return oauth2ErrorResponse(
|
|
18176
|
+
c.raw,
|
|
18177
|
+
400,
|
|
18178
|
+
"invalid_request",
|
|
18179
|
+
"client_id is required. A public client identifies itself with it (RFC 6749 \xA72.3.1)."
|
|
18180
|
+
);
|
|
18181
|
+
}
|
|
18182
|
+
await revokeOAuth2TokenService(body.token ?? "", body.client_id);
|
|
18183
|
+
return oauth2JsonResponse(c.raw, 200, {});
|
|
18184
|
+
});
|
|
18185
|
+
|
|
16754
18186
|
// src/server/routes/index.ts
|
|
16755
18187
|
var mainAuthRouter = defineRouter6({
|
|
16756
18188
|
// Auth routes
|
|
@@ -16828,7 +18260,17 @@ var mainAuthRouter = defineRouter6({
|
|
|
16828
18260
|
// Ops token routes (admin only)
|
|
16829
18261
|
issueOpsToken,
|
|
16830
18262
|
listOpsTokens,
|
|
16831
|
-
revokeOpsToken
|
|
18263
|
+
revokeOpsToken,
|
|
18264
|
+
// OAuth 2.1 authorization server routes (MCP clients).
|
|
18265
|
+
// Answer 404 unless the app passed `authorizationServer` to createAuthLifecycle().
|
|
18266
|
+
registerOAuth2Client,
|
|
18267
|
+
getOAuth2Authorize,
|
|
18268
|
+
createOAuth2AuthorizationCode,
|
|
18269
|
+
oauth2Token,
|
|
18270
|
+
oauth2Revoke,
|
|
18271
|
+
listOAuth2Grants,
|
|
18272
|
+
revokeOAuth2Grant,
|
|
18273
|
+
oauth2AuthorizationServerMetadata
|
|
16832
18274
|
});
|
|
16833
18275
|
|
|
16834
18276
|
// src/server.ts
|
|
@@ -16939,10 +18381,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
16939
18381
|
|
|
16940
18382
|
// src/server/lib/session.ts
|
|
16941
18383
|
import * as jose2 from "jose";
|
|
16942
|
-
import { env as
|
|
18384
|
+
import { env as env17 } from "@spfn/auth/config";
|
|
16943
18385
|
import { env as coreEnv } from "@spfn/core/config";
|
|
16944
18386
|
async function getSessionSecretKey() {
|
|
16945
|
-
const secret =
|
|
18387
|
+
const secret = env17.SPFN_AUTH_SESSION_SECRET;
|
|
16946
18388
|
const encoder = new TextEncoder();
|
|
16947
18389
|
const data = encoder.encode(secret);
|
|
16948
18390
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -17023,55 +18465,6 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
17023
18465
|
return hoursRemaining < thresholdHours;
|
|
17024
18466
|
}
|
|
17025
18467
|
|
|
17026
|
-
// src/server/lib/csrf.ts
|
|
17027
|
-
import { env as env17 } from "@spfn/auth/config";
|
|
17028
|
-
var CSRF_HEADER = "x-spfn-csrf";
|
|
17029
|
-
var CSRF_SUBKEY_LABEL = "spfn-auth-csrf-token-v1";
|
|
17030
|
-
var MAX_CANDIDATES = 32;
|
|
17031
|
-
function sessionSecret() {
|
|
17032
|
-
const secret = env17.SPFN_AUTH_SESSION_SECRET;
|
|
17033
|
-
if (!secret) {
|
|
17034
|
-
throw new Error(
|
|
17035
|
-
"SPFN_AUTH_SESSION_SECRET is required for CSRF protection. Set it (sessions need it anyway), or set SPFN_AUTH_CSRF=off."
|
|
17036
|
-
);
|
|
17037
|
-
}
|
|
17038
|
-
return secret;
|
|
17039
|
-
}
|
|
17040
|
-
async function hmacSha256(key, message) {
|
|
17041
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
17042
|
-
"raw",
|
|
17043
|
-
key.buffer,
|
|
17044
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
17045
|
-
false,
|
|
17046
|
-
["sign"]
|
|
17047
|
-
);
|
|
17048
|
-
const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(message));
|
|
17049
|
-
return new Uint8Array(signature);
|
|
17050
|
-
}
|
|
17051
|
-
function toHex(bytes) {
|
|
17052
|
-
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
17053
|
-
}
|
|
17054
|
-
async function deriveCsrfToken(keyId) {
|
|
17055
|
-
const subkey = await hmacSha256(new TextEncoder().encode(sessionSecret()), CSRF_SUBKEY_LABEL);
|
|
17056
|
-
return toHex(await hmacSha256(subkey, keyId));
|
|
17057
|
-
}
|
|
17058
|
-
function timingSafeEqualString(a, b) {
|
|
17059
|
-
if (a.length !== b.length) {
|
|
17060
|
-
return false;
|
|
17061
|
-
}
|
|
17062
|
-
let difference = 0;
|
|
17063
|
-
for (let i = 0; i < a.length; i++) {
|
|
17064
|
-
difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
17065
|
-
}
|
|
17066
|
-
return difference === 0;
|
|
17067
|
-
}
|
|
17068
|
-
function matchesCsrfToken(expected, presented) {
|
|
17069
|
-
if (!presented) {
|
|
17070
|
-
return false;
|
|
17071
|
-
}
|
|
17072
|
-
return presented.split(",", MAX_CANDIDATES).some((candidate) => timingSafeEqualString(expected, candidate.trim()));
|
|
17073
|
-
}
|
|
17074
|
-
|
|
17075
18468
|
// src/server/setup.ts
|
|
17076
18469
|
import { env as env18 } from "@spfn/auth/config";
|
|
17077
18470
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
@@ -17265,18 +18658,21 @@ function assertOAuthRedirectUris(env19 = process.env) {
|
|
|
17265
18658
|
function createAuthLifecycle(options = {}) {
|
|
17266
18659
|
configureDeletion(options.deletion);
|
|
17267
18660
|
configureDeviceAuth(options.deviceAuth);
|
|
18661
|
+
configureAuthorizationServer(options.authorizationServer);
|
|
17268
18662
|
return {
|
|
17269
18663
|
/**
|
|
17270
18664
|
* Initialize auth system after database is ready
|
|
17271
18665
|
*
|
|
17272
18666
|
* Performs:
|
|
17273
|
-
* 0. Refuses boot on an OAuth redirect URI override off the web app origin
|
|
18667
|
+
* 0. Refuses boot on an OAuth redirect URI override off the web app origin,
|
|
18668
|
+
* or on an authorization server issuer no client could use
|
|
17274
18669
|
* 1. Ensures admin account exists (creates if missing)
|
|
17275
18670
|
* 2. Initializes RBAC system with built-in + custom roles/permissions
|
|
17276
18671
|
* 3. Initializes one-time token manager
|
|
17277
18672
|
*/
|
|
17278
18673
|
afterInfrastructure: async () => {
|
|
17279
18674
|
assertOAuthRedirectUris();
|
|
18675
|
+
assertAuthorizationServerIssuer();
|
|
17280
18676
|
await initializeAuth(options);
|
|
17281
18677
|
try {
|
|
17282
18678
|
await normalizeStoredEmails();
|
|
@@ -17306,10 +18702,23 @@ function createAuthDeletionPurgeJob(cronExpression = DEFAULT_DELETION_PURGE_CRON
|
|
|
17306
18702
|
});
|
|
17307
18703
|
}
|
|
17308
18704
|
|
|
18705
|
+
// src/server/jobs/oauth2-client-purge.ts
|
|
18706
|
+
import { job as job3 } from "@spfn/core/job";
|
|
18707
|
+
var DEFAULT_OAUTH2_CLIENT_PURGE_CRON = "0 5 * * *";
|
|
18708
|
+
function createOAuth2ClientPurgeJob(cronExpression = DEFAULT_OAUTH2_CLIENT_PURGE_CRON) {
|
|
18709
|
+
return job3("auth.oauth2.client-purge").cron(cronExpression).options({ retryLimit: 1 }).handler(async () => {
|
|
18710
|
+
const { deleted } = await purgeStaleOAuth2ClientsService();
|
|
18711
|
+
if (deleted > 0) {
|
|
18712
|
+
authLogger.service.info("[auth.oauth2.client-purge] sweep complete", { deleted });
|
|
18713
|
+
}
|
|
18714
|
+
});
|
|
18715
|
+
}
|
|
18716
|
+
|
|
17309
18717
|
// src/server/jobs/index.ts
|
|
17310
18718
|
function createAuthJobRouter(options) {
|
|
17311
18719
|
return defineJobRouter({
|
|
17312
18720
|
deletionPurge: createAuthDeletionPurgeJob(options?.purgeCron),
|
|
18721
|
+
oauth2ClientPurge: createOAuth2ClientPurgeJob(options?.oauth2ClientPurgeCron),
|
|
17313
18722
|
linkMail: linkMailJob
|
|
17314
18723
|
});
|
|
17315
18724
|
}
|
|
@@ -17318,11 +18727,14 @@ var createAuthDeletionJobRouter = createAuthJobRouter;
|
|
|
17318
18727
|
export {
|
|
17319
18728
|
ACCOUNT_DELETION_REQUESTED_BY,
|
|
17320
18729
|
ACCOUNT_DELETION_REQUEST_STATUSES,
|
|
18730
|
+
AUTHORIZE_PATH,
|
|
17321
18731
|
AccountDeletionRequestsRepository,
|
|
17322
18732
|
AuthMetadataRepository,
|
|
17323
18733
|
AuthProviderSchema,
|
|
17324
18734
|
COOKIE_NAMES,
|
|
17325
18735
|
CSRF_HEADER,
|
|
18736
|
+
DEFAULT_ACCESS_TOKEN_TTL_MS,
|
|
18737
|
+
DEFAULT_CODE_TTL_MS,
|
|
17326
18738
|
DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE,
|
|
17327
18739
|
DEFAULT_DELETION_GRACE_PERIOD_DAYS,
|
|
17328
18740
|
DEFAULT_DELETION_PURGE_CRON,
|
|
@@ -17330,6 +18742,7 @@ export {
|
|
|
17330
18742
|
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
17331
18743
|
DEFAULT_DEVICE_AUTH_INTERVAL_MS,
|
|
17332
18744
|
DEFAULT_DEVICE_AUTH_TTL_MS,
|
|
18745
|
+
DEFAULT_REFRESH_TOKEN_TTL_MS,
|
|
17333
18746
|
DEVICE_AUTH_STATUSES,
|
|
17334
18747
|
DeviceAuthPollResponseSchema,
|
|
17335
18748
|
DeviceAuthorizationsRepository,
|
|
@@ -17345,6 +18758,14 @@ export {
|
|
|
17345
18758
|
KEY_PLATFORM,
|
|
17346
18759
|
KeyIdSchema,
|
|
17347
18760
|
KeysRepository,
|
|
18761
|
+
MAX_UNGRANTED_CLIENTS_PER_IP,
|
|
18762
|
+
OAUTH2_ACCESS_TOKEN_PREFIX,
|
|
18763
|
+
OAUTH2_REFRESH_TOKEN_PREFIX,
|
|
18764
|
+
OAUTH2_TOKEN_KINDS,
|
|
18765
|
+
OAuth2AuthorizationCodesRepository,
|
|
18766
|
+
OAuth2ClientsRepository,
|
|
18767
|
+
OAuth2GrantsRepository,
|
|
18768
|
+
OAuth2TokensRepository,
|
|
17348
18769
|
OPS_TOKEN_PREFIX,
|
|
17349
18770
|
OpsTokensRepository,
|
|
17350
18771
|
PASSKEY_DEVICE_TYPES,
|
|
@@ -17360,6 +18781,9 @@ export {
|
|
|
17360
18781
|
RolePermissionsRepository,
|
|
17361
18782
|
RolesRepository,
|
|
17362
18783
|
SOCIAL_PROVIDERS,
|
|
18784
|
+
STALE_CLIENT_MAX_AGE_MS,
|
|
18785
|
+
SUPPORTED_GRANT_TYPES,
|
|
18786
|
+
SUPPORTED_RESPONSE_TYPES,
|
|
17363
18787
|
SignupLinkTokensRepository,
|
|
17364
18788
|
SocialAccountsRepository,
|
|
17365
18789
|
TargetTypeSchema,
|
|
@@ -17383,6 +18807,8 @@ export {
|
|
|
17383
18807
|
addPermissionToRole,
|
|
17384
18808
|
appleProvider,
|
|
17385
18809
|
approveDeviceAuthService,
|
|
18810
|
+
approveOAuth2AuthorizeService,
|
|
18811
|
+
assertAuthorizationServerIssuer,
|
|
17386
18812
|
assertCanAssignRole,
|
|
17387
18813
|
assertKeyMatchesAlgorithm,
|
|
17388
18814
|
assertNotLastRecoveryCredential,
|
|
@@ -17409,6 +18835,7 @@ export {
|
|
|
17409
18835
|
completePasswordResetService,
|
|
17410
18836
|
completeSignupService,
|
|
17411
18837
|
configureAuth,
|
|
18838
|
+
configureAuthorizationServer,
|
|
17412
18839
|
configureDeletion,
|
|
17413
18840
|
configureDeviceAuth,
|
|
17414
18841
|
configureOAuthTokenCipher,
|
|
@@ -17426,7 +18853,9 @@ export {
|
|
|
17426
18853
|
deleteInvitation,
|
|
17427
18854
|
deleteRole,
|
|
17428
18855
|
denyDeviceAuthService,
|
|
18856
|
+
denyOAuth2AuthorizeService,
|
|
17429
18857
|
deriveCsrfToken,
|
|
18858
|
+
describeOAuth2AuthorizeRequestService,
|
|
17430
18859
|
deviceAuthorizations,
|
|
17431
18860
|
deviceAuthorizationsRepository,
|
|
17432
18861
|
encryptToken,
|
|
@@ -17435,18 +18864,22 @@ export {
|
|
|
17435
18864
|
finishPasskeyEnrollmentService,
|
|
17436
18865
|
finishPasskeyLoginService,
|
|
17437
18866
|
formatUserCode,
|
|
18867
|
+
generateAccessToken,
|
|
18868
|
+
generateAuthorizationCode,
|
|
17438
18869
|
generateClientToken,
|
|
17439
18870
|
generateDeviceCode,
|
|
17440
18871
|
generateKeyPair,
|
|
17441
18872
|
generateKeyPairES256,
|
|
17442
18873
|
generateKeyPairRS256,
|
|
17443
18874
|
generateOAuthNonce,
|
|
18875
|
+
generateRefreshToken,
|
|
17444
18876
|
generateToken,
|
|
17445
18877
|
generateUserCode,
|
|
17446
18878
|
getAllRoles,
|
|
17447
18879
|
getAuth,
|
|
17448
18880
|
getAuthConfig,
|
|
17449
18881
|
getAuthSessionService,
|
|
18882
|
+
getAuthorizationServerConfig,
|
|
17450
18883
|
getCsrfExemptPaths,
|
|
17451
18884
|
getCsrfMode,
|
|
17452
18885
|
getDeletionConfig,
|
|
@@ -17492,16 +18925,21 @@ export {
|
|
|
17492
18925
|
hasPermission,
|
|
17493
18926
|
hasRole,
|
|
17494
18927
|
hashDeviceCode,
|
|
18928
|
+
hashOAuth2Secret,
|
|
17495
18929
|
hashPassword,
|
|
17496
18930
|
initOneTimeTokenManager,
|
|
17497
18931
|
initializeAuth,
|
|
17498
18932
|
invitationAcceptedEvent,
|
|
17499
18933
|
invitationCreatedEvent,
|
|
17500
18934
|
invitationsRepository,
|
|
18935
|
+
isAccessTokenShaped,
|
|
17501
18936
|
isEncrypted,
|
|
17502
18937
|
isGoogleOAuthEnabled,
|
|
18938
|
+
isLoopbackHostname,
|
|
17503
18939
|
isOAuthProviderEnabled,
|
|
17504
18940
|
isOpsToken,
|
|
18941
|
+
isPkceS256ChallengeShaped,
|
|
18942
|
+
isPkceVerifierShaped,
|
|
17505
18943
|
isSafeReturnPath,
|
|
17506
18944
|
issueOneTimeTokenService,
|
|
17507
18945
|
issueOpsTokenService,
|
|
@@ -17510,6 +18948,7 @@ export {
|
|
|
17510
18948
|
linkMailJob,
|
|
17511
18949
|
listInvitations,
|
|
17512
18950
|
listKeysService,
|
|
18951
|
+
listOAuth2GrantsService,
|
|
17513
18952
|
listOpsTokensService,
|
|
17514
18953
|
listPasskeysService,
|
|
17515
18954
|
loginService,
|
|
@@ -17517,11 +18956,22 @@ export {
|
|
|
17517
18956
|
machineAuth,
|
|
17518
18957
|
matchOAuthCsrfCookies,
|
|
17519
18958
|
matchesCsrfToken,
|
|
18959
|
+
matchesRegisteredRedirectUri,
|
|
17520
18960
|
naverProvider,
|
|
17521
18961
|
normalizeEmail,
|
|
17522
18962
|
normalizeOptionalEmail,
|
|
18963
|
+
normalizeResource,
|
|
17523
18964
|
normalizeStoredEmails,
|
|
17524
18965
|
normalizeUserCode,
|
|
18966
|
+
oauth2AuthorizationCodes,
|
|
18967
|
+
oauth2AuthorizationCodesRepository,
|
|
18968
|
+
oauth2Clients,
|
|
18969
|
+
oauth2ClientsRepository,
|
|
18970
|
+
oauth2Grants,
|
|
18971
|
+
oauth2GrantsRepository,
|
|
18972
|
+
oauth2TokenService,
|
|
18973
|
+
oauth2Tokens,
|
|
18974
|
+
oauth2TokensRepository,
|
|
17525
18975
|
oauthCallbackService,
|
|
17526
18976
|
oauthNativeService,
|
|
17527
18977
|
oauthStartService,
|
|
@@ -17540,11 +18990,16 @@ export {
|
|
|
17540
18990
|
passwordResetTokensRepository,
|
|
17541
18991
|
permissions,
|
|
17542
18992
|
permissionsRepository,
|
|
18993
|
+
pkceChallengeFor,
|
|
17543
18994
|
pollDeviceAuthService,
|
|
18995
|
+
purgeStaleOAuth2ClientsService,
|
|
17544
18996
|
purgeUserService,
|
|
18997
|
+
redirectHostOf,
|
|
17545
18998
|
refreshAccessToken,
|
|
18999
|
+
refuseRedirectUriRegistration,
|
|
17546
19000
|
registerAuthProfile,
|
|
17547
19001
|
registerMachineVerifier,
|
|
19002
|
+
registerOAuth2ClientService,
|
|
17548
19003
|
registerOAuthProvider,
|
|
17549
19004
|
registerPublicKeyService,
|
|
17550
19005
|
registerService,
|
|
@@ -17561,8 +19016,12 @@ export {
|
|
|
17561
19016
|
requireRole,
|
|
17562
19017
|
resendInvitation,
|
|
17563
19018
|
resolveAuthenticatedUser,
|
|
19019
|
+
resolveIssuerSource,
|
|
17564
19020
|
revokeAllKeysService,
|
|
19021
|
+
revokeAllOAuth2GrantsForUser,
|
|
17565
19022
|
revokeKeyService,
|
|
19023
|
+
revokeOAuth2GrantService,
|
|
19024
|
+
revokeOAuth2TokenService,
|
|
17566
19025
|
revokeOpsTokenService,
|
|
17567
19026
|
revokePasskeyService,
|
|
17568
19027
|
roleGuard,
|
|
@@ -17573,7 +19032,10 @@ export {
|
|
|
17573
19032
|
rotateKeyService,
|
|
17574
19033
|
runAuthProfile,
|
|
17575
19034
|
runBeforeRegister,
|
|
19035
|
+
sameOAuth2Hash,
|
|
19036
|
+
sameResource,
|
|
17576
19037
|
sealSession,
|
|
19038
|
+
secondsUntil,
|
|
17577
19039
|
selectAuthProfile,
|
|
17578
19040
|
sendVerificationCodeService,
|
|
17579
19041
|
setRolePermissions,
|
|
@@ -17586,6 +19048,7 @@ export {
|
|
|
17586
19048
|
startPasskeyEnrollmentService,
|
|
17587
19049
|
startPasskeyLoginService,
|
|
17588
19050
|
sweepDuePurges,
|
|
19051
|
+
toEpochSeconds,
|
|
17589
19052
|
unsealSession,
|
|
17590
19053
|
updateLastLoginService,
|
|
17591
19054
|
updateLocaleService,
|
|
@@ -17606,6 +19069,7 @@ export {
|
|
|
17606
19069
|
validatePasswordStrength,
|
|
17607
19070
|
verificationCodes,
|
|
17608
19071
|
verificationCodesRepository,
|
|
19072
|
+
verifyAccessToken,
|
|
17609
19073
|
verifyClientToken,
|
|
17610
19074
|
verifyCodeService,
|
|
17611
19075
|
verifyIdToken,
|