@luckystack/core 0.10.2 → 0.11.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 (122) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +2 -88
  4. package/dist/apiInterceptors.d.ts +36 -0
  5. package/dist/apiMethodMapRegistry.d.ts +5 -0
  6. package/dist/apiRequest.d.ts +71 -0
  7. package/dist/apiTypeStubs.d.ts +3 -5
  8. package/dist/avatarConfig.d.ts +22 -0
  9. package/dist/bindAddress.d.ts +15 -0
  10. package/dist/bootUuid.d.ts +24 -0
  11. package/dist/cancelRegistry.d.ts +7 -0
  12. package/dist/checkOrigin.d.ts +6 -0
  13. package/dist/client.d.ts +46 -335
  14. package/dist/client.js +2 -2
  15. package/dist/client.js.map +1 -1
  16. package/dist/clientHookBus.d.ts +62 -0
  17. package/dist/clients.d.ts +16 -0
  18. package/dist/config.d.ts +8 -2
  19. package/dist/configUtils.d.ts +16 -0
  20. package/dist/consoleLog.d.ts +1 -0
  21. package/dist/cookies.d.ts +28 -0
  22. package/dist/createRegistry.d.ts +39 -0
  23. package/dist/csrf.d.ts +30 -0
  24. package/dist/csrfConfig.d.ts +32 -0
  25. package/dist/db.d.ts +12 -0
  26. package/dist/dbHealthCheck.d.ts +6 -0
  27. package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
  28. package/dist/emailRegistry.d.ts +139 -0
  29. package/dist/env.d.ts +38 -0
  30. package/dist/errorFormatterRegistry.d.ts +54 -0
  31. package/dist/errorTrackerIdentity.d.ts +14 -0
  32. package/dist/errorTrackerRegistry.d.ts +85 -0
  33. package/dist/escapeHtml.d.ts +6 -0
  34. package/dist/eslint/index.d.ts +5 -8
  35. package/dist/eslint/internal/hasPackage.d.ts +1 -0
  36. package/dist/eslint/internal/ruleTypes.d.ts +4 -0
  37. package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
  38. package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
  39. package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
  40. package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
  41. package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
  42. package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
  43. package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
  44. package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
  45. package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
  46. package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
  47. package/dist/extractToken.d.ts +12 -0
  48. package/dist/extractTokenFromRequest.d.ts +12 -0
  49. package/dist/getParams.d.ts +9 -0
  50. package/dist/hooks/registry.d.ts +12 -0
  51. package/dist/hooks/types.d.ts +446 -0
  52. package/dist/httpApiUtils.d.ts +54 -0
  53. package/dist/index.d.ts +110 -1892
  54. package/dist/index.js.map +1 -1
  55. package/dist/lease.d.ts +18 -0
  56. package/dist/localSocketEnumerationGuard.d.ts +22 -0
  57. package/dist/localeReloader.d.ts +3 -0
  58. package/dist/localesRegistry.d.ts +8 -0
  59. package/dist/localizedNormalizer.d.ts +50 -0
  60. package/dist/loggerRegistry.d.ts +12 -0
  61. package/dist/middlewareRegistry.d.ts +45 -0
  62. package/dist/notifier.d.ts +17 -0
  63. package/dist/offlineQueue.d.ts +48 -0
  64. package/dist/oneTimeToken.d.ts +43 -0
  65. package/dist/pageRouteValidation.d.ts +37 -0
  66. package/dist/paths.d.ts +35 -0
  67. package/dist/peerDeps.d.ts +23 -0
  68. package/dist/portOverrideRegistry.d.ts +3 -0
  69. package/dist/processUpload.d.ts +44 -0
  70. package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
  71. package/dist/rateLimiter.d.ts +90 -0
  72. package/dist/react/AvatarProvider.d.ts +11 -0
  73. package/dist/react/Middleware.d.ts +10 -0
  74. package/dist/react/Router.d.ts +1 -0
  75. package/dist/react/TranslationProvider.d.ts +22 -0
  76. package/dist/react/notify.d.ts +12 -0
  77. package/dist/react/sessionContext.d.ts +31 -0
  78. package/dist/react/useTheme.d.ts +5 -0
  79. package/dist/redactedLogKeys.d.ts +22 -0
  80. package/dist/redis.d.ts +15 -0
  81. package/dist/redisKeyFormatter.d.ts +7 -0
  82. package/dist/resolveClientIp.d.ts +51 -0
  83. package/dist/responseNormalizer.d.ts +33 -0
  84. package/dist/roomNameFormatterRegistry.d.ts +36 -0
  85. package/dist/roomSockets.d.ts +23 -0
  86. package/dist/routedDataQuery.d.ts +7 -0
  87. package/dist/routedHttpInvocation.d.ts +20 -0
  88. package/dist/runtimeMapsRegistry.d.ts +18 -0
  89. package/dist/runtimeTypeValidation.d.ts +13 -0
  90. package/dist/secretsResolved.d.ts +3 -0
  91. package/dist/sentrySetup.d.ts +21 -0
  92. package/dist/serveAvatars.d.ts +5 -0
  93. package/dist/serviceRoute.d.ts +27 -0
  94. package/dist/servicesConfigRegistry.d.ts +22 -0
  95. package/dist/sessionProviderRegistry.d.ts +46 -0
  96. package/dist/sessionTypes.d.ts +79 -0
  97. package/dist/sleep.d.ts +1 -0
  98. package/dist/socketEvents.d.ts +37 -0
  99. package/dist/socketMiddlewareRegistry.d.ts +20 -0
  100. package/dist/socketRedisAdapter.d.ts +21 -0
  101. package/dist/socketState.d.ts +5 -0
  102. package/dist/socketStatusTypes.d.ts +6 -0
  103. package/dist/socketTypes.d.ts +34 -0
  104. package/dist/synchronizedEnvHashes.d.ts +66 -0
  105. package/dist/testFileConvention.d.ts +11 -0
  106. package/dist/testResetTargets.d.ts +5 -0
  107. package/dist/tryCatch.d.ts +1 -0
  108. package/dist/tryCatchClient.d.ts +1 -0
  109. package/dist/tryCatchSync.d.ts +1 -0
  110. package/dist/validateRequest.d.ts +50 -0
  111. package/docs/app-bootstrap.md +4 -449
  112. package/docs/config-registry.md +3 -346
  113. package/docs/csrf-config.md +3 -86
  114. package/docs/error-tracker-registry.md +3 -192
  115. package/docs/hooks.md +2 -295
  116. package/docs/rate-limit-strategy.md +3 -168
  117. package/docs/redis-adapter.md +5 -110
  118. package/docs/session-types.md +3 -178
  119. package/docs/socket-bootstrap.md +5 -385
  120. package/package.json +2 -2
  121. package/CLAUDE.md +0 -256
  122. package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,12 @@
1
+ import { Socket } from 'socket.io';
2
+ /**
3
+ * Extract the authentication token from a Socket.io connection.
4
+ *
5
+ * Checks `config.sessionBasedToken` to determine where to look:
6
+ * - `true`: Token is in `socket.handshake.auth.token` (sessionStorage on client)
7
+ * - `false`: Token is in cookies via `socket.handshake.headers.cookie`
8
+ *
9
+ * @param socket - The Socket.io socket instance
10
+ * @returns The token string or null if not found
11
+ */
12
+ export declare const extractTokenFromSocket: (socket: Pick<Socket, 'handshake'>) => string | null;
@@ -0,0 +1,12 @@
1
+ import { IncomingMessage } from 'node:http';
2
+ /**
3
+ * Extract the authentication token from an HTTP request.
4
+ *
5
+ * Supports tokens from:
6
+ * - `Authorization: Bearer <token>` header
7
+ * - the configured session cookie
8
+ *
9
+ * @param req - The HTTP incoming message
10
+ * @returns The token string or null if not found
11
+ */
12
+ export declare const extractTokenFromRequest: (req: IncomingMessage) => string | null;
@@ -0,0 +1,9 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ interface getParamsType {
3
+ method: string;
4
+ req: IncomingMessage;
5
+ res: ServerResponse;
6
+ queryString?: string;
7
+ }
8
+ export default function getParams({ method, req, res: _res, queryString }: getParamsType): Promise<Record<string, unknown> | null>;
9
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { HookName, HookPayloads, HookHandler, HookStopSignal, SyncHookHandler, SyncHookName, SyncHookPayloads } from './types.js';
2
+ export type DispatchResult = {
3
+ stopped: false;
4
+ } | {
5
+ stopped: true;
6
+ signal: HookStopSignal;
7
+ };
8
+ export declare const registerHook: <TName extends HookName>(name: TName, handler: HookHandler<HookPayloads[TName]>) => (() => void);
9
+ export declare const dispatchHook: <TName extends HookName>(name: TName, payload: HookPayloads[TName]) => Promise<DispatchResult>;
10
+ export declare const clearAllHooks: () => void;
11
+ export declare const registerSyncHook: <TName extends SyncHookName>(name: TName, handler: SyncHookHandler<SyncHookPayloads[TName]>) => (() => void);
12
+ export declare const dispatchSyncHook: <TName extends SyncHookName>(name: TName, payload: SyncHookPayloads[TName]) => void;
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Core hook contract: framework-generic payload shapes + augmentable registry.
3
+ *
4
+ * Feature packages (login/sync/api/presence/...) extend `HookPayloads` via
5
+ * TypeScript module augmentation:
6
+ *
7
+ * ```ts
8
+ * // packages/<feature>/src/hookPayloads.ts
9
+ * declare module '@luckystack/core' {
10
+ * interface HookPayloads {
11
+ * myFeatureHook: { ... };
12
+ * }
13
+ * }
14
+ * export {};
15
+ * ```
16
+ *
17
+ * Module-augmentation files MUST be in a tsconfig `include` path for TS to
18
+ * pick up the merge; a side-effect import from the package's `index.ts`
19
+ * (`import './hookPayloads';`) guarantees this.
20
+ */
21
+ import type { ErrorResponseInput, NormalizedErrorResponse } from '../responseNormalizer.js';
22
+ /**
23
+ * Minimal session shape used inside core-owned hook payloads. Defined here
24
+ * (instead of imported from `@luckystack/login`) so `@luckystack/core` stays
25
+ * independent of login. Any concrete session type (`BaseSessionLayout`,
26
+ * project-level `SessionLayout` extending Prisma User, etc.) is structurally
27
+ * assignable to this shape.
28
+ */
29
+ export interface HookSessionShape {
30
+ id: string;
31
+ token?: string;
32
+ email?: string | null;
33
+ name?: string | null;
34
+ avatar?: string | null;
35
+ avatarFallback?: string | null;
36
+ admin?: boolean | null;
37
+ language?: string | null;
38
+ roomCodes?: string[];
39
+ }
40
+ export interface HookStopSignal {
41
+ stop: true;
42
+ errorCode: string;
43
+ httpStatus?: number;
44
+ /**
45
+ * Optional output override for per-recipient hooks (`preSyncRecipient`).
46
+ * When present on a stop signal the framework DOES NOT skip the recipient —
47
+ * instead it sends this value in place of the full `serverOutput`. Use to
48
+ * redact PII before delivery without excluding the socket from the fanout.
49
+ * Ignored by all other hook dispatch sites.
50
+ */
51
+ overrideOutput?: unknown;
52
+ }
53
+ /** Handlers return undefined / void to continue, or a stop signal to abort the main flow. */
54
+ export type HookResult = void | undefined | HookStopSignal;
55
+ export type HookHandler<TPayload> = (payload: TPayload) => Promise<HookResult> | HookResult;
56
+ export interface PreApiValidatePayload {
57
+ routeName: string;
58
+ data: Record<string, unknown>;
59
+ user: HookSessionShape | null;
60
+ /**
61
+ * Optional transport tag — populated by the framework's socket/HTTP API
62
+ * handlers so hook subscribers can branch on it (e.g. open span with a
63
+ * different op name). Optional so consumer-side dispatchers don't have to
64
+ * set it explicitly.
65
+ */
66
+ transport?: 'socket' | 'http';
67
+ }
68
+ export interface PostApiValidatePayload extends PreApiValidatePayload {
69
+ validation: {
70
+ status: 'success';
71
+ } | {
72
+ status: 'error';
73
+ message: string;
74
+ };
75
+ }
76
+ export interface PreApiExecutePayload {
77
+ routeName: string;
78
+ data: Record<string, unknown>;
79
+ user: HookSessionShape | null;
80
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
81
+ transport?: 'socket' | 'http';
82
+ }
83
+ export interface PostApiExecutePayload {
84
+ routeName: string;
85
+ data: Record<string, unknown>;
86
+ user: HookSessionShape | null;
87
+ result: unknown;
88
+ error: Error | null;
89
+ durationMs: number;
90
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
91
+ transport?: 'socket' | 'http';
92
+ }
93
+ export interface ApiResponseEnvelope {
94
+ status: 'success' | 'error';
95
+ httpStatus?: number;
96
+ errorCode?: string;
97
+ message?: string;
98
+ [key: string]: unknown;
99
+ }
100
+ export interface PreApiRespondPayload {
101
+ routeName: string;
102
+ user: HookSessionShape | null;
103
+ response: ApiResponseEnvelope;
104
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
105
+ transport?: 'socket' | 'http';
106
+ }
107
+ export interface PostApiRespondPayload {
108
+ routeName: string;
109
+ user: HookSessionShape | null;
110
+ response: ApiResponseEnvelope;
111
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
112
+ transport?: 'socket' | 'http';
113
+ }
114
+ export interface PreSyncAuthorizePayload {
115
+ routeName: string;
116
+ /** Raw (un-validated) data payload. May be reshape-able by the consumer. */
117
+ data: Record<string, unknown>;
118
+ user: HookSessionShape | null;
119
+ /** Room code / receiver token — the sync's intended audience. */
120
+ receiver: string;
121
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
122
+ transport?: 'socket' | 'http';
123
+ }
124
+ export interface PostSyncAuthorizePayload {
125
+ routeName: string;
126
+ data: Record<string, unknown>;
127
+ user: HookSessionShape | null;
128
+ receiver: string;
129
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
130
+ transport?: 'socket' | 'http';
131
+ }
132
+ export interface PreSyncValidatePayload {
133
+ routeName: string;
134
+ data: Record<string, unknown>;
135
+ user: HookSessionShape | null;
136
+ receiver: string;
137
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
138
+ transport?: 'socket' | 'http';
139
+ }
140
+ export interface PostSyncValidatePayload extends PreSyncValidatePayload {
141
+ validation: {
142
+ status: 'success';
143
+ } | {
144
+ status: 'error';
145
+ message: string;
146
+ };
147
+ }
148
+ export interface PreSyncExecutePayload {
149
+ routeName: string;
150
+ data: Record<string, unknown>;
151
+ user: HookSessionShape | null;
152
+ receiver: string;
153
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
154
+ transport?: 'socket' | 'http';
155
+ }
156
+ export interface PostSyncExecutePayload {
157
+ routeName: string;
158
+ data: Record<string, unknown>;
159
+ user: HookSessionShape | null;
160
+ receiver: string;
161
+ result: unknown;
162
+ error: Error | null;
163
+ durationMs: number;
164
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
165
+ transport?: 'socket' | 'http';
166
+ }
167
+ export interface PreSyncFanoutPayload {
168
+ routeName: string;
169
+ data: Record<string, unknown>;
170
+ user: HookSessionShape | null;
171
+ receiver: string;
172
+ serverOutput: unknown;
173
+ /** Optional transport tag — see {@link PreApiValidatePayload.transport}. */
174
+ transport?: 'socket' | 'http';
175
+ }
176
+ export interface PostSyncFanoutPayload extends PreSyncFanoutPayload {
177
+ recipientCount: number;
178
+ }
179
+ export interface PreSyncStreamPayload {
180
+ routeName: string;
181
+ /** Stream chunk being sent (already serialised by the framework). */
182
+ chunk: unknown;
183
+ /** Final recipient — socket id when known, room code otherwise. */
184
+ recipient: string;
185
+ }
186
+ export interface PostSyncStreamPayload extends PreSyncStreamPayload {
187
+ /** Total chunks streamed so far in this sync invocation. */
188
+ chunkIndex: number;
189
+ }
190
+ export interface ApiErrorPayload {
191
+ route: string;
192
+ method?: string;
193
+ requestId?: string;
194
+ user?: HookSessionShape | null;
195
+ error: Error;
196
+ }
197
+ export interface SyncErrorPayload {
198
+ route: string;
199
+ method?: string;
200
+ requestId?: string;
201
+ user?: HookSessionShape | null;
202
+ error: Error;
203
+ }
204
+ export interface RateLimitExceededPayload {
205
+ /** Where the limit was hit — IP, user, or route. */
206
+ scope: 'ip' | 'user' | 'route' | 'auth';
207
+ /** The key that exceeded its limit (sanitized — no tokens). */
208
+ key: string;
209
+ /** Configured limit count for this key. */
210
+ limit: number;
211
+ /** Window in milliseconds. */
212
+ windowMs: number;
213
+ /** Current bucket count after the rejected request. */
214
+ count: number;
215
+ /** Optional route or transport context. */
216
+ route?: string;
217
+ ip?: string;
218
+ userId?: string;
219
+ }
220
+ export interface CorsRejectedPayload {
221
+ /** Origin header sent by the client. */
222
+ origin: string;
223
+ /** Origin after framework normalization (scheme + host[:port]). */
224
+ normalizedOrigin: string;
225
+ /** Effective allowed-origins set at the time of rejection. */
226
+ allowedOrigins: string[];
227
+ /** Whether `allowLocalhost` was on. */
228
+ allowLocalhost: boolean;
229
+ /** Optional route the rejected request was for. */
230
+ route?: string;
231
+ /**
232
+ * Why the request was rejected (server HOK-27). Distinguishes a present-but-
233
+ * disallowed Origin (`'origin-not-allowed'`) from a MISSING Origin header
234
+ * (`'origin-missing'`, the 403 dispatched by the server's origin gate) and the
235
+ * normalization-failure case. Optional so existing `allowedOrigin()` callers
236
+ * that don't set it stay source-compatible.
237
+ */
238
+ reason?: 'origin-not-allowed' | 'origin-missing' | 'origin-malformed';
239
+ }
240
+ export interface PreSessionRefreshPayload {
241
+ token: string;
242
+ userId: string | null;
243
+ oldTtl: number | null;
244
+ newTtl: number;
245
+ }
246
+ export interface PostSessionRefreshPayload {
247
+ token: string;
248
+ userId: string | null;
249
+ oldTtl: number | null;
250
+ newTtl: number;
251
+ /** True if Redis EXPIRE succeeded; false on failure or non-existent key. */
252
+ applied: boolean;
253
+ }
254
+ export interface SessionCreatedPayload {
255
+ token: string;
256
+ userId: string;
257
+ /** How the session came to exist (credentials login, OAuth, refresh-mint, ...). */
258
+ via?: string;
259
+ }
260
+ export interface SessionRevokedPayload {
261
+ token: string;
262
+ userId: string | null;
263
+ /** Why the session was revoked (logout, kicked-by-new-device, admin, expiry, ...). */
264
+ reason?: string;
265
+ }
266
+ export interface OnUploadStartPayload {
267
+ userId: string;
268
+ contentType: string;
269
+ sizeBytes: number;
270
+ uploadKind: 'avatar' | string;
271
+ }
272
+ export interface OnUploadCompletePayload {
273
+ userId: string;
274
+ fileName: string;
275
+ sizeBytes: number;
276
+ uploadKind: 'avatar' | string;
277
+ }
278
+ export interface PreAvatarServePayload {
279
+ /** The route path requested (`/avatars/:fileId`). */
280
+ routePath: string;
281
+ /** The allowlisted file id resolved from the route path. */
282
+ fileId: string;
283
+ }
284
+ export interface PostAvatarServePayload {
285
+ routePath: string;
286
+ fileId: string;
287
+ /** Disk extension of the format that was served (e.g. `'webp'`). */
288
+ extension: string;
289
+ /** Content-Type header sent for the served file. */
290
+ contentType: string;
291
+ }
292
+ export interface CsrfMismatchPayload {
293
+ /** Path of the rejected request. */
294
+ route: string;
295
+ /** HTTP method of the rejected request. */
296
+ method?: string;
297
+ /** Request id (X-Request-Id) for log correlation. */
298
+ requestId?: string;
299
+ /** User id from the session that the request claimed, if any. */
300
+ userId?: string;
301
+ /** Whether a token was provided in `x-csrf-token` (presence only — never the value). */
302
+ providedToken: boolean;
303
+ }
304
+ export interface PreSocketMessagePayload {
305
+ /** Which socket pipeline received the message. */
306
+ channel: 'api' | 'sync';
307
+ /** Socket.io connection id of the originator. */
308
+ socketId: string;
309
+ /** Handshake address of the originator (best-effort; honours no proxy trust). */
310
+ ip: string;
311
+ /** Whether the originating socket carried a session token (presence only — never the value). */
312
+ authenticated: boolean;
313
+ /** Raw route name from the message envelope, when present (un-parsed, un-trusted). */
314
+ routeName?: string;
315
+ }
316
+ export interface PreHttpRequestPayload {
317
+ /** HTTP method (always uppercase: GET/POST/...). */
318
+ method: string;
319
+ /** Request URL (path + query). */
320
+ url: string;
321
+ /** Caller-supplied or framework-generated `X-Request-Id`. */
322
+ requestId: string;
323
+ /** Origin header value (or empty string when absent). */
324
+ origin: string;
325
+ /**
326
+ * Subset of headers — `authorization`, `cookie`, etc. are NOT included
327
+ * to keep payloads safe for logging. Use the full request object via
328
+ * a custom route if you need raw access.
329
+ */
330
+ headers: Record<string, string>;
331
+ }
332
+ export interface PostHttpRequestPayload {
333
+ /** HTTP method (always uppercase). */
334
+ method: string;
335
+ /** Request URL (path + query). */
336
+ url: string;
337
+ /** Caller-supplied or framework-generated `X-Request-Id`. */
338
+ requestId: string;
339
+ /** Final HTTP status code written to the response. */
340
+ statusCode: number;
341
+ /** Wall-clock duration from request receipt to response, in ms. */
342
+ durationMs: number;
343
+ }
344
+ export interface ApiAuthRejectedPayload {
345
+ /** Resolved route name (`api/billing/getInvoice/v1`). */
346
+ routeName: string;
347
+ /** Why auth failed. `'login-required'` = no/invalid session on a login route;
348
+ * `'additional-failed'` = a session existed but an `additional[]` predicate
349
+ * rejected; `'invalid-condition'` = a misconfigured predicate (setup error). */
350
+ reason: 'login-required' | 'additional-failed' | 'invalid-condition';
351
+ /** Session user id when a (insufficient) session was present, else null. */
352
+ userId: string | null;
353
+ /** Resolved client IP (honours `http.trustProxy`). */
354
+ ip?: string;
355
+ /** Transport the request arrived on. */
356
+ transport?: 'socket' | 'http';
357
+ /** The specific `additional[]` key that failed, when `reason === 'additional-failed'`. */
358
+ failedKey?: string;
359
+ }
360
+ export interface PreSyncRecipientPayload {
361
+ /** Resolved route name (`sync/board/moveCard/v1`). */
362
+ routeName: string;
363
+ /** Room code / receiver this fanout is for. */
364
+ receiver: string;
365
+ /** Socket id of THIS recipient. */
366
+ recipientSocketId: string;
367
+ /**
368
+ * Session token of THIS recipient, as read from its socket handshake — the
369
+ * same value the fan-out uses for `ignoreSelf`. `null` for an anonymous
370
+ * socket. Cheap: no session read is performed to produce it.
371
+ */
372
+ recipientToken: string | null;
373
+ /**
374
+ * Always `null`. Kept on the payload for backward compatibility; resolve the
375
+ * user from `recipientToken` when you need it.
376
+ */
377
+ recipientUserId: string | null;
378
+ /** The server-validated output about to be sent. */
379
+ serverOutput: unknown;
380
+ }
381
+ export interface PreServerStopPayload {
382
+ /** Why the server is stopping — the signal name, or `'manual'` for a programmatic stop. */
383
+ reason: 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'manual';
384
+ /**
385
+ * Soft budget (ms) the server intends to wait for shutdown handlers + in-flight
386
+ * requests to drain before forcing exit. Handlers should not block longer than
387
+ * this. `undefined` when the server applies no deadline.
388
+ */
389
+ timeoutMs?: number;
390
+ }
391
+ export interface HookPayloads {
392
+ preHttpRequest: PreHttpRequestPayload;
393
+ postHttpRequest: PostHttpRequestPayload;
394
+ preSocketMessage: PreSocketMessagePayload;
395
+ preApiValidate: PreApiValidatePayload;
396
+ postApiValidate: PostApiValidatePayload;
397
+ preApiExecute: PreApiExecutePayload;
398
+ postApiExecute: PostApiExecutePayload;
399
+ preApiRespond: PreApiRespondPayload;
400
+ transformApiResponse: PreApiRespondPayload;
401
+ postApiRespond: PostApiRespondPayload;
402
+ preSyncAuthorize: PreSyncAuthorizePayload;
403
+ postSyncAuthorize: PostSyncAuthorizePayload;
404
+ preSyncValidate: PreSyncValidatePayload;
405
+ postSyncValidate: PostSyncValidatePayload;
406
+ preSyncExecute: PreSyncExecutePayload;
407
+ postSyncExecute: PostSyncExecutePayload;
408
+ preSyncFanout: PreSyncFanoutPayload;
409
+ postSyncFanout: PostSyncFanoutPayload;
410
+ preSyncRecipient: PreSyncRecipientPayload;
411
+ preSyncStream: PreSyncStreamPayload;
412
+ postSyncStream: PostSyncStreamPayload;
413
+ apiError: ApiErrorPayload;
414
+ syncError: SyncErrorPayload;
415
+ apiAuthRejected: ApiAuthRejectedPayload;
416
+ rateLimitExceeded: RateLimitExceededPayload;
417
+ corsRejected: CorsRejectedPayload;
418
+ csrfMismatch: CsrfMismatchPayload;
419
+ preSessionRefresh: PreSessionRefreshPayload;
420
+ postSessionRefresh: PostSessionRefreshPayload;
421
+ sessionCreated: SessionCreatedPayload;
422
+ sessionRevoked: SessionRevokedPayload;
423
+ onUploadStart: OnUploadStartPayload;
424
+ onUploadComplete: OnUploadCompletePayload;
425
+ preAvatarServe: PreAvatarServePayload;
426
+ postAvatarServe: PostAvatarServePayload;
427
+ preServerStop: PreServerStopPayload;
428
+ }
429
+ export interface PreErrorNormalizePayload {
430
+ response: ErrorResponseInput;
431
+ preferredLocale?: string | null;
432
+ userLanguage?: string | null;
433
+ fallbackHttpStatus?: number;
434
+ }
435
+ export interface PostErrorNormalizePayload {
436
+ normalized: NormalizedErrorResponse;
437
+ preferredLocale?: string | null;
438
+ userLanguage?: string | null;
439
+ }
440
+ export interface SyncHookPayloads {
441
+ preErrorNormalize: PreErrorNormalizePayload;
442
+ postErrorNormalize: PostErrorNormalizePayload;
443
+ }
444
+ export type SyncHookName = keyof SyncHookPayloads;
445
+ export type SyncHookHandler<TPayload> = (payload: TPayload) => void;
446
+ export type HookName = keyof HookPayloads;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * HTTP API Utilities
3
+ *
4
+ * Helpers for HTTP API request handling including:
5
+ * - Smart HTTP method detection based on API name
6
+ * - Method validation
7
+ */
8
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
9
+ /**
10
+ * Single source of truth for the GET-prefix naming heuristic
11
+ * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the
12
+ * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two
13
+ * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.
14
+ */
15
+ export declare const isGetMethodName: (methodNameLower: string) => boolean;
16
+ /**
17
+ * Infer the HTTP method from an API name based on common naming conventions.
18
+ *
19
+ * - Names starting with "get" → GET
20
+ * - Names starting with "delete" or "remove" → DELETE
21
+ * - Names starting with "update" or "edit" → PUT
22
+ * - Everything else → POST (create operations, actions)
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * inferHttpMethod('getUserData') // → 'GET'
27
+ * inferHttpMethod('deleteAccount') // → 'DELETE'
28
+ * inferHttpMethod('updateProfile') // → 'PUT'
29
+ * inferHttpMethod('createUser') // → 'POST'
30
+ * inferHttpMethod('sendEmail') // → 'POST'
31
+ * ```
32
+ */
33
+ export declare const inferHttpMethod: (apiName: string) => HttpMethod;
34
+ /**
35
+ * Get the effective HTTP method for an API.
36
+ * Uses explicit export if available, otherwise infers from name.
37
+ */
38
+ export declare const getEffectiveHttpMethod: (apiConfig: {
39
+ httpMethod?: HttpMethod;
40
+ }, apiName: string) => HttpMethod;
41
+ /**
42
+ * Check if an HTTP method is valid for an API.
43
+ *
44
+ * @param requestMethod - The method from the HTTP request
45
+ * @param allowedMethod - The method the API accepts
46
+ * @returns true only when the method matches the route's allowed method.
47
+ *
48
+ * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route
49
+ * author wiring this into their dispatch must answer CORS preflights BEFORE the
50
+ * route check — letting `OPTIONS` through would execute the handler on a request
51
+ * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a
52
+ * bypass on method-locked routes.
53
+ */
54
+ export declare const isMethodAllowed: (requestMethod: string, allowedMethod: HttpMethod) => boolean;