@technomoron/api-server-base 2.0.0-beta.21 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.txt CHANGED
@@ -111,6 +111,9 @@ cookiePath (string, default '/') Path applied to auth cookies.
111
111
  cookieSameSite ('lax' | 'strict' | 'none', default 'lax') SameSite attribute applied to auth cookies.
112
112
  cookieSecure (boolean | 'auto', default 'auto') Secure attribute applied to auth cookies; 'auto' enables Secure only when the request is HTTPS (or forwarded as HTTPS).
113
113
  cookieHttpOnly (boolean, default true) HttpOnly attribute applied to auth cookies.
114
+ apiKeyPrefix (string, default '') Optional prefix required before API key secrets in Authorization Bearer tokens.
115
+ tokenParam (string, default '') Optional request field name used for token fallback when Bearer is missing.
116
+ tokenParamLocation ('body' | 'query' | 'body-query', default 'body') Controls where tokenParam is read from.
114
117
  accessCookie (string, default 'dat') Access token cookie name.
115
118
  refreshCookie (string, default 'drt') Refresh token cookie name.
116
119
  accessExpiry (number, default 60 * 15) Access token lifetime in seconds.
@@ -129,7 +132,7 @@ Tip: If you add new configuration fields in downstream projects, extend ApiServe
129
132
  -----------------
130
133
  1. Express middlewares (express.json, cookie-parser, optional multer) run before your handler.
131
134
  2. ApiServer wraps the route inside handle_request, creating an ApiRequest and logging when debug is enabled.
132
- 3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. Bearer JWTs and the access cookie (`accessCookie`, default `dat`) are accepted for `yes`/`strict`, while API key tokens prefixed with `apikey-` always delegate to `getApiKey`. When `refreshSecret` is configured and your storage supports refresh lookups (`getToken({ refreshToken })` + `updateToken(...)`), `yes`/`strict` routes will automatically mint a new access token when it is missing or expired (and also recover from "Authorization token is no longer valid" by refreshing). `maybe` routes only do the same when `refreshMaybe: true`. The optional `strict` type (or server-wide `validateTokens` flag) requires the signed JWT to exist in storage; when it does, the persisted row is attached to `apiReq.authToken`. The dedicated `apikey` type simply means “an API key is required”; otherwise API keys are still accepted by `yes`/`strict` routes alongside JWTs, and `apiReq.apiKey` is populated when present.
135
+ 3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. It always prioritizes `Authorization: Bearer ...`. If Bearer is missing and `tokenParam` is configured, token fallback is read from the configured request location (`body`, `query`, or `body-query`) before checking the access cookie (`accessCookie`, default `dat`). API key tokens are matched with the optional `apiKeyPrefix` config (default `''`, meaning raw key values). When `refreshSecret` is configured and your storage supports refresh lookups (`getToken({ refreshToken })` + `updateToken(...)`), `yes`/`strict` routes will automatically mint a new access token when it is missing or expired (and also recover from "Authorization token is no longer valid" by refreshing). `maybe` routes only do the same when `refreshMaybe: true`. The optional `strict` type (or server-wide `validateTokens` flag) requires the signed JWT to exist in storage; when it does, the persisted row is attached to `apiReq.authToken`. The dedicated `apikey` type simply means “an API key is required”; otherwise API keys are still accepted by `yes`/`strict` routes alongside JWTs, and `apiReq.apiKey` is populated when present.
133
136
  4. authorize runs with the requested auth class (any or admin in the base implementation). Override to connect to your role system.
134
137
  5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
135
138
  6. Errors bubble into the wrapper. ApiError instances respect the provided status codes; other exceptions result in a 500 with text derived from guessExceptionText.
@@ -4,7 +4,10 @@ export type ApiHandler<Data = unknown> = (apiReq: ApiRequest) => Promise<ApiHand
4
4
  export type ApiAuthType = 'none' | 'maybe' | 'yes' | 'strict' | 'apikey';
5
5
  export type ApiAuthClass = 'any' | 'admin';
6
6
  export interface ApiKey {
7
+ /** Real user identity — who the key belongs to. */
7
8
  uid: unknown;
9
+ /** Effective user identity — who the key acts as. When set, the request is treated as impersonation (isImpersonating() === true). */
10
+ euid?: unknown;
8
11
  }
9
12
  export type ApiRoute = {
10
13
  method: 'get' | 'post' | 'put' | 'patch' | 'delete';
@@ -372,6 +372,10 @@ function fillConfig(config) {
372
372
  cookieSameSite: config.cookieSameSite ?? 'lax',
373
373
  cookieSecure: config.cookieSecure ?? 'auto',
374
374
  cookieHttpOnly: config.cookieHttpOnly ?? true,
375
+ apiKeyPrefix: config.apiKeyPrefix ?? '',
376
+ apiKeyEnabled: config.apiKeyEnabled ?? false,
377
+ tokenParam: config.tokenParam ?? '',
378
+ tokenParamLocation: config.tokenParamLocation ?? 'body',
375
379
  accessCookie: config.accessCookie ?? 'dat',
376
380
  refreshCookie: config.refreshCookie ?? 'drt',
377
381
  accessExpiry: config.accessExpiry ?? 60 * 15,
@@ -1042,19 +1046,28 @@ class ApiServer {
1042
1046
  }
1043
1047
  let token = null;
1044
1048
  const authHeader = apiReq.req.headers.authorization;
1049
+ const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() || null : null;
1050
+ const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
1045
1051
  const requiresAuthToken = this.requiresAuthToken(authType);
1046
1052
  const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
1047
- const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
1048
- if (apiKeyAuth) {
1049
- return apiKeyAuth;
1053
+ if (this.config.apiKeyEnabled || authType === 'apikey') {
1054
+ const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, bearerToken ?? paramToken);
1055
+ if (apiKeyAuth) {
1056
+ return apiKeyAuth;
1057
+ }
1058
+ }
1059
+ token = bearerToken ?? paramToken;
1060
+ if (bearerToken) {
1061
+ apiReq.authMethod = 'bearer';
1050
1062
  }
1051
- if (authHeader?.startsWith('Bearer ')) {
1052
- token = authHeader.slice(7).trim();
1063
+ else if (paramToken) {
1064
+ apiReq.authMethod = 'param';
1053
1065
  }
1054
1066
  if (!token) {
1055
1067
  const access = apiReq.req.cookies?.[this.config.accessCookie];
1056
1068
  if (access) {
1057
1069
  token = access;
1070
+ apiReq.authMethod = 'cookie';
1058
1071
  }
1059
1072
  }
1060
1073
  let tokenData;
@@ -1134,40 +1147,83 @@ class ApiServer {
1134
1147
  apiReq.token = token;
1135
1148
  return tokenData;
1136
1149
  }
1137
- async tryAuthenticateApiKey(apiReq, authType, authHeader) {
1138
- if (!authHeader?.startsWith('Bearer ')) {
1150
+ async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
1151
+ if (!tokenCandidate) {
1139
1152
  if (authType === 'apikey') {
1140
1153
  throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
1141
1154
  }
1142
1155
  return null;
1143
1156
  }
1144
- const keyToken = authHeader.slice(7).trim();
1145
- if (!keyToken.startsWith('apikey-')) {
1157
+ const keyToken = tokenCandidate;
1158
+ const prefix = this.config.apiKeyPrefix;
1159
+ const secret = prefix === '' ? keyToken : keyToken.startsWith(prefix) ? keyToken.slice(prefix.length) : null;
1160
+ if (!secret) {
1146
1161
  if (authType === 'apikey') {
1147
1162
  throw new ApiError({ code: 401, message: 'Invalid API Key' });
1148
1163
  }
1149
1164
  return null;
1150
1165
  }
1151
- const secret = keyToken.replace(/^apikey-/, '');
1152
1166
  const key = await this.getApiKey(secret);
1153
1167
  if (!key) {
1154
- throw new ApiError({ code: 401, message: 'Invalid API Key' });
1168
+ if (authType === 'apikey') {
1169
+ throw new ApiError({ code: 401, message: 'Invalid API Key' });
1170
+ }
1171
+ return null;
1155
1172
  }
1156
1173
  apiReq.token = secret;
1157
1174
  apiReq.apiKey = key;
1158
- // Treat API keys as authenticated identities, consistent with JWT-based flows.
1159
- const resolvedUid = this.normalizeAuthIdentifier(key.uid);
1160
- if (resolvedUid !== null) {
1161
- apiReq.realUid = resolvedUid;
1175
+ apiReq.authMethod = 'apikey';
1176
+ // uid is the real identity; euid (if set) is the effective/impersonated identity.
1177
+ const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
1178
+ const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
1179
+ const effectiveUid = resolvedEuid ?? resolvedRuid;
1180
+ if (resolvedRuid !== null) {
1181
+ apiReq.realUid = resolvedRuid;
1162
1182
  }
1163
1183
  return {
1164
- uid: key.uid,
1184
+ uid: effectiveUid,
1185
+ ...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
1165
1186
  domain: '',
1166
1187
  fingerprint: '',
1167
1188
  iat: 0,
1168
1189
  exp: 0
1169
1190
  };
1170
1191
  }
1192
+ resolveTokenFromRequest(req) {
1193
+ const paramName = this.config.tokenParam.trim();
1194
+ if (!paramName) {
1195
+ return null;
1196
+ }
1197
+ const location = this.config.tokenParamLocation;
1198
+ if (location === 'body') {
1199
+ return this.readNamedValue(req.body, paramName);
1200
+ }
1201
+ if (location === 'query') {
1202
+ return this.readNamedValue(req.query, paramName);
1203
+ }
1204
+ return this.readNamedValue(req.body, paramName) ?? this.readNamedValue(req.query, paramName);
1205
+ }
1206
+ readNamedValue(container, key) {
1207
+ if (!container || typeof container !== 'object' || Array.isArray(container)) {
1208
+ return null;
1209
+ }
1210
+ const raw = container[key];
1211
+ if (typeof raw === 'string') {
1212
+ const token = raw.trim();
1213
+ return token.length > 0 ? token : null;
1214
+ }
1215
+ if (Array.isArray(raw)) {
1216
+ for (const entry of raw) {
1217
+ if (typeof entry === 'string') {
1218
+ const token = entry.trim();
1219
+ if (token.length > 0) {
1220
+ return token;
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ return null;
1226
+ }
1171
1227
  requiresAuthToken(authType) {
1172
1228
  return authType === 'yes' || authType === 'strict';
1173
1229
  }
@@ -1238,6 +1294,7 @@ class ApiServer {
1238
1294
  res,
1239
1295
  token: '',
1240
1296
  tokenData: null,
1297
+ authMethod: null,
1241
1298
  realUid: null,
1242
1299
  getClientInfo: () => ensureClientInfo(apiReq),
1243
1300
  getClientIp: () => ensureClientInfo(apiReq).ip,
@@ -1431,12 +1488,25 @@ class ApiServer {
1431
1488
  }
1432
1489
  dumpRequest(apiReq) {
1433
1490
  const req = apiReq.req;
1491
+ const tokenParam = this.config.tokenParam.trim();
1434
1492
  console.log('--- Incoming Request! ---');
1435
1493
  const url = req.originalUrl || req.url;
1436
1494
  console.log('URL:', url);
1437
1495
  console.log('Method:', req.method);
1438
- console.log('Query Params:', req.query || {});
1496
+ if (tokenParam &&
1497
+ req.query &&
1498
+ typeof req.query === 'object' &&
1499
+ tokenParam in req.query) {
1500
+ const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
1501
+ console.log('Query Params:', maskedQuery);
1502
+ }
1503
+ else {
1504
+ console.log('Query Params:', req.query || {});
1505
+ }
1439
1506
  const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
1507
+ if (tokenParam) {
1508
+ sensitiveBodyKeys.push(tokenParam);
1509
+ }
1440
1510
  const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
1441
1511
  if (body && typeof body === 'object') {
1442
1512
  for (const key of sensitiveBodyKeys) {
@@ -1507,7 +1577,21 @@ class ApiServer {
1507
1577
  return value;
1508
1578
  }
1509
1579
  dumpResponse(apiReq, payload, status) {
1510
- const url = apiReq.req.originalUrl || apiReq.req.url;
1580
+ const rawUrl = apiReq.req.originalUrl || apiReq.req.url;
1581
+ const tokenParam = this.config.tokenParam.trim();
1582
+ let url = rawUrl;
1583
+ if (tokenParam && rawUrl) {
1584
+ try {
1585
+ const parsed = new URL(rawUrl, 'http://x');
1586
+ if (parsed.searchParams.has(tokenParam)) {
1587
+ parsed.searchParams.set(tokenParam, '[REDACTED]');
1588
+ url = parsed.pathname + '?' + parsed.searchParams.toString();
1589
+ }
1590
+ }
1591
+ catch {
1592
+ // Leave url unmodified if parsing fails
1593
+ }
1594
+ }
1511
1595
  console.log('--- Outgoing Response! ---');
1512
1596
  console.log('URL:', url);
1513
1597
  console.log('Status:', status);
@@ -31,6 +31,7 @@ export interface ApiTokenData extends JwtPayload, Partial<Token> {
31
31
  iat?: number;
32
32
  exp?: number;
33
33
  }
34
+ export type ApiAuthMethod = 'bearer' | 'cookie' | 'param' | 'apikey' | null;
34
35
  export interface ApiRequest {
35
36
  server: ApiServer;
36
37
  req: ExtendedReq;
@@ -39,6 +40,8 @@ export interface ApiRequest {
39
40
  token?: string;
40
41
  authToken?: Token | null;
41
42
  apiKey?: ApiKey | null;
43
+ /** How this request was authenticated. null when unauthenticated. */
44
+ authMethod?: ApiAuthMethod;
42
45
  clientInfo?: ClientInfo;
43
46
  realUid?: AuthIdentifier | null;
44
47
  getClientInfo: () => ClientInfo;
@@ -115,6 +118,18 @@ export interface ApiServerConf {
115
118
  cookieSecure?: boolean | 'auto';
116
119
  /** Cookie HttpOnly attribute for auth cookies. */
117
120
  cookieHttpOnly?: boolean;
121
+ /** Prefix used for API keys in Authorization Bearer tokens. */
122
+ apiKeyPrefix: string;
123
+ /**
124
+ * Enable API key authentication on non-apikey routes (e.g. 'yes', 'strict', 'maybe').
125
+ * When false (default), API key lookups only run on routes with authType 'apikey'.
126
+ * Set to true if you need API keys to authenticate on JWT routes as well.
127
+ */
128
+ apiKeyEnabled: boolean;
129
+ /** Optional request field name used to read auth tokens when Bearer is missing. */
130
+ tokenParam: string;
131
+ /** Where to read `tokenParam` from. */
132
+ tokenParamLocation: 'body' | 'query' | 'body-query';
118
133
  accessCookie: string;
119
134
  refreshCookie: string;
120
135
  accessExpiry: number;
@@ -238,6 +253,8 @@ export declare class ApiServer {
238
253
  private tryRefreshAccessToken;
239
254
  private authenticate;
240
255
  private tryAuthenticateApiKey;
256
+ private resolveTokenFromRequest;
257
+ private readNamedValue;
241
258
  private requiresAuthToken;
242
259
  private shouldValidateStoredToken;
243
260
  private assertStoredAccessToken;
@@ -4,7 +4,10 @@ export type ApiHandler<Data = unknown> = (apiReq: ApiRequest) => Promise<ApiHand
4
4
  export type ApiAuthType = 'none' | 'maybe' | 'yes' | 'strict' | 'apikey';
5
5
  export type ApiAuthClass = 'any' | 'admin';
6
6
  export interface ApiKey {
7
+ /** Real user identity — who the key belongs to. */
7
8
  uid: unknown;
9
+ /** Effective user identity — who the key acts as. When set, the request is treated as impersonation (isImpersonating() === true). */
10
+ euid?: unknown;
8
11
  }
9
12
  export type ApiRoute = {
10
13
  method: 'get' | 'post' | 'put' | 'patch' | 'delete';
@@ -31,6 +31,7 @@ export interface ApiTokenData extends JwtPayload, Partial<Token> {
31
31
  iat?: number;
32
32
  exp?: number;
33
33
  }
34
+ export type ApiAuthMethod = 'bearer' | 'cookie' | 'param' | 'apikey' | null;
34
35
  export interface ApiRequest {
35
36
  server: ApiServer;
36
37
  req: ExtendedReq;
@@ -39,6 +40,8 @@ export interface ApiRequest {
39
40
  token?: string;
40
41
  authToken?: Token | null;
41
42
  apiKey?: ApiKey | null;
43
+ /** How this request was authenticated. null when unauthenticated. */
44
+ authMethod?: ApiAuthMethod;
42
45
  clientInfo?: ClientInfo;
43
46
  realUid?: AuthIdentifier | null;
44
47
  getClientInfo: () => ClientInfo;
@@ -115,6 +118,18 @@ export interface ApiServerConf {
115
118
  cookieSecure?: boolean | 'auto';
116
119
  /** Cookie HttpOnly attribute for auth cookies. */
117
120
  cookieHttpOnly?: boolean;
121
+ /** Prefix used for API keys in Authorization Bearer tokens. */
122
+ apiKeyPrefix: string;
123
+ /**
124
+ * Enable API key authentication on non-apikey routes (e.g. 'yes', 'strict', 'maybe').
125
+ * When false (default), API key lookups only run on routes with authType 'apikey'.
126
+ * Set to true if you need API keys to authenticate on JWT routes as well.
127
+ */
128
+ apiKeyEnabled: boolean;
129
+ /** Optional request field name used to read auth tokens when Bearer is missing. */
130
+ tokenParam: string;
131
+ /** Where to read `tokenParam` from. */
132
+ tokenParamLocation: 'body' | 'query' | 'body-query';
118
133
  accessCookie: string;
119
134
  refreshCookie: string;
120
135
  accessExpiry: number;
@@ -238,6 +253,8 @@ export declare class ApiServer {
238
253
  private tryRefreshAccessToken;
239
254
  private authenticate;
240
255
  private tryAuthenticateApiKey;
256
+ private resolveTokenFromRequest;
257
+ private readNamedValue;
241
258
  private requiresAuthToken;
242
259
  private shouldValidateStoredToken;
243
260
  private assertStoredAccessToken;
@@ -364,6 +364,10 @@ function fillConfig(config) {
364
364
  cookieSameSite: config.cookieSameSite ?? 'lax',
365
365
  cookieSecure: config.cookieSecure ?? 'auto',
366
366
  cookieHttpOnly: config.cookieHttpOnly ?? true,
367
+ apiKeyPrefix: config.apiKeyPrefix ?? '',
368
+ apiKeyEnabled: config.apiKeyEnabled ?? false,
369
+ tokenParam: config.tokenParam ?? '',
370
+ tokenParamLocation: config.tokenParamLocation ?? 'body',
367
371
  accessCookie: config.accessCookie ?? 'dat',
368
372
  refreshCookie: config.refreshCookie ?? 'drt',
369
373
  accessExpiry: config.accessExpiry ?? 60 * 15,
@@ -1034,19 +1038,28 @@ export class ApiServer {
1034
1038
  }
1035
1039
  let token = null;
1036
1040
  const authHeader = apiReq.req.headers.authorization;
1041
+ const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() || null : null;
1042
+ const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
1037
1043
  const requiresAuthToken = this.requiresAuthToken(authType);
1038
1044
  const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
1039
- const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
1040
- if (apiKeyAuth) {
1041
- return apiKeyAuth;
1045
+ if (this.config.apiKeyEnabled || authType === 'apikey') {
1046
+ const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, bearerToken ?? paramToken);
1047
+ if (apiKeyAuth) {
1048
+ return apiKeyAuth;
1049
+ }
1050
+ }
1051
+ token = bearerToken ?? paramToken;
1052
+ if (bearerToken) {
1053
+ apiReq.authMethod = 'bearer';
1042
1054
  }
1043
- if (authHeader?.startsWith('Bearer ')) {
1044
- token = authHeader.slice(7).trim();
1055
+ else if (paramToken) {
1056
+ apiReq.authMethod = 'param';
1045
1057
  }
1046
1058
  if (!token) {
1047
1059
  const access = apiReq.req.cookies?.[this.config.accessCookie];
1048
1060
  if (access) {
1049
1061
  token = access;
1062
+ apiReq.authMethod = 'cookie';
1050
1063
  }
1051
1064
  }
1052
1065
  let tokenData;
@@ -1126,40 +1139,83 @@ export class ApiServer {
1126
1139
  apiReq.token = token;
1127
1140
  return tokenData;
1128
1141
  }
1129
- async tryAuthenticateApiKey(apiReq, authType, authHeader) {
1130
- if (!authHeader?.startsWith('Bearer ')) {
1142
+ async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
1143
+ if (!tokenCandidate) {
1131
1144
  if (authType === 'apikey') {
1132
1145
  throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
1133
1146
  }
1134
1147
  return null;
1135
1148
  }
1136
- const keyToken = authHeader.slice(7).trim();
1137
- if (!keyToken.startsWith('apikey-')) {
1149
+ const keyToken = tokenCandidate;
1150
+ const prefix = this.config.apiKeyPrefix;
1151
+ const secret = prefix === '' ? keyToken : keyToken.startsWith(prefix) ? keyToken.slice(prefix.length) : null;
1152
+ if (!secret) {
1138
1153
  if (authType === 'apikey') {
1139
1154
  throw new ApiError({ code: 401, message: 'Invalid API Key' });
1140
1155
  }
1141
1156
  return null;
1142
1157
  }
1143
- const secret = keyToken.replace(/^apikey-/, '');
1144
1158
  const key = await this.getApiKey(secret);
1145
1159
  if (!key) {
1146
- throw new ApiError({ code: 401, message: 'Invalid API Key' });
1160
+ if (authType === 'apikey') {
1161
+ throw new ApiError({ code: 401, message: 'Invalid API Key' });
1162
+ }
1163
+ return null;
1147
1164
  }
1148
1165
  apiReq.token = secret;
1149
1166
  apiReq.apiKey = key;
1150
- // Treat API keys as authenticated identities, consistent with JWT-based flows.
1151
- const resolvedUid = this.normalizeAuthIdentifier(key.uid);
1152
- if (resolvedUid !== null) {
1153
- apiReq.realUid = resolvedUid;
1167
+ apiReq.authMethod = 'apikey';
1168
+ // uid is the real identity; euid (if set) is the effective/impersonated identity.
1169
+ const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
1170
+ const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
1171
+ const effectiveUid = resolvedEuid ?? resolvedRuid;
1172
+ if (resolvedRuid !== null) {
1173
+ apiReq.realUid = resolvedRuid;
1154
1174
  }
1155
1175
  return {
1156
- uid: key.uid,
1176
+ uid: effectiveUid,
1177
+ ...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
1157
1178
  domain: '',
1158
1179
  fingerprint: '',
1159
1180
  iat: 0,
1160
1181
  exp: 0
1161
1182
  };
1162
1183
  }
1184
+ resolveTokenFromRequest(req) {
1185
+ const paramName = this.config.tokenParam.trim();
1186
+ if (!paramName) {
1187
+ return null;
1188
+ }
1189
+ const location = this.config.tokenParamLocation;
1190
+ if (location === 'body') {
1191
+ return this.readNamedValue(req.body, paramName);
1192
+ }
1193
+ if (location === 'query') {
1194
+ return this.readNamedValue(req.query, paramName);
1195
+ }
1196
+ return this.readNamedValue(req.body, paramName) ?? this.readNamedValue(req.query, paramName);
1197
+ }
1198
+ readNamedValue(container, key) {
1199
+ if (!container || typeof container !== 'object' || Array.isArray(container)) {
1200
+ return null;
1201
+ }
1202
+ const raw = container[key];
1203
+ if (typeof raw === 'string') {
1204
+ const token = raw.trim();
1205
+ return token.length > 0 ? token : null;
1206
+ }
1207
+ if (Array.isArray(raw)) {
1208
+ for (const entry of raw) {
1209
+ if (typeof entry === 'string') {
1210
+ const token = entry.trim();
1211
+ if (token.length > 0) {
1212
+ return token;
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ return null;
1218
+ }
1163
1219
  requiresAuthToken(authType) {
1164
1220
  return authType === 'yes' || authType === 'strict';
1165
1221
  }
@@ -1230,6 +1286,7 @@ export class ApiServer {
1230
1286
  res,
1231
1287
  token: '',
1232
1288
  tokenData: null,
1289
+ authMethod: null,
1233
1290
  realUid: null,
1234
1291
  getClientInfo: () => ensureClientInfo(apiReq),
1235
1292
  getClientIp: () => ensureClientInfo(apiReq).ip,
@@ -1423,12 +1480,25 @@ export class ApiServer {
1423
1480
  }
1424
1481
  dumpRequest(apiReq) {
1425
1482
  const req = apiReq.req;
1483
+ const tokenParam = this.config.tokenParam.trim();
1426
1484
  console.log('--- Incoming Request! ---');
1427
1485
  const url = req.originalUrl || req.url;
1428
1486
  console.log('URL:', url);
1429
1487
  console.log('Method:', req.method);
1430
- console.log('Query Params:', req.query || {});
1488
+ if (tokenParam &&
1489
+ req.query &&
1490
+ typeof req.query === 'object' &&
1491
+ tokenParam in req.query) {
1492
+ const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
1493
+ console.log('Query Params:', maskedQuery);
1494
+ }
1495
+ else {
1496
+ console.log('Query Params:', req.query || {});
1497
+ }
1431
1498
  const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
1499
+ if (tokenParam) {
1500
+ sensitiveBodyKeys.push(tokenParam);
1501
+ }
1432
1502
  const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
1433
1503
  if (body && typeof body === 'object') {
1434
1504
  for (const key of sensitiveBodyKeys) {
@@ -1499,7 +1569,21 @@ export class ApiServer {
1499
1569
  return value;
1500
1570
  }
1501
1571
  dumpResponse(apiReq, payload, status) {
1502
- const url = apiReq.req.originalUrl || apiReq.req.url;
1572
+ const rawUrl = apiReq.req.originalUrl || apiReq.req.url;
1573
+ const tokenParam = this.config.tokenParam.trim();
1574
+ let url = rawUrl;
1575
+ if (tokenParam && rawUrl) {
1576
+ try {
1577
+ const parsed = new URL(rawUrl, 'http://x');
1578
+ if (parsed.searchParams.has(tokenParam)) {
1579
+ parsed.searchParams.set(tokenParam, '[REDACTED]');
1580
+ url = parsed.pathname + '?' + parsed.searchParams.toString();
1581
+ }
1582
+ }
1583
+ catch {
1584
+ // Leave url unmodified if parsing fails
1585
+ }
1586
+ }
1503
1587
  console.log('--- Outgoing Response! ---');
1504
1588
  console.log('URL:', url);
1505
1589
  console.log('Status:', status);