@technomoron/api-server-base 2.0.0-beta.17 → 2.0.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.
Files changed (63) hide show
  1. package/README.txt +48 -35
  2. package/dist/cjs/api-module.cjs +9 -0
  3. package/dist/cjs/api-module.d.ts +4 -2
  4. package/dist/cjs/api-server-base.cjs +178 -57
  5. package/dist/cjs/api-server-base.d.ts +31 -2
  6. package/dist/cjs/auth-api/auth-module.d.ts +12 -1
  7. package/dist/cjs/auth-api/auth-module.js +77 -35
  8. package/dist/cjs/auth-api/mem-auth-store.js +2 -23
  9. package/dist/cjs/auth-api/sql-auth-store.js +4 -31
  10. package/dist/cjs/auth-api/user-id.d.ts +4 -0
  11. package/dist/cjs/auth-api/user-id.js +31 -0
  12. package/dist/cjs/auth-cookie-options.d.ts +11 -0
  13. package/dist/cjs/auth-cookie-options.js +57 -0
  14. package/dist/cjs/oauth/memory.js +4 -10
  15. package/dist/cjs/oauth/models.js +4 -15
  16. package/dist/cjs/oauth/sequelize.js +8 -23
  17. package/dist/cjs/passkey/config.d.ts +2 -0
  18. package/dist/cjs/passkey/config.js +26 -0
  19. package/dist/cjs/passkey/memory.js +2 -9
  20. package/dist/cjs/passkey/models.js +4 -15
  21. package/dist/cjs/passkey/sequelize.js +6 -22
  22. package/dist/cjs/passkey/service.js +1 -1
  23. package/dist/cjs/passkey/types.d.ts +5 -0
  24. package/dist/cjs/sequelize-utils.d.ts +3 -0
  25. package/dist/cjs/sequelize-utils.js +17 -0
  26. package/dist/cjs/token/memory.d.ts +4 -0
  27. package/dist/cjs/token/memory.js +90 -25
  28. package/dist/cjs/token/sequelize.js +16 -22
  29. package/dist/cjs/token/types.d.ts +7 -0
  30. package/dist/cjs/user/memory.js +2 -9
  31. package/dist/cjs/user/sequelize.js +6 -22
  32. package/dist/esm/api-module.d.ts +4 -2
  33. package/dist/esm/api-module.js +9 -0
  34. package/dist/esm/api-server-base.d.ts +31 -2
  35. package/dist/esm/api-server-base.js +178 -57
  36. package/dist/esm/auth-api/auth-module.d.ts +12 -1
  37. package/dist/esm/auth-api/auth-module.js +77 -35
  38. package/dist/esm/auth-api/mem-auth-store.js +1 -22
  39. package/dist/esm/auth-api/sql-auth-store.js +2 -29
  40. package/dist/esm/auth-api/user-id.d.ts +4 -0
  41. package/dist/esm/auth-api/user-id.js +26 -0
  42. package/dist/esm/auth-cookie-options.d.ts +11 -0
  43. package/dist/esm/auth-cookie-options.js +54 -0
  44. package/dist/esm/oauth/memory.js +4 -10
  45. package/dist/esm/oauth/models.js +1 -12
  46. package/dist/esm/oauth/sequelize.js +5 -20
  47. package/dist/esm/passkey/config.d.ts +2 -0
  48. package/dist/esm/passkey/config.js +23 -0
  49. package/dist/esm/passkey/memory.js +2 -9
  50. package/dist/esm/passkey/models.js +1 -12
  51. package/dist/esm/passkey/sequelize.js +3 -19
  52. package/dist/esm/passkey/service.js +1 -1
  53. package/dist/esm/passkey/types.d.ts +5 -0
  54. package/dist/esm/sequelize-utils.d.ts +3 -0
  55. package/dist/esm/sequelize-utils.js +12 -0
  56. package/dist/esm/token/memory.d.ts +4 -0
  57. package/dist/esm/token/memory.js +90 -25
  58. package/dist/esm/token/sequelize.js +12 -18
  59. package/dist/esm/token/types.d.ts +7 -0
  60. package/dist/esm/user/memory.js +2 -9
  61. package/dist/esm/user/sequelize.js +3 -19
  62. package/docs/swagger/openapi.json +11 -145
  63. package/package.json +12 -12
@@ -100,7 +100,21 @@ export interface ApiServerConf {
100
100
  swaggerPath?: string;
101
101
  accessSecret: string;
102
102
  refreshSecret: string;
103
+ /** Cookie domain for auth cookies. Prefer leaving empty for localhost/development. */
103
104
  cookieDomain: string;
105
+ /** Cookie path for auth cookies. */
106
+ cookiePath?: string;
107
+ /** Cookie SameSite attribute for auth cookies. */
108
+ cookieSameSite?: 'lax' | 'strict' | 'none';
109
+ /**
110
+ * Cookie Secure attribute for auth cookies.
111
+ * - true: always secure
112
+ * - false: never secure
113
+ * - 'auto': secure when request is HTTPS (or forwarded as HTTPS)
114
+ */
115
+ cookieSecure?: boolean | 'auto';
116
+ /** Cookie HttpOnly attribute for auth cookies. */
117
+ cookieHttpOnly?: boolean;
104
118
  accessCookie: string;
105
119
  refreshCookie: string;
106
120
  accessExpiry: number;
@@ -115,24 +129,38 @@ export interface ApiServerConf {
115
129
  minClientVersion: string;
116
130
  tokenStore?: TokenStore;
117
131
  authStores?: ApiServerAuthStores;
132
+ onStartError?: (error: Error) => void;
118
133
  }
119
134
  export declare class ApiServer {
120
135
  app: Application;
121
- currReq: ApiRequest | null;
122
136
  readonly config: ApiServerConf;
123
137
  readonly startedAt: number;
124
138
  private readonly apiBasePath;
139
+ private readonly apiRouter;
140
+ private finalized;
125
141
  private storageAdapter;
126
142
  private moduleAdapter;
127
143
  private serverAuthAdapter;
128
144
  private apiNotFoundHandler;
145
+ private apiErrorHandlerInstalled;
129
146
  private tokenStoreAdapter;
130
147
  private userStoreAdapter;
131
148
  private passkeyServiceAdapter;
132
149
  private oauthStoreAdapter;
133
150
  private canImpersonateAdapter;
134
151
  private readonly jwtHelper;
152
+ private currReqDeprecationWarned;
153
+ /**
154
+ * @deprecated ApiServer does not track a global "current request". This value is always null.
155
+ * Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
156
+ * when mounting raw Express endpoints.
157
+ */
158
+ get currReq(): ApiRequest | null;
159
+ set currReq(_value: ApiRequest | null);
135
160
  constructor(config?: Partial<ApiServerConf>);
161
+ private assertNotFinalized;
162
+ private toApiRouterPath;
163
+ finalize(): this;
136
164
  authStorage<UserRow, SafeUser>(storage: AuthAdapter<UserRow, SafeUser>): this;
137
165
  /**
138
166
  * @deprecated Use {@link ApiServer.authStorage} instead.
@@ -200,9 +228,10 @@ export declare class ApiServer {
200
228
  private installSwaggerHandler;
201
229
  private normalizeApiBasePath;
202
230
  private installApiNotFoundHandler;
203
- private ensureApiNotFoundOrdering;
231
+ private installApiErrorHandler;
204
232
  private describeMissingEndpoint;
205
233
  start(): this;
234
+ private internalServerErrorMessage;
206
235
  private verifyJWT;
207
236
  private jwtCookieOptions;
208
237
  private setAccessCookie;
@@ -10,10 +10,16 @@ interface CanImpersonateContext<UserEntity> {
10
10
  targetUser: UserEntity;
11
11
  effectiveUserId: AuthIdentifier;
12
12
  }
13
+ type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token';
13
14
  interface AuthModuleOptions<UserEntity> {
14
15
  namespace?: string;
15
16
  defaultDomain?: string;
16
17
  canImpersonate?: (context: CanImpersonateContext<UserEntity>) => Promise<boolean> | boolean;
18
+ rateLimit?: (context: {
19
+ apiReq: ApiRequest;
20
+ endpoint: AuthRateLimitEndpoint;
21
+ }) => Promise<void> | void;
22
+ allowInsecurePkcePlain?: boolean;
17
23
  }
18
24
  type TokenMetadata = Partial<Token> & {
19
25
  sessionCookie?: boolean;
@@ -42,9 +48,12 @@ type AuthCapableServer<PublicUser> = ApiServer & {
42
48
  };
43
49
  export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<UserEntity> implements AuthProviderModule<UserEntity> {
44
50
  static defaultNamespace: string;
45
- server: AuthCapableServer<PublicUser>;
51
+ get server(): AuthCapableServer<PublicUser>;
52
+ set server(value: AuthCapableServer<PublicUser>);
46
53
  private readonly defaultDomain?;
47
54
  private readonly canImpersonateHook?;
55
+ private readonly rateLimitHook?;
56
+ private readonly allowInsecurePkcePlain;
48
57
  constructor(options?: AuthModuleOptions<UserEntity>);
49
58
  protected get storage(): AuthAdapter<UserEntity, PublicUser>;
50
59
  protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
@@ -100,6 +109,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
100
109
  private hasOAuthStore;
101
110
  private storageImplements;
102
111
  private storageImplementsAll;
112
+ private applyRateLimit;
113
+ private resolvePkceChallengeMethod;
103
114
  defineRoutes(): ApiRoute[];
104
115
  }
105
116
  export {};
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_crypto_1 = require("node:crypto");
4
4
  const helpers_1 = require("@simplewebauthn/server/helpers");
5
5
  const api_server_base_js_1 = require("../api-server-base.js");
6
+ const auth_cookie_options_js_1 = require("../auth-cookie-options.js");
6
7
  const module_js_1 = require("./module.js");
7
8
  const storage_js_1 = require("./storage.js");
8
9
  function isAuthIdentifier(value) {
@@ -32,10 +33,18 @@ function sha256Base64Url(value) {
32
33
  return base64UrlEncode(hash);
33
34
  }
34
35
  class AuthModule extends module_js_1.BaseAuthModule {
36
+ get server() {
37
+ return super.server;
38
+ }
39
+ set server(value) {
40
+ super.server = value;
41
+ }
35
42
  constructor(options = {}) {
36
43
  super({ namespace: options.namespace ?? AuthModule.defaultNamespace });
37
44
  this.defaultDomain = options.defaultDomain;
38
45
  this.canImpersonateHook = options.canImpersonate;
46
+ this.rateLimitHook = options.rateLimit;
47
+ this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
39
48
  }
40
49
  get storage() {
41
50
  return this.server.getAuthStorage();
@@ -236,29 +245,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
236
245
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
237
246
  }
238
247
  cookieOptions(apiReq) {
239
- const conf = this.server.config;
240
- const forwarded = apiReq.req.headers['x-forwarded-proto'];
241
- const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
242
- const origin = typeof referer === 'string' ? referer : '';
243
- const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
244
- const isLocalhost = origin.includes('localhost');
245
- const options = {
246
- httpOnly: true,
247
- secure: true,
248
- sameSite: 'strict',
249
- domain: conf.cookieDomain || undefined,
250
- path: '/',
251
- maxAge: undefined
252
- };
253
- if (conf.devMode) {
254
- options.secure = isHttps;
255
- options.httpOnly = false;
256
- options.sameSite = 'lax';
257
- if (isLocalhost) {
258
- options.domain = undefined;
259
- }
260
- }
261
- return options;
248
+ return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.server.config, apiReq.req);
262
249
  }
263
250
  setJwtCookies(apiReq, tokens, preferences = {}) {
264
251
  const conf = this.server.config;
@@ -285,7 +272,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
285
272
  async issueTokens(apiReq, user, metadata = {}) {
286
273
  const conf = this.server.config;
287
274
  const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
288
- const payload = this.buildTokenPayload(user, enrichedMetadata);
275
+ const payload = {
276
+ ...this.buildTokenPayload(user, enrichedMetadata),
277
+ jti: (0, node_crypto_1.randomUUID)()
278
+ };
289
279
  const access = this.server.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
290
280
  if (!access.success || !access.token) {
291
281
  throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
@@ -455,6 +445,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
455
445
  return undefined;
456
446
  }
457
447
  async postLogin(apiReq) {
448
+ await this.applyRateLimit(apiReq, 'login');
458
449
  this.assertAuthReady();
459
450
  const { login, password, ...metadata } = this.parseLoginBody(apiReq);
460
451
  const user = await this.storage.getUser(login);
@@ -552,10 +543,39 @@ class AuthModule extends module_js_1.BaseAuthModule {
552
543
  apiReq.req.cookies[conf.accessCookie].trim().length > 0);
553
544
  const shouldRefresh = Boolean(body.refresh) || !hasAccessToken;
554
545
  if (shouldRefresh) {
555
- const access = this.server.jwtSign(this.buildTokenPayload(user, stored), conf.accessSecret, conf.accessExpiry);
546
+ const updateToken = this.storage.updateToken;
547
+ if (typeof updateToken !== 'function' || !this.storageImplements('updateToken')) {
548
+ throw new api_server_base_js_1.ApiError({ code: 501, message: 'Token update storage is not configured' });
549
+ }
550
+ // Sign a new access token without embedding stored token secrets into the JWT payload.
551
+ const metadata = {
552
+ ruid: stored.ruid,
553
+ domain: stored.domain,
554
+ fingerprint: stored.fingerprint,
555
+ label: stored.label,
556
+ clientId: stored.clientId,
557
+ scope: stored.scope,
558
+ browser: stored.browser,
559
+ device: stored.device,
560
+ ip: stored.ip,
561
+ os: stored.os,
562
+ loginType: stored.loginType,
563
+ refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds),
564
+ sessionCookie: stored.sessionCookie
565
+ };
566
+ const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
567
+ const access = this.server.jwtSign(this.buildTokenPayload(user, enrichedMetadata), conf.accessSecret, conf.accessExpiry);
556
568
  if (!access.success || !access.token) {
557
569
  throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
558
570
  }
571
+ const updated = await updateToken.call(this.storage, {
572
+ refreshToken,
573
+ accessToken: access.token,
574
+ lastSeenAt: new Date()
575
+ });
576
+ if (!updated) {
577
+ throw new api_server_base_js_1.ApiError({ code: 500, message: 'Unable to persist refreshed access token' });
578
+ }
559
579
  const cookiePrefs = this.mergeSessionPreferences({
560
580
  sessionCookie: stored.sessionCookie,
561
581
  refreshTtlSeconds: this.normalizeRefreshTtlSeconds(stored.refreshTtlSeconds)
@@ -589,6 +609,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
589
609
  ];
590
610
  }
591
611
  async postPasskeyChallenge(apiReq) {
612
+ await this.applyRateLimit(apiReq, 'passkey-challenge');
592
613
  if (typeof this.storage.createPasskeyChallenge !== 'function') {
593
614
  throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey support is not configured' });
594
615
  }
@@ -726,7 +747,8 @@ class AuthModule extends module_js_1.BaseAuthModule {
726
747
  async deleteImpersonation(apiReq) {
727
748
  this.assertAuthReady();
728
749
  const actor = await this.resolveActorContext(apiReq);
729
- const metadata = this.buildImpersonationMetadata((apiReq.req.body ?? {}));
750
+ const query = (apiReq.req.query ?? {});
751
+ const metadata = this.buildImpersonationMetadata(query);
730
752
  metadata.loginType = metadata.loginType ?? 'impersonation-end';
731
753
  const tokens = await this.issueTokens(apiReq, actor.user, metadata);
732
754
  const publicUser = this.storage.filterUser(actor.user);
@@ -798,6 +820,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
798
820
  const state = toStringOrNull(body.state) ?? undefined;
799
821
  const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
800
822
  const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
823
+ const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
801
824
  if (!clientId) {
802
825
  throw new api_server_base_js_1.ApiError({ code: 400, message: 'clientId is required' });
803
826
  }
@@ -817,7 +840,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
817
840
  redirectUri,
818
841
  scope: resolvedScope,
819
842
  codeChallenge,
820
- codeChallengeMethod: codeChallengeMethod === 'S256' ? 'S256' : codeChallengeMethod === 'plain' ? 'plain' : undefined,
843
+ codeChallengeMethod: resolvedCodeChallengeMethod,
821
844
  expiresInSeconds: 300
822
845
  });
823
846
  const redirect = new URL(redirectUri);
@@ -828,6 +851,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
828
851
  return [200, { code: codeRecord.code, redirectUri: redirect.toString(), state }];
829
852
  }
830
853
  async postOAuthToken(apiReq) {
854
+ await this.applyRateLimit(apiReq, 'oauth-token');
831
855
  if (typeof this.storage.getClient !== 'function' || typeof this.storage.consumeAuthCode !== 'function') {
832
856
  throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth token storage is not configured' });
833
857
  }
@@ -881,6 +905,9 @@ class AuthModule extends module_js_1.BaseAuthModule {
881
905
  }
882
906
  }
883
907
  else if (record.codeChallengeMethod === 'plain') {
908
+ if (!this.allowInsecurePkcePlain) {
909
+ throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
910
+ }
884
911
  if (codeVerifier !== record.codeChallenge) {
885
912
  throw new api_server_base_js_1.ApiError({ code: 400, message: 'code_verifier does not match challenge' });
886
913
  }
@@ -1001,14 +1028,11 @@ class AuthModule extends module_js_1.BaseAuthModule {
1001
1028
  if (!secretProvided) {
1002
1029
  throw new api_server_base_js_1.ApiError({ code: 400, message: 'Client authentication is required' });
1003
1030
  }
1004
- let valid = false;
1005
- if (this.storage.verifyClientSecret) {
1006
- const verifySecret = this.storage.verifyClientSecret.bind(this.storage);
1007
- valid = await verifySecret(client, clientSecret);
1008
- }
1009
- else {
1010
- valid = client.clientSecret === clientSecret;
1031
+ const verifySecret = this.storage.verifyClientSecret;
1032
+ if (typeof verifySecret !== 'function' || !this.storageImplements('verifyClientSecret')) {
1033
+ throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth client secret verification is not configured' });
1011
1034
  }
1035
+ const valid = await verifySecret.call(this.storage, client, clientSecret);
1012
1036
  if (!valid) {
1013
1037
  throw new api_server_base_js_1.ApiError({ code: 401, message: 'Invalid client credentials' });
1014
1038
  }
@@ -1084,6 +1108,24 @@ class AuthModule extends module_js_1.BaseAuthModule {
1084
1108
  storageImplementsAll(keys) {
1085
1109
  return keys.every((key) => this.storageImplements(key));
1086
1110
  }
1111
+ async applyRateLimit(apiReq, endpoint) {
1112
+ if (!this.rateLimitHook) {
1113
+ return;
1114
+ }
1115
+ await this.rateLimitHook({ apiReq, endpoint });
1116
+ }
1117
+ resolvePkceChallengeMethod(value) {
1118
+ if (value === 'S256') {
1119
+ return 'S256';
1120
+ }
1121
+ if (value === 'plain') {
1122
+ if (!this.allowInsecurePkcePlain) {
1123
+ throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
1124
+ }
1125
+ return 'plain';
1126
+ }
1127
+ return undefined;
1128
+ }
1087
1129
  defineRoutes() {
1088
1130
  const routes = [];
1089
1131
  const coreAuthSupported = this.storageImplementsAll([
@@ -1156,7 +1198,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
1156
1198
  auth: { type: 'strict', req: 'any' }
1157
1199
  }, {
1158
1200
  method: 'delete',
1159
- path: '/v1/passkeys/:credentialId?',
1201
+ path: '/v1/passkeys/:credentialId',
1160
1202
  handler: (req) => this.deletePasskey(req),
1161
1203
  auth: { type: 'strict', req: 'any' }
1162
1204
  });
@@ -2,32 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemAuthStore = void 0;
4
4
  const memory_js_1 = require("../oauth/memory.js");
5
+ const config_js_1 = require("../passkey/config.js");
5
6
  const memory_js_2 = require("../passkey/memory.js");
6
7
  const memory_js_3 = require("../token/memory.js");
7
8
  const memory_js_4 = require("../user/memory.js");
8
9
  const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
9
- const DEFAULT_PASSKEY_CONFIG = {
10
- rpId: 'localhost',
11
- rpName: 'API Server',
12
- origins: ['http://localhost:5173'],
13
- timeoutMs: 5 * 60 * 1000,
14
- userVerification: 'preferred'
15
- };
16
- function isOriginString(origin) {
17
- return typeof origin === 'string' && origin.trim().length > 0;
18
- }
19
- function normalizePasskeyConfig(config = {}) {
20
- const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
21
- return {
22
- rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
23
- rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
24
- origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
25
- timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
26
- ? config.timeoutMs
27
- : DEFAULT_PASSKEY_CONFIG.timeoutMs,
28
- userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification
29
- };
30
- }
31
10
  class MemAuthStore {
32
11
  constructor(params = {}) {
33
12
  this.userStore = new memory_js_4.MemoryUserStore({
@@ -42,7 +21,7 @@ class MemAuthStore {
42
21
  let passkeyStore;
43
22
  let passkeyConfig;
44
23
  if (params.passkeys !== false) {
45
- passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
24
+ passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
46
25
  const resolveUser = async (lookup) => {
47
26
  const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
48
27
  if (!found) {
@@ -2,48 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SqlAuthStore = void 0;
4
4
  const sequelize_js_1 = require("../oauth/sequelize.js");
5
+ const config_js_1 = require("../passkey/config.js");
5
6
  const sequelize_js_2 = require("../passkey/sequelize.js");
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
6
8
  const sequelize_js_3 = require("../token/sequelize.js");
7
9
  const sequelize_js_4 = require("../user/sequelize.js");
8
10
  const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
9
- const DEFAULT_PASSKEY_CONFIG = {
10
- rpId: 'localhost',
11
- rpName: 'API Server',
12
- origins: ['http://localhost:5173'],
13
- timeoutMs: 5 * 60 * 1000,
14
- userVerification: 'preferred'
15
- };
16
- function normalizeTablePrefix(prefix) {
17
- if (!prefix) {
18
- return undefined;
19
- }
20
- const trimmed = prefix.trim();
21
- return trimmed.length > 0 ? trimmed : undefined;
22
- }
23
11
  function resolveTablePrefix(...prefixes) {
24
12
  for (const prefix of prefixes) {
25
- const normalized = normalizeTablePrefix(prefix);
13
+ const normalized = (0, sequelize_utils_js_1.normalizeTablePrefix)(prefix);
26
14
  if (normalized) {
27
15
  return normalized;
28
16
  }
29
17
  }
30
18
  return undefined;
31
19
  }
32
- function isOriginString(origin) {
33
- return typeof origin === 'string' && origin.trim().length > 0;
34
- }
35
- function normalizePasskeyConfig(config = {}) {
36
- const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
37
- return {
38
- rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
39
- rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
40
- origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
41
- timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
42
- ? config.timeoutMs
43
- : DEFAULT_PASSKEY_CONFIG.timeoutMs,
44
- userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification
45
- };
46
- }
47
20
  class SqlAuthStore {
48
21
  constructor(params) {
49
22
  this.closed = false;
@@ -83,7 +56,7 @@ class SqlAuthStore {
83
56
  let passkeyConfig;
84
57
  if (params.passkeys !== false) {
85
58
  const passkeyTablePrefix = resolveTablePrefix(moduleTablePrefixes.passkey, params.tablePrefix);
86
- passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
59
+ passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
87
60
  const resolveUser = async (lookup) => {
88
61
  const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
89
62
  if (!found) {
@@ -0,0 +1,4 @@
1
+ import type { AuthIdentifier } from './types.js';
2
+ export declare function normalizeComparableUserId(identifier: AuthIdentifier): string;
3
+ export declare function normalizeNumericUserId(identifier: AuthIdentifier): number;
4
+ export declare function normalizeStringUserId(identifier: AuthIdentifier): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeComparableUserId = normalizeComparableUserId;
4
+ exports.normalizeNumericUserId = normalizeNumericUserId;
5
+ exports.normalizeStringUserId = normalizeStringUserId;
6
+ function normalizeComparableUserId(identifier) {
7
+ if (typeof identifier === 'number' && Number.isFinite(identifier)) {
8
+ return String(identifier);
9
+ }
10
+ if (typeof identifier === 'string') {
11
+ const trimmed = identifier.trim();
12
+ if (trimmed.length === 0) {
13
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
14
+ }
15
+ if (/^\d+$/.test(trimmed)) {
16
+ return String(Number(trimmed));
17
+ }
18
+ return trimmed;
19
+ }
20
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
21
+ }
22
+ function normalizeNumericUserId(identifier) {
23
+ const normalized = normalizeComparableUserId(identifier);
24
+ if (/^\d+$/.test(normalized)) {
25
+ return Number(normalized);
26
+ }
27
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
28
+ }
29
+ function normalizeStringUserId(identifier) {
30
+ return normalizeComparableUserId(identifier);
31
+ }
@@ -0,0 +1,11 @@
1
+ import type { Request } from 'express';
2
+ import type { CookieOptions } from 'express-serve-static-core';
3
+ export interface AuthCookieConfig {
4
+ cookieSecure?: boolean | 'auto';
5
+ cookieSameSite?: 'lax' | 'strict' | 'none';
6
+ cookieHttpOnly?: boolean;
7
+ cookieDomain?: string;
8
+ cookiePath?: string;
9
+ devMode?: boolean;
10
+ }
11
+ export declare function buildAuthCookieOptions(config: AuthCookieConfig, req: Pick<Request, 'headers' | 'protocol'>): CookieOptions;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAuthCookieOptions = buildAuthCookieOptions;
4
+ function firstHeaderValue(value) {
5
+ if (typeof value === 'string') {
6
+ return value;
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value[0] ?? '';
10
+ }
11
+ return '';
12
+ }
13
+ function resolveOriginHostname(origin) {
14
+ try {
15
+ const url = new URL(origin);
16
+ const hostname = url.hostname.trim().toLowerCase();
17
+ return hostname.length > 0 ? hostname : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function isLocalhostOrigin(origin) {
24
+ const hostname = resolveOriginHostname(origin);
25
+ if (!hostname) {
26
+ return false;
27
+ }
28
+ return hostname === 'localhost' || hostname.endsWith('.localhost');
29
+ }
30
+ function buildAuthCookieOptions(config, req) {
31
+ const forwardedProto = firstHeaderValue(req.headers['x-forwarded-proto']).split(',')[0].trim().toLowerCase();
32
+ const isHttps = forwardedProto === 'https' || req.protocol === 'https';
33
+ const origin = firstHeaderValue(req.headers.origin ?? req.headers.referer);
34
+ const secure = config.cookieSecure === true ? true : config.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
35
+ let sameSite = config.cookieSameSite ?? 'lax';
36
+ if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
37
+ sameSite = 'lax';
38
+ }
39
+ let resolvedSecure = secure;
40
+ if (sameSite === 'none' && resolvedSecure !== true) {
41
+ // Modern browsers reject SameSite=None cookies unless Secure is set.
42
+ resolvedSecure = true;
43
+ }
44
+ const options = {
45
+ httpOnly: config.cookieHttpOnly ?? true,
46
+ secure: resolvedSecure,
47
+ sameSite,
48
+ domain: config.cookieDomain || undefined,
49
+ path: config.cookiePath || '/',
50
+ maxAge: undefined
51
+ };
52
+ if (config.devMode && isLocalhostOrigin(origin)) {
53
+ // Domain cookies do not work on localhost; avoid breaking local development when cookieDomain is set.
54
+ options.domain = undefined;
55
+ }
56
+ return options;
57
+ }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MemoryOAuthStore = void 0;
7
7
  const bcryptjs_1 = __importDefault(require("bcryptjs"));
8
+ const user_id_js_1 = require("../auth-api/user-id.js");
8
9
  const base_js_1 = require("./base.js");
9
10
  function cloneClient(client) {
10
11
  if (!client) {
@@ -12,7 +13,8 @@ function cloneClient(client) {
12
13
  }
13
14
  return {
14
15
  clientId: client.clientId,
15
- clientSecret: client.clientSecret,
16
+ // clientSecret is stored hashed; do not return the hash.
17
+ clientSecret: client.clientSecret ? '__stored__' : undefined,
16
18
  name: client.name,
17
19
  redirectUris: [...client.redirectUris],
18
20
  scope: client.scope ? [...client.scope] : undefined,
@@ -28,15 +30,7 @@ function cloneCode(code) {
28
30
  metadata: code.metadata ? { ...code.metadata } : undefined
29
31
  };
30
32
  }
31
- function normalizeUserId(identifier) {
32
- if (typeof identifier === 'number' && Number.isFinite(identifier)) {
33
- return identifier;
34
- }
35
- if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
36
- return Number(identifier);
37
- }
38
- throw new Error(`Unable to normalise user identifier: ${identifier}`);
39
- }
33
+ const normalizeUserId = user_id_js_1.normalizeNumericUserId;
40
34
  class MemoryOAuthStore extends base_js_1.OAuthStore {
41
35
  constructor(options = {}) {
42
36
  super();
@@ -4,27 +4,16 @@ exports.OAuthCodeModel = exports.OAuthClientModel = void 0;
4
4
  exports.initOAuthClientModel = initOAuthClientModel;
5
5
  exports.initOAuthCodeModel = initOAuthCodeModel;
6
6
  const sequelize_1 = require("sequelize");
7
- const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
8
- function normalizeTablePrefix(prefix) {
9
- if (!prefix) {
10
- return undefined;
11
- }
12
- const trimmed = prefix.trim();
13
- return trimmed.length > 0 ? trimmed : undefined;
14
- }
15
- function applyTablePrefix(prefix, tableName) {
16
- const normalized = normalizeTablePrefix(prefix);
17
- return normalized ? `${normalized}${tableName}` : tableName;
18
- }
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.js");
19
8
  function integerIdType(sequelize) {
20
- return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
9
+ return sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
21
10
  }
22
11
  function tableOptions(sequelize, tableName, tablePrefix, extra) {
23
- const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
12
+ const opts = { sequelize, tableName: (0, sequelize_utils_js_1.applyTablePrefix)(tablePrefix, tableName) };
24
13
  if (extra) {
25
14
  Object.assign(opts, extra);
26
15
  }
27
- if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
16
+ if (sequelize_utils_js_1.DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
28
17
  opts.charset = 'utf8mb4';
29
18
  opts.collate = 'utf8mb4_unicode_ci';
30
19
  }