@tumbaland/backend-core 1.44.0 → 1.46.0

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 (123) hide show
  1. package/dist/fileService/client.d.ts +54 -0
  2. package/dist/fileService/client.d.ts.map +1 -0
  3. package/dist/fileService/client.js +137 -0
  4. package/dist/fileService/client.js.map +1 -0
  5. package/dist/fileService/index.d.ts +4 -0
  6. package/dist/fileService/index.d.ts.map +1 -0
  7. package/dist/fileService/index.js +40 -0
  8. package/dist/fileService/index.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware/corsMiddleware.d.ts.map +1 -1
  14. package/dist/middleware/corsMiddleware.js +4 -1
  15. package/dist/middleware/corsMiddleware.js.map +1 -1
  16. package/dist/middleware/errorHandler.d.ts.map +1 -1
  17. package/dist/middleware/errorHandler.js +5 -8
  18. package/dist/middleware/errorHandler.js.map +1 -1
  19. package/dist/utils/escapeRegex.d.ts +8 -0
  20. package/dist/utils/escapeRegex.d.ts.map +1 -0
  21. package/dist/utils/escapeRegex.js +12 -0
  22. package/dist/utils/escapeRegex.js.map +1 -0
  23. package/package.json +5 -1
  24. package/.versionrc.json +0 -7
  25. package/__mocks__/uuid.js +0 -8
  26. package/jest.config.js +0 -24
  27. package/src/apiKeys/ApiKey.test.ts +0 -142
  28. package/src/apiKeys/ApiKey.ts +0 -102
  29. package/src/apiKeys/crypto.test.ts +0 -161
  30. package/src/apiKeys/crypto.ts +0 -163
  31. package/src/apiKeys/index.test.ts +0 -72
  32. package/src/apiKeys/index.ts +0 -35
  33. package/src/apiKeys/middleware.test.ts +0 -651
  34. package/src/apiKeys/middleware.ts +0 -341
  35. package/src/apiKeys/service.test.ts +0 -401
  36. package/src/apiKeys/service.ts +0 -168
  37. package/src/apiKeys/types.test.ts +0 -41
  38. package/src/apiKeys/types.ts +0 -124
  39. package/src/app/createBaseApp.test.ts +0 -109
  40. package/src/app/createBaseApp.ts +0 -102
  41. package/src/app/shutdown.test.ts +0 -129
  42. package/src/app/shutdown.ts +0 -81
  43. package/src/audit/AuditEvent.ts +0 -123
  44. package/src/audit/actor.test.ts +0 -95
  45. package/src/audit/actor.ts +0 -68
  46. package/src/audit/context.test.ts +0 -91
  47. package/src/audit/context.ts +0 -83
  48. package/src/audit/index.ts +0 -11
  49. package/src/audit/plugin.test.ts +0 -258
  50. package/src/audit/plugin.ts +0 -254
  51. package/src/audit/reads.test.ts +0 -164
  52. package/src/audit/reads.ts +0 -88
  53. package/src/audit/service.test.ts +0 -115
  54. package/src/audit/service.ts +0 -95
  55. package/src/auth/session.test.ts +0 -89
  56. package/src/auth/session.ts +0 -75
  57. package/src/config/env.test.ts +0 -28
  58. package/src/config/env.ts +0 -13
  59. package/src/database/connection.test.ts +0 -188
  60. package/src/database/connection.ts +0 -90
  61. package/src/entitlements/UsageMeter.ts +0 -49
  62. package/src/entitlements/client.test.ts +0 -200
  63. package/src/entitlements/client.ts +0 -179
  64. package/src/entitlements/definitions.test.ts +0 -161
  65. package/src/entitlements/definitions.ts +0 -268
  66. package/src/entitlements/index.ts +0 -42
  67. package/src/entitlements/middleware.test.ts +0 -196
  68. package/src/entitlements/middleware.ts +0 -150
  69. package/src/entitlements/reconcile.test.ts +0 -333
  70. package/src/entitlements/reconcile.ts +0 -384
  71. package/src/entitlements/types.ts +0 -21
  72. package/src/entitlements/usage.test.ts +0 -314
  73. package/src/entitlements/usage.ts +0 -223
  74. package/src/errors/HttpError.test.ts +0 -76
  75. package/src/errors/HttpError.ts +0 -91
  76. package/src/groups/client.test.ts +0 -215
  77. package/src/groups/client.ts +0 -182
  78. package/src/groups/index.ts +0 -7
  79. package/src/groups/membership.test.ts +0 -84
  80. package/src/groups/membership.ts +0 -133
  81. package/src/groups/subject.test.ts +0 -85
  82. package/src/groups/subject.ts +0 -50
  83. package/src/health/createHealthCheck.test.ts +0 -89
  84. package/src/health/createHealthCheck.ts +0 -67
  85. package/src/health/healthController.test.ts +0 -113
  86. package/src/health/healthController.ts +0 -56
  87. package/src/index.ts +0 -88
  88. package/src/logging/logger.test.ts +0 -91
  89. package/src/logging/logger.ts +0 -103
  90. package/src/metrics/index.test.ts +0 -116
  91. package/src/metrics/index.ts +0 -111
  92. package/src/middleware/authMiddleware.test.ts +0 -275
  93. package/src/middleware/authMiddleware.ts +0 -91
  94. package/src/middleware/corsMiddleware.test.ts +0 -135
  95. package/src/middleware/corsMiddleware.ts +0 -65
  96. package/src/middleware/errorHandler.test.ts +0 -188
  97. package/src/middleware/errorHandler.ts +0 -103
  98. package/src/middleware/internalServiceAuth.test.ts +0 -173
  99. package/src/middleware/internalServiceAuth.ts +0 -96
  100. package/src/middleware/requestLogger.test.ts +0 -81
  101. package/src/middleware/requestLogger.ts +0 -48
  102. package/src/middleware/security.test.ts +0 -45
  103. package/src/middleware/security.ts +0 -43
  104. package/src/middleware/validate.test.ts +0 -72
  105. package/src/middleware/validate.ts +0 -23
  106. package/src/oauth/index.ts +0 -29
  107. package/src/oauth/models.ts +0 -164
  108. package/src/oauth/service.test.ts +0 -432
  109. package/src/oauth/service.ts +0 -299
  110. package/src/oauth/tokens.test.ts +0 -146
  111. package/src/oauth/tokens.ts +0 -186
  112. package/src/testing/serviceTestSetup.ts +0 -17
  113. package/src/tracing/index.test.ts +0 -272
  114. package/src/tracing/index.ts +0 -110
  115. package/src/types/auth.ts +0 -45
  116. package/src/utils/correlation.test.ts +0 -47
  117. package/src/utils/correlation.ts +0 -22
  118. package/src/utils/permissionUtils.test.ts +0 -47
  119. package/src/utils/permissionUtils.ts +0 -68
  120. package/src/utils/response.test.ts +0 -64
  121. package/src/utils/response.ts +0 -60
  122. package/tsconfig.build.json +0 -7
  123. package/tsconfig.json +0 -23
@@ -1,341 +0,0 @@
1
- import { NextFunction, Request, RequestHandler, Response } from 'express';
2
- import jwt from 'jsonwebtoken';
3
- import { requireEnv } from '../config/env';
4
- import logger from '../logging/logger';
5
- import { beginAudit } from '../audit/context';
6
- import { isAccessTokenClaims, type AccessTokenClaims } from '../oauth/tokens';
7
- import type { UserPayload } from '../types/auth';
8
- import { looksLikeApiKey } from './crypto';
9
- import { verifyApiKey } from './service';
10
- import {
11
- PERSONAL_TENANT,
12
- groupIdsOf,
13
- isApiKeyScope,
14
- readTenants,
15
- type ApiKeyScope,
16
- type ApiKeyTenant,
17
- type Tenant
18
- } from './types';
19
- import { isCurrentSession } from '../auth/session';
20
- import { currentGroupsOf } from '../groups/membership';
21
-
22
- /**
23
- * Request-scoped facts about the key a request arrived on. Absent on ordinary
24
- * session requests, which is itself the signal a handler needs: `req.apiKey`
25
- * being set means software is acting, not a person.
26
- */
27
- export interface ApiKeyContext {
28
- keyId: string;
29
- scopes: ApiKeyScope[];
30
- /** every tenant this credential may act in, and the one it acts in by default */
31
- tenants: ApiKeyTenant;
32
- /** the tenant this particular request resolved to */
33
- actingAs: Tenant;
34
- /** which credential this is; they are revoked and audited differently */
35
- kind: 'api_key' | 'oauth';
36
- /** what the owner called it — a key's name, or the connected app's */
37
- label: string;
38
- /**
39
- * The stable identity of the credential.
40
- *
41
- * A key's document id, or an OAuth client id — never the access token's
42
- * `jti`, which rotates hourly and would make one connection look like a new
43
- * actor every time it refreshed.
44
- */
45
- credentialId: string;
46
- }
47
-
48
- declare global {
49
- namespace Express {
50
- interface Request {
51
- apiKey?: ApiKeyContext;
52
- }
53
- }
54
- }
55
-
56
- /** Both auth paths read the token from the same two places. */
57
- const extractToken = (req: Request): string | undefined =>
58
- req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
59
-
60
- const unauthorized = (res: Response): void => {
61
- res.status(401).json({ success: false, message: 'Invalid or expired credentials' });
62
- };
63
-
64
- /**
65
- * The tenant a request is asking to act in, wherever it named one.
66
- *
67
- * Handlers read `groupId` from either the query string or the body depending on
68
- * the verb, so both are checked — a rule that only covered one of them would be
69
- * no rule at all. A caller names the personal tenant with the literal
70
- * `personal`, since "no group" is what the resolved default gets written into
71
- * and would otherwise be indistinguishable from not having asked.
72
- */
73
- const requestedTenant = (req: Request): Tenant | undefined => {
74
- const fromQuery = req.query?.groupId;
75
- if (typeof fromQuery === 'string' && fromQuery.length > 0) return fromQuery;
76
-
77
- const fromBody = (req.body as Record<string, unknown> | undefined)?.groupId;
78
- if (typeof fromBody === 'string' && fromBody.length > 0) return fromBody;
79
-
80
- return undefined;
81
- };
82
-
83
- /** Express 5 makes `req.query` a getter, so it is redefined rather than assigned. */
84
- const setQueryGroupId = (req: Request, groupId: string | undefined): void => {
85
- const query = { ...req.query } as Record<string, unknown>;
86
- if (groupId === undefined) delete query.groupId;
87
- else query.groupId = groupId;
88
-
89
- Object.defineProperty(req, 'query', {
90
- value: query,
91
- writable: true,
92
- configurable: true,
93
- enumerable: true
94
- });
95
- };
96
-
97
- /**
98
- * Hold a credential to the tenants it was granted, and settle which one this
99
- * request is acting in.
100
- *
101
- * A request naming a tenant outside the grant is refused outright — that
102
- * boundary is the whole reason a credential is safe to hand to an agent, and it
103
- * is unchanged by the grant being a set rather than one. What the set adds is a
104
- * choice *inside* it, which is what lets one connection reach a shared journal
105
- * and a private photo library without reconnecting.
106
- *
107
- * A request that names nothing gets the default written in for it, so an agent
108
- * granted a single tenant never has to know a group id exists and a handler's
109
- * `if (groupId) … else personal` branch lands where the owner intended.
110
- */
111
- const applyTenantGrant = (req: Request, tenants: ApiKeyTenant): Tenant | null => {
112
- const requested = requestedTenant(req) ?? tenants.default;
113
-
114
- if (!tenants.allowed.includes(requested)) return null;
115
-
116
- // Resolved either way, including when the caller asked for exactly what the
117
- // default already was: handlers read the personal tenant as an absent
118
- // `groupId`, so `personal` has to be erased rather than passed through.
119
- const groupId = requested === PERSONAL_TENANT ? undefined : requested;
120
- setQueryGroupId(req, groupId);
121
- if (req.body && typeof req.body === 'object') {
122
- if (groupId === undefined) delete (req.body as Record<string, unknown>).groupId;
123
- else (req.body as Record<string, unknown>).groupId = groupId;
124
- }
125
-
126
- return requested;
127
- };
128
-
129
- /** A verified non-session credential, whichever kind it arrived as. */
130
- interface AgentCredential {
131
- /** the key's document id, or the OAuth client id */
132
- credentialId: string;
133
- kind: 'api_key' | 'oauth';
134
- /** what the owner called it, for a trail a person can read */
135
- label: string;
136
- userId: string;
137
- email: string;
138
- name: string;
139
- scopes: ApiKeyScope[];
140
- tenants: ApiKeyTenant;
141
- }
142
-
143
- /**
144
- * Admit software acting for a user, on the terms its credential carries.
145
- *
146
- * Shared by both non-session credentials on purpose. An API key and an OAuth
147
- * access token differ entirely in how they are issued and verified, and not at
148
- * all in what they mean once they are: a user, a set of scopes, and one tenant.
149
- * Settling that in one place is what keeps the two from drifting into subtly
150
- * different amounts of access.
151
- */
152
- async function admitAgent(
153
- req: Request,
154
- res: Response,
155
- next: NextFunction,
156
- requiredScopes: ApiKeyScope[],
157
- credential: AgentCredential
158
- ): Promise<void> {
159
- const missing = requiredScopes.filter((scope) => !credential.scopes.includes(scope));
160
- if (missing.length > 0) {
161
- res.status(403).json({
162
- success: false,
163
- message: `Credential is missing required scope: ${missing.join(', ')}`
164
- });
165
- return;
166
- }
167
-
168
- // A grant names the groups the owner belonged to when they made it. Only the
169
- // ones they still belong to count, so leaving a group also takes it from
170
- // every key and connection they gave it to.
171
- const stillMemberOf = new Set(await currentGroupsOf({ email: credential.email }));
172
- const tenants: ApiKeyTenant = {
173
- ...credential.tenants,
174
- allowed: credential.tenants.allowed.filter((tenant) => tenant === PERSONAL_TENANT || stillMemberOf.has(tenant))
175
- };
176
-
177
- const actingAs = applyTenantGrant(req, tenants);
178
- if (actingAs === null) {
179
- res.status(403).json({
180
- success: false,
181
- message: 'Credential is not permitted to act in the requested group'
182
- });
183
- return;
184
- }
185
-
186
- req.user = { id: credential.userId, email: credential.email, name: credential.name };
187
- // Only the granted groups, never the owner's full membership: this is what
188
- // stops a credential reaching a group it was not issued for through any
189
- // handler that consults `userGroups` instead of the `groupId` parameter.
190
- req.userGroups = groupIdsOf(tenants.allowed);
191
- req.apiKey = {
192
- keyId: credential.credentialId,
193
- scopes: credential.scopes,
194
- tenants,
195
- actingAs,
196
- kind: credential.kind,
197
- label: credential.label,
198
- credentialId: credential.credentialId
199
- };
200
-
201
- // Attribution comes from authentication, so it is established here rather
202
- // than mounted separately — a router that authenticates per route would
203
- // otherwise have no actor in scope when a router-level middleware ran.
204
- beginAudit(req, res, next);
205
- }
206
-
207
- /**
208
- * Authenticate a request that software may legitimately be making.
209
- *
210
- * Accepts either an ordinary session — a person at a keyboard, who keeps the
211
- * full run of their own account — or an API key carrying every one of
212
- * `requiredScopes`.
213
- *
214
- * This is deliberately *not* what `authenticateToken` became. Keys stay refused
215
- * everywhere by default, and a route opts in by naming the scopes it needs; the
216
- * alternative, teaching the existing middleware about keys, would have silently
217
- * opened every route in every service at once, account deletion included.
218
- */
219
- export const authenticateAgent = (...requiredScopes: ApiKeyScope[]): RequestHandler =>
220
- async function authenticateAgentHandler(req: Request, res: Response, next: NextFunction): Promise<void> {
221
- const token = extractToken(req);
222
- if (!token) {
223
- res.status(401).json({ success: false, message: 'Access token required' });
224
- return;
225
- }
226
-
227
- if (!looksLikeApiKey(token)) {
228
- let claims: UserPayload | AccessTokenClaims;
229
- try {
230
- claims = jwt.verify(token, requireEnv('JWT_SECRET')) as UserPayload | AccessTokenClaims;
231
- } catch {
232
- unauthorized(res);
233
- return;
234
- }
235
-
236
- if (!isAccessTokenClaims(claims)) {
237
- if (!(await isCurrentSession(claims))) {
238
- unauthorized(res);
239
- return;
240
- }
241
- const groups = await currentGroupsOf(claims as UserPayload);
242
- req.user = { ...(claims as UserPayload), groups };
243
- req.userGroups = groups;
244
- beginAudit(req, res, next);
245
- return;
246
- }
247
-
248
- // An OAuth access token: signed with the same secret as a session and
249
- // arriving in the same header, but nothing like one. It names its user in
250
- // `sub`, carries scopes, and is pinned to a tenant — so it is admitted
251
- // the way a key is, not the way a person is.
252
- //
253
- // Reading it as a session was the original bug and it failed quietly: the
254
- // claims have no `id` and no `groups`, so `req.user.id` arrived as
255
- // `undefined`, scoped writes were refused as inaccessible, and a read
256
- // whose scope collapsed to nothing fell through to "no user context" and
257
- // answered from the whole collection.
258
- await admitAgent(req, res, next, requiredScopes, {
259
- // The client, not the token: `jti` is a new value every hour.
260
- credentialId: claims.clientId ?? 'unknown-client',
261
- kind: 'oauth',
262
- label: claims.clientName || 'A connected app',
263
- userId: claims.sub,
264
- email: claims.email ?? '',
265
- name: claims.name ?? '',
266
- scopes: (claims.scope ?? '').split(' ').filter(isApiKeyScope),
267
- tenants: readTenants(claims)
268
- });
269
- return;
270
- }
271
-
272
- const result = await verifyApiKey(token);
273
- if (!result.ok) {
274
- // The client is told only that the credentials failed; which of the five
275
- // ways it failed is a detail that would help someone guessing.
276
- logger.warn('API key rejected', { rejection: result.rejection, path: req.path });
277
- unauthorized(res);
278
- return;
279
- }
280
-
281
- await admitAgent(req, res, next, requiredScopes, {
282
- credentialId: result.keyId!,
283
- kind: 'api_key',
284
- label: result.label || 'An API key',
285
- userId: result.userId!,
286
- email: result.userEmail ?? '',
287
- name: result.userName ?? '',
288
- scopes: result.scopes ?? [],
289
- tenants: result.tenants ?? readTenants({})
290
- });
291
- };
292
-
293
- /**
294
- * Require a scope on a route already behind `authenticateAgent`.
295
- *
296
- * Splitting authentication from authorization lets one `router.use` cover a
297
- * whole router while each route still states what it needs — so a read-only key
298
- * reaches the GETs and stops at the POSTs, instead of being turned away at the
299
- * door for lacking a scope half the router never uses.
300
- *
301
- * A session passes unconditionally: scopes narrow what software may do on a
302
- * person's behalf, not what the person may do themselves. Everything that is
303
- * not a session — an API key or an OAuth access token alike — arrives with
304
- * `req.apiKey` set and is held to it.
305
- */
306
- export const requireScope = (...requiredScopes: ApiKeyScope[]): RequestHandler =>
307
- function requireScopeHandler(req, res, next) {
308
- if (!req.apiKey) {
309
- next();
310
- return;
311
- }
312
-
313
- const missing = requiredScopes.filter((scope) => !req.apiKey!.scopes.includes(scope));
314
- if (missing.length > 0) {
315
- res.status(403).json({
316
- success: false,
317
- message: `Credential is missing required scope: ${missing.join(', ')}`
318
- });
319
- return;
320
- }
321
-
322
- next();
323
- };
324
-
325
- /**
326
- * Refuse every non-session credential on a route that a session may still use.
327
- *
328
- * For the handful of operations that should stay a person's to perform — key
329
- * management itself, most obviously, since a key that can mint keys is a key
330
- * that cannot be revoked.
331
- */
332
- export const denyApiKeys: RequestHandler = (req, res, next) => {
333
- if (req.apiKey) {
334
- res.status(403).json({
335
- success: false,
336
- message: 'This operation requires an interactive session, not an API key'
337
- });
338
- return;
339
- }
340
- next();
341
- };