@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.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +7 -0
- package/README.md +2 -88
- package/dist/apiInterceptors.d.ts +36 -0
- package/dist/apiMethodMapRegistry.d.ts +5 -0
- package/dist/apiRequest.d.ts +71 -0
- package/dist/apiTypeStubs.d.ts +3 -5
- package/dist/avatarConfig.d.ts +22 -0
- package/dist/bindAddress.d.ts +15 -0
- package/dist/bootUuid.d.ts +24 -0
- package/dist/cancelRegistry.d.ts +7 -0
- package/dist/checkOrigin.d.ts +6 -0
- package/dist/client.d.ts +46 -335
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/clientHookBus.d.ts +62 -0
- package/dist/clients.d.ts +16 -0
- package/dist/config.d.ts +8 -2
- package/dist/configUtils.d.ts +16 -0
- package/dist/consoleLog.d.ts +1 -0
- package/dist/cookies.d.ts +28 -0
- package/dist/createRegistry.d.ts +39 -0
- package/dist/csrf.d.ts +30 -0
- package/dist/csrfConfig.d.ts +32 -0
- package/dist/db.d.ts +12 -0
- package/dist/dbHealthCheck.d.ts +6 -0
- package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
- package/dist/emailRegistry.d.ts +139 -0
- package/dist/env.d.ts +38 -0
- package/dist/errorFormatterRegistry.d.ts +54 -0
- package/dist/errorTrackerIdentity.d.ts +14 -0
- package/dist/errorTrackerRegistry.d.ts +85 -0
- package/dist/escapeHtml.d.ts +6 -0
- package/dist/eslint/index.d.ts +5 -8
- package/dist/eslint/internal/hasPackage.d.ts +1 -0
- package/dist/eslint/internal/ruleTypes.d.ts +4 -0
- package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
- package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
- package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
- package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
- package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
- package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
- package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
- package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
- package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
- package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
- package/dist/extractToken.d.ts +12 -0
- package/dist/extractTokenFromRequest.d.ts +12 -0
- package/dist/getParams.d.ts +9 -0
- package/dist/hooks/registry.d.ts +12 -0
- package/dist/hooks/types.d.ts +446 -0
- package/dist/httpApiUtils.d.ts +54 -0
- package/dist/index.d.ts +110 -1892
- package/dist/index.js.map +1 -1
- package/dist/lease.d.ts +18 -0
- package/dist/localSocketEnumerationGuard.d.ts +22 -0
- package/dist/localeReloader.d.ts +3 -0
- package/dist/localesRegistry.d.ts +8 -0
- package/dist/localizedNormalizer.d.ts +50 -0
- package/dist/loggerRegistry.d.ts +12 -0
- package/dist/middlewareRegistry.d.ts +45 -0
- package/dist/notifier.d.ts +17 -0
- package/dist/offlineQueue.d.ts +48 -0
- package/dist/oneTimeToken.d.ts +43 -0
- package/dist/pageRouteValidation.d.ts +37 -0
- package/dist/paths.d.ts +35 -0
- package/dist/peerDeps.d.ts +23 -0
- package/dist/portOverrideRegistry.d.ts +3 -0
- package/dist/processUpload.d.ts +44 -0
- package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
- package/dist/rateLimiter.d.ts +90 -0
- package/dist/react/AvatarProvider.d.ts +11 -0
- package/dist/react/Middleware.d.ts +10 -0
- package/dist/react/Router.d.ts +1 -0
- package/dist/react/TranslationProvider.d.ts +22 -0
- package/dist/react/notify.d.ts +12 -0
- package/dist/react/sessionContext.d.ts +31 -0
- package/dist/react/useTheme.d.ts +5 -0
- package/dist/redactedLogKeys.d.ts +22 -0
- package/dist/redis.d.ts +15 -0
- package/dist/redisKeyFormatter.d.ts +7 -0
- package/dist/resolveClientIp.d.ts +51 -0
- package/dist/responseNormalizer.d.ts +33 -0
- package/dist/roomNameFormatterRegistry.d.ts +36 -0
- package/dist/roomSockets.d.ts +23 -0
- package/dist/routedDataQuery.d.ts +7 -0
- package/dist/routedHttpInvocation.d.ts +20 -0
- package/dist/runtimeMapsRegistry.d.ts +18 -0
- package/dist/runtimeTypeValidation.d.ts +13 -0
- package/dist/secretsResolved.d.ts +3 -0
- package/dist/sentrySetup.d.ts +21 -0
- package/dist/serveAvatars.d.ts +5 -0
- package/dist/serviceRoute.d.ts +27 -0
- package/dist/servicesConfigRegistry.d.ts +22 -0
- package/dist/sessionProviderRegistry.d.ts +46 -0
- package/dist/sessionTypes.d.ts +79 -0
- package/dist/sleep.d.ts +1 -0
- package/dist/socketEvents.d.ts +37 -0
- package/dist/socketMiddlewareRegistry.d.ts +20 -0
- package/dist/socketRedisAdapter.d.ts +21 -0
- package/dist/socketState.d.ts +5 -0
- package/dist/socketStatusTypes.d.ts +6 -0
- package/dist/socketTypes.d.ts +34 -0
- package/dist/synchronizedEnvHashes.d.ts +66 -0
- package/dist/testFileConvention.d.ts +11 -0
- package/dist/testResetTargets.d.ts +5 -0
- package/dist/tryCatch.d.ts +1 -0
- package/dist/tryCatchClient.d.ts +1 -0
- package/dist/tryCatchSync.d.ts +1 -0
- package/dist/validateRequest.d.ts +50 -0
- package/docs/app-bootstrap.md +4 -449
- package/docs/config-registry.md +3 -346
- package/docs/csrf-config.md +3 -86
- package/docs/error-tracker-registry.md +3 -192
- package/docs/hooks.md +2 -295
- package/docs/rate-limit-strategy.md +3 -168
- package/docs/redis-adapter.md +5 -110
- package/docs/session-types.md +3 -178
- package/docs/socket-bootstrap.md +5 -385
- package/package.json +2 -2
- package/CLAUDE.md +0 -256
- package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
|
@@ -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;
|