@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/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(index16) {
901
- return CreateType({ [Kind]: "Argument", index: index16 });
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, index16, char) {
1140
- return pattern[index16] === char && pattern.charCodeAt(index16 - 1) !== 92;
1139
+ function IsNonEscaped(pattern, index18, char) {
1140
+ return pattern[index18] === char && pattern.charCodeAt(index18 - 1) !== 92;
1141
1141
  }
1142
- function IsOpenParen(pattern, index16) {
1143
- return IsNonEscaped(pattern, index16, "(");
1142
+ function IsOpenParen(pattern, index18) {
1143
+ return IsNonEscaped(pattern, index18, "(");
1144
1144
  }
1145
- function IsCloseParen(pattern, index16) {
1146
- return IsNonEscaped(pattern, index16, ")");
1145
+ function IsCloseParen(pattern, index18) {
1146
+ return IsNonEscaped(pattern, index18, ")");
1147
1147
  }
1148
- function IsSeparator(pattern, index16) {
1149
- return IsNonEscaped(pattern, index16, "|");
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 index16 = 0; index16 < pattern.length; index16++) {
1156
- if (IsOpenParen(pattern, index16))
1155
+ for (let index18 = 0; index18 < pattern.length; index18++) {
1156
+ if (IsOpenParen(pattern, index18))
1157
1157
  count += 1;
1158
- if (IsCloseParen(pattern, index16))
1158
+ if (IsCloseParen(pattern, index18))
1159
1159
  count -= 1;
1160
- if (count === 0 && index16 !== pattern.length - 1)
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 index16 = 0; index16 < pattern.length; index16++) {
1171
- if (IsOpenParen(pattern, index16))
1170
+ for (let index18 = 0; index18 < pattern.length; index18++) {
1171
+ if (IsOpenParen(pattern, index18))
1172
1172
  count += 1;
1173
- if (IsCloseParen(pattern, index16))
1173
+ if (IsCloseParen(pattern, index18))
1174
1174
  count -= 1;
1175
- if (IsSeparator(pattern, index16) && count === 0)
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 index16 = 0; index16 < pattern.length; index16++) {
1182
- if (IsOpenParen(pattern, index16))
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 index16 = 0; index16 < pattern.length; index16++) {
1191
- if (IsOpenParen(pattern, index16))
1190
+ for (let index18 = 0; index18 < pattern.length; index18++) {
1191
+ if (IsOpenParen(pattern, index18))
1192
1192
  count += 1;
1193
- if (IsCloseParen(pattern, index16))
1193
+ if (IsCloseParen(pattern, index18))
1194
1194
  count -= 1;
1195
- if (IsSeparator(pattern, index16) && count === 0) {
1196
- const range2 = pattern.slice(start, index16);
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 = index16 + 1;
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, index16) {
1213
- if (!IsOpenParen(value, index16))
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 = index16; scan < value.length; 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 [index16, scan];
1222
+ return [index18, scan];
1223
1223
  }
1224
1224
  throw new TemplateLiteralParserError(`TemplateLiteralParser: Unclosed group parens in expression`);
1225
1225
  }
1226
- function Range(pattern2, index16) {
1227
- for (let scan = index16; scan < pattern2.length; scan++) {
1226
+ function Range(pattern2, index18) {
1227
+ for (let scan = index18; scan < pattern2.length; scan++) {
1228
1228
  if (IsOpenParen(pattern2, scan))
1229
- return [index16, scan];
1229
+ return [index18, scan];
1230
1230
  }
1231
- return [index16, pattern2.length];
1231
+ return [index18, pattern2.length];
1232
1232
  }
1233
1233
  const expressions = [];
1234
- for (let index16 = 0; index16 < pattern.length; index16++) {
1235
- if (IsOpenParen(pattern, index16)) {
1236
- const [start, end] = Group(pattern, index16);
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
- index16 = end;
1239
+ index18 = end;
1240
1240
  } else {
1241
- const [start, end] = Range(pattern, index16);
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
- index16 = end - 1;
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, index16) => IntoBooleanResult(Visit3(right.parameters[index16], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
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, index16) => IntoBooleanResult(Visit3(right.parameters[index16], schema)) === ExtendsResult.True) ? ExtendsResult.False : IntoBooleanResult(Visit3(left.returns, right.returns));
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, index16) => Visit3(schema, right.items[index16]) === ExtendsResult.True) ? ExtendsResult.True : ExtendsResult.False;
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 text16, timestamp } from "drizzle-orm/pg-core";
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: text16("key").primaryKey(),
5913
+ key: text20("key").primaryKey(),
5756
5914
  // Metadata value
5757
- value: text16("value").notNull(),
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 text17 } from "drizzle-orm/pg-core";
5767
- import { id as id17, timestamps as timestamps15, utcTimestamp as utcTimestamp13 } from "@spfn/core/db";
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: id17(),
5934
+ id: id21(),
5777
5935
  // Operator-facing label ("ci-deploy", "rayim-laptop")
5778
- name: text17("name").notNull(),
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: text17("token_hash").notNull().unique(),
5939
+ tokenHash: text21("token_hash").notNull().unique(),
5782
5940
  // Granted scopes as permission strings ('waitlist:read', ...).
5783
5941
  // '*' grants every scope.
5784
- scopes: text17("scopes").array().notNull(),
5942
+ scopes: text21("scopes").array().notNull(),
5785
5943
  // null = the token does not expire
5786
- expiresAt: utcTimestamp13("expires_at"),
5944
+ expiresAt: utcTimestamp17("expires_at"),
5787
5945
  // null = active; a timestamp revokes the token permanently
5788
- revokedAt: utcTimestamp13("revoked_at"),
5946
+ revokedAt: utcTimestamp17("revoked_at"),
5789
5947
  // Last successful verification, updated fire-and-forget
5790
- lastUsedAt: utcTimestamp13("last_used_at"),
5791
- ...timestamps15()
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(id18) {
5839
- const result = await this.readDb.select().from(users).where(eq(users.id, id18)).limit(1);
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(id18) {
5850
- const result = await this.db.select().from(users).where(eq(users.id, id18)).limit(1);
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(id18) {
5867
- await this.db.select({ id: users.id }).from(users).where(eq(users.id, id18)).for("update");
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(id18) {
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, id18)).limit(1);
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(id18, data) {
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, id18)).returning();
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(id18) {
6194
+ async reactivateFromPendingDeletion(id22) {
6033
6195
  const result = await this.db.update(users).set({ status: "active" }).where(
6034
6196
  and(
6035
- eq(users.id, id18),
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(id18, passwordHash, clearPasswordChangeRequired = true) {
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, id18)).returning();
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(id18) {
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, id18)).returning();
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(id18) {
6070
- const result = await this.db.delete(users).where(eq(users.id, id18)).returning();
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(id18, identity) {
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, id18), lastUsedIsStale));
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, id18),
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(id18) {
6482
- const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id18)).limit(1);
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(id18) {
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, id18)).returning();
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(id18) {
6507
- const code = await this.findById(id18);
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, id18)).returning();
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(id18, tokenHash) {
6757
+ async issue(id22, tokenHash) {
6596
6758
  const result = await this.db.update(signupLinkTokens).set({ tokenHash }).where(
6597
6759
  and4(
6598
- eq4(signupLinkTokens.id, id18),
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(id18, setupSecretHash, setupExpiresAt) {
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, id18),
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(id18) {
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, id18),
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(id18, tokenHash) {
6901
+ async issue(id22, tokenHash) {
6740
6902
  const result = await this.db.update(passwordResetTokens).set({ tokenHash }).where(
6741
6903
  and5(
6742
- eq5(passwordResetTokens.id, id18),
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(id18, setupSecretHash, setupExpiresAt) {
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, id18),
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(id18) {
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, id18),
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(id18, userId) {
7056
+ async findLiveByIdAndUserId(id22, userId) {
6895
7057
  const result = await this.db.select().from(passkeys).where(and6(
6896
- eq6(passkeys.id, id18),
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(id18, counter) {
6907
- await this.db.update(passkeys).set({ counter, lastUsedAt: /* @__PURE__ */ new Date() }).where(eq6(passkeys.id, id18));
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(id18, userId, label) {
7076
+ async renameByIdAndUserId(id22, userId, label) {
6915
7077
  const result = await this.db.update(passkeys).set({ label }).where(and6(
6916
- eq6(passkeys.id, id18),
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(id18, userId, reason) {
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, id18),
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(id18, userId) {
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, id18),
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(id18) {
7245
+ async deny(id22) {
7084
7246
  const result = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
7085
7247
  and8(
7086
- eq8(deviceAuthorizations.id, id18),
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(id18) {
7165
- const result = await this.readDb.select().from(roles).where(eq9(roles.id, id18)).limit(1);
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(id18, data) {
7197
- const result = await this.db.update(roles).set(data).where(eq9(roles.id, id18)).returning();
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(id18) {
7204
- const result = await this.db.delete(roles).where(eq9(roles.id, id18)).returning();
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(id18) {
7225
- const result = await this.readDb.select().from(permissions).where(eq10(permissions.id, id18)).limit(1);
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(id18, data) {
7277
- const result = await this.db.update(permissions).set(data).where(eq10(permissions.id, id18)).returning();
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(id18) {
7284
- const result = await this.db.delete(permissions).where(eq10(permissions.id, id18)).returning();
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(id18, data) {
7415
- const result = await this.db.update(userPermissions).set(data).where(eq12(userPermissions.id, id18)).returning();
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(id18) {
7468
- const result = await this.readDb.select().from(userProfiles).where(eq13(userProfiles.id, id18)).limit(1);
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(id18, data) {
7495
- const result = await this.db.update(userProfiles).set(data).where(eq13(userProfiles.id, id18)).returning();
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(id18) {
7509
- const result = await this.db.delete(userProfiles).where(eq13(userProfiles.id, id18)).returning();
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(id18) {
7601
- const result = await this.readDb.select().from(userInvitations).where(eq14(userInvitations.id, id18)).limit(1);
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(id18, status, timestamp2) {
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, id18)).returning();
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(id18) {
7662
- const result = await this.db.delete(userInvitations).where(eq14(userInvitations.id, id18)).returning();
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(id18, data) {
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, id18)).returning();
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(id18, newExpiresAt) {
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, id18)).returning();
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(id18, cancelledBy, reason, currentMetadata) {
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, id18)).returning();
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(id18, data) {
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, id18)).limit(1);
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, id18)).returning();
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(id18) {
8662
- const result = await this.db.delete(userSocialAccounts).where(eq15(userSocialAccounts.id, id18)).returning();
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(id18) {
8743
- const result = await this.readDb.select().from(accountDeletionRequests).where(eq17(accountDeletionRequests.id, id18)).limit(1);
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(id18) {
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, id18),
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(id18, purgeStrategy) {
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, id18),
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(id18) {
8876
- const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and14(eq18(opsTokens.id, id18), isNull8(opsTokens.revokedAt))).returning();
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(id18) {
8881
- await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq18(opsTokens.id, id18));
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((id18) => Number(id18));
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((id18) => permissionsRepository.findById(id18))
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/lib/config.ts
9270
- init_email();
9271
- import { env as env4 } from "@spfn/auth/config";
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(config3) {
9872
+ function configureAuth(config4) {
9359
9873
  globalConfig = {
9360
9874
  ...globalConfig,
9361
- ...config3
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, text18) {
10234
- const result = await sendEmail3({ to, subject, text: text18 });
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 config3 = getDeletionConfig();
10806
+ const config4 = getDeletionConfig();
10287
10807
  const wantsImmediate = immediate === true;
10288
- if (wantsImmediate && requestedBy === "self" && !config3.allowSelfImmediate) {
10808
+ if (wantsImmediate && requestedBy === "self" && !config4.allowSelfImmediate) {
10289
10809
  throw new ImmediateDeletionNotAllowedError();
10290
10810
  }
10291
- const gracePeriodDays = wantsImmediate ? 0 : config3.gracePeriodDays;
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 config3 = getDeletionConfig();
10398
- if (config3.onBeforePurge) {
10918
+ const config4 = getDeletionConfig();
10919
+ if (config4.onBeforePurge) {
10399
10920
  try {
10400
- await config3.onBeforePurge({
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 = config3.purgeStrategy;
10935
+ const purgeStrategy = config4.purgeStrategy;
10415
10936
  let purgedUser = null;
10416
- await runInTransaction(async () => {
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 runInTransaction2 } from "@spfn/core/db";
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(text18) {
11784
+ function parseJson(text22) {
11262
11785
  try {
11263
- return JSON.parse(text18);
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 runInTransaction2(async () => {
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 config3 of configs) {
11567
- const existing = existingByName.get(config3.name);
12089
+ for (const config4 of configs) {
12090
+ const existing = existingByName.get(config4.name);
11568
12091
  if (!existing) {
11569
12092
  await rolesRepository.create({
11570
- name: config3.name,
11571
- displayName: config3.displayName,
11572
- description: config3.description || null,
11573
- priority: config3.priority ?? 10,
11574
- isSystem: config3.isSystem ?? false,
11575
- isBuiltin: config3.isBuiltin ?? false,
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: ${config3.name}`);
12101
+ authLogger.service.info(` \u2705 Created role: ${config4.name}`);
11579
12102
  } else {
11580
12103
  const updateData = {
11581
- displayName: config3.displayName,
11582
- description: config3.description || null
12104
+ displayName: config4.displayName,
12105
+ description: config4.description || null
11583
12106
  };
11584
12107
  if (!existing.isBuiltin) {
11585
- updateData.priority = config3.priority ?? existing.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 config3 of configs) {
11593
- const existing = existingByName.get(config3.name);
12115
+ for (const config4 of configs) {
12116
+ const existing = existingByName.get(config4.name);
11594
12117
  if (!existing) {
11595
12118
  await permissionsRepository.create({
11596
- name: config3.name,
11597
- displayName: config3.displayName,
11598
- description: config3.description || null,
11599
- category: config3.category || null,
11600
- isSystem: config3.isSystem ?? false,
11601
- isBuiltin: config3.isBuiltin ?? false,
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: ${config3.name}`);
12128
+ authLogger.service.info(` \u2705 Created permission: ${config4.name}`);
11606
12129
  } else {
11607
12130
  await permissionsRepository.updateById(existing.id, {
11608
- displayName: config3.displayName,
11609
- description: config3.description || null,
11610
- category: config3.category || null
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((id18) => permissionsRepository.findById(id18))
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(id18, cancelledBy, reason) {
11886
- const invitation = await invitationsRepository.findById(id18);
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(id18, cancelledBy, reason, invitation.metadata);
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(id18) {
11897
- await invitationsRepository.deleteById(id18);
11898
- console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id18}`);
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(id18, expiresInDays = 7) {
11908
- const invitation = await invitationsRepository.findById(id18);
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(id18, newExpiresAt);
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(config3) {
12478
+ function initOneTimeTokenManager(config4) {
11956
12479
  if (manager) {
11957
12480
  manager.destroy();
11958
12481
  }
11959
12482
  manager = new SSETokenManager({
11960
- ttl: config3?.ttl,
11961
- store: config3?.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 config3 = getGoogleOAuthConfig();
12632
+ const config4 = getGoogleOAuthConfig();
12110
12633
  const params = new URLSearchParams({
12111
- client_id: config3.clientId,
12112
- redirect_uri: config3.redirectUri,
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 config3 = getGoogleOAuthConfig();
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: config3.clientId,
12132
- client_secret: config3.clientSecret,
12133
- redirect_uri: config3.redirectUri,
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 config3 = getGoogleOAuthConfig();
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: config3.clientId,
12165
- client_secret: config3.clientSecret,
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(id18) {
12231
- return registry2.get(id18);
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 config3 = getGithubConfig();
13008
+ const config4 = getGithubConfig();
12486
13009
  const params = new URLSearchParams({
12487
- client_id: config3.clientId,
12488
- redirect_uri: config3.redirectUri,
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 config3 = getGithubConfig();
13018
+ const config4 = getGithubConfig();
12496
13019
  return requestGithubTokens(new URLSearchParams({
12497
- client_id: config3.clientId,
12498
- client_secret: config3.clientSecret,
12499
- redirect_uri: config3.redirectUri,
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 config3 = getGithubConfig();
13052
+ const config4 = getGithubConfig();
12530
13053
  return requestGithubTokens(new URLSearchParams({
12531
13054
  grant_type: "refresh_token",
12532
- client_id: config3.clientId,
12533
- client_secret: config3.clientSecret,
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 config3 = getKakaoConfig();
13179
+ const config4 = getKakaoConfig();
12657
13180
  const params = new URLSearchParams({
12658
13181
  response_type: "code",
12659
- client_id: config3.clientId,
12660
- redirect_uri: config3.redirectUri,
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 config3 = getKakaoConfig();
13190
+ const config4 = getKakaoConfig();
12668
13191
  const params = new URLSearchParams({
12669
13192
  grant_type: "authorization_code",
12670
- client_id: config3.clientId,
12671
- redirect_uri: config3.redirectUri,
13193
+ client_id: config4.clientId,
13194
+ redirect_uri: config4.redirectUri,
12672
13195
  code
12673
13196
  });
12674
- if (config3.clientSecret) {
12675
- params.set("client_secret", config3.clientSecret);
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 config3 = getKakaoConfig();
13240
+ const config4 = getKakaoConfig();
12718
13241
  const params = new URLSearchParams({
12719
13242
  grant_type: "refresh_token",
12720
- client_id: config3.clientId,
13243
+ client_id: config4.clientId,
12721
13244
  refresh_token: refreshToken
12722
13245
  });
12723
- if (config3.clientSecret) {
12724
- params.set("client_secret", config3.clientSecret);
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 config3 = getNaverConfig();
13405
+ const config4 = getNaverConfig();
12883
13406
  const params = new URLSearchParams({
12884
13407
  response_type: "code",
12885
- client_id: config3.clientId,
12886
- redirect_uri: config3.redirectUri,
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 config3 = getNaverConfig();
13415
+ const config4 = getNaverConfig();
12893
13416
  return requestNaverTokens(new URLSearchParams({
12894
13417
  grant_type: "authorization_code",
12895
- client_id: config3.clientId,
12896
- client_secret: config3.clientSecret,
12897
- redirect_uri: config3.redirectUri,
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 config3 = getNaverConfig();
13461
+ const config4 = getNaverConfig();
12939
13462
  return requestNaverTokens(new URLSearchParams({
12940
13463
  grant_type: "refresh_token",
12941
- client_id: config3.clientId,
12942
- client_secret: config3.clientSecret,
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 runInTransaction3, onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
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 runInTransaction3(async () => {
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(id18) {
13375
- return await opsTokensRepository.revokeById(id18);
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/routes/auth/index.ts
13382
- init_esm();
13383
- import { Transactional } from "@spfn/core/db";
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/rate-limit-keys.ts
13387
- init_email();
13388
- import { createHash as createHash6 } from "crypto";
13389
- import { getClientIp } from "@spfn/core/middleware";
13390
- async function readJsonBody(c) {
13391
- try {
13392
- return await c.req.json();
13393
- } catch {
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 accountKey(body) {
13398
- if (typeof body.email === "string" && body.email.trim()) {
13399
- return `email:${normalizeEmail(body.email)}`;
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
- if (typeof body.phone === "string" && body.phone.trim()) {
13402
- return `phone:${body.phone.trim()}`;
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
- return void 0;
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 targetKey(body) {
13407
- if (typeof body.target !== "string" || !body.target.trim()) {
13408
- return void 0;
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 byIpAndAccount(options = {}) {
13415
- return async (c) => {
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 byIpAndCaller(options = {}) {
13425
- return async (c) => {
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 idTokenKey(body) {
13434
- if (typeof body.idToken !== "string" || !body.idToken) {
13435
- return void 0;
13959
+ function assertAuthorizationServerIssuer() {
13960
+ const resolved = getAuthorizationServerConfig();
13961
+ if (!resolved) {
13962
+ return;
13436
13963
  }
13437
- return `tok:${createHash6("sha256").update(body.idToken).digest("hex")}`;
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 byIpAndIdToken(options = {}) {
13440
- return async (c) => {
13441
- const body = await readJsonBody(c);
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 byIpAndTarget(options = {}) {
13449
- return async (c) => {
13450
- const body = await readJsonBody(c);
13451
- const target = targetKey(body);
13452
- return [
13453
- { key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
13454
- target ? `tgt:${target}` : void 0
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 randomBytes3 } from "crypto";
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 text18;
15235
+ let text22;
13984
15236
  try {
13985
- text18 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
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(text18);
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(text18) {
14011
- this.text = text18;
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 randomBytes3(16).toString("hex");
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 createHash8 } from "crypto";
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 createHash7, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
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 createHash7("sha256").update(bytes).digest("hex");
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(config3) {
15739
- const roles2 = config3.roles ?? [];
15740
- const permissions2 = config3.permissions ?? [];
15741
- if (!config3.opsScopes || config3.opsScopes.length === 0) {
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(...config3.opsScopes)]);
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 (index16) => {
15763
- const produced = index16 < handlers.length ? await handlers[index16](c, () => runFrom(index16 + 1)) : await next();
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 env16 } from "@spfn/auth/config";
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 = env16.SPFN_AUTH_SESSION_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,