@luckystack/core 0.6.7 → 0.7.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/CHANGELOG.md +148 -107
- package/CLAUDE.md +245 -245
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
- package/dist/chunk-AFG7KKMS.js.map +1 -0
- package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
- package/dist/chunk-J3UOWYT4.js.map +1 -0
- package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
- package/dist/chunk-LTCDQU5Z.js.map +1 -0
- package/dist/chunk-R7IRJFVC.js +67 -0
- package/dist/chunk-R7IRJFVC.js.map +1 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/config-DZCzBNwP.d.ts +122 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +31 -0
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +21 -149
- package/dist/index.js +66 -70
- package/dist/index.js.map +1 -1
- package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
- package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
- package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
- package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
- package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +348 -348
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +194 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +6 -2
- package/dist/chunk-3AWP2YEA.js.map +0 -1
- package/dist/chunk-LU3IPFTR.js.map +0 -1
- package/dist/chunk-WFABOO6J.js.map +0 -1
- /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
- /package/dist/{sentrySetup-4NTWLN2A.js.map → projectConfig-DBLSQ3NY.js.map} +0 -0
package/docs/socket-bootstrap.md
CHANGED
|
@@ -1,366 +1,366 @@
|
|
|
1
|
-
# Socket Bootstrap
|
|
2
|
-
|
|
3
|
-
> Deep specs for socket transport bootstrap, token extraction, CORS check, offline queue, CSRF, cookies, service-route helpers and event constants. Source: `packages/core/src/`. Bijgewerkt: 2026-05-20.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This topic covers the wiring around the LuckyStack socket transport:
|
|
8
|
-
|
|
9
|
-
- **Server-side socket slot** — `setIoInstance` / `getIoInstance` so framework code can broadcast.
|
|
10
|
-
- **Client-side socket singleton** — live binding `socket`, `setSocket`, `waitForSocket`, `incrementResponseIndex`.
|
|
11
|
-
- **Token extraction** — pull the session token from a Socket.io handshake (`extractTokenFromSocket`) or a Node `IncomingMessage` (`extractTokenFromRequest`).
|
|
12
|
-
- **Origin check** — `allowedOrigin` for CORS-style allow-listing, with `corsRejected` hook fan-out.
|
|
13
|
-
- **Offline queue** — `enqueueApiRequest`, `enqueueSyncRequest`, flushers, and per-item `dropPolicy` overrides.
|
|
14
|
-
- **CSRF + httpFetch** — lazy `/auth/csrf` token cache + drop-in fetch wrapper.
|
|
15
|
-
- **Wire-protocol constants** — `socketEventNames` map + per-prefix event-name builders.
|
|
16
|
-
- **Route parsers** — `parseServiceRouteName`, `parseTransportRouteName`.
|
|
17
|
-
- **Cookie helpers** — `getCookieValue`, `hasCookie`.
|
|
18
|
-
- **HTTP helpers** — `inferHttpMethod`, `getEffectiveHttpMethod`, `isMethodAllowed`.
|
|
19
|
-
|
|
20
|
-
Token extraction respects `projectConfig.session.basedToken`: when `true` (token-mode) the framework prefers `socket.handshake.auth.token` / `Authorization: Bearer ...` with the cookie as fallback. When `false` (cookie-mode) the configured session cookie is read; the bearer / handshake-auth fallback is IGNORED by default (CORE-O10 — a stolen token replayed through a header can't bypass the cookie/CSRF model) unless `projectConfig.http.acceptBearerInCookieMode` is set `true` to restore the legacy cookie-then-bearer fallback.
|
|
21
|
-
|
|
22
|
-
## API Reference — IO Instance
|
|
23
|
-
|
|
24
|
-
### `setIoInstance(io: SocketIOServer | null): void`
|
|
25
|
-
|
|
26
|
-
**Signature:**
|
|
27
|
-
```typescript
|
|
28
|
-
export const setIoInstance = (io: SocketIOServer | null): void
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**Behavior:** Stores the running Socket.io server in a module-level slot. Framework code (`@luckystack/sync`, presence package, etc.) reads via `getIoInstance()` for broadcast paths.
|
|
32
|
-
|
|
33
|
-
### `getIoInstance(): SocketIOServer | null`
|
|
34
|
-
|
|
35
|
-
Returns the registered instance or `null` when no server has been set. Callers must null-check.
|
|
36
|
-
|
|
37
|
-
## API Reference — Client Socket Singleton
|
|
38
|
-
|
|
39
|
-
### `socket: Socket | null`
|
|
40
|
-
|
|
41
|
-
A live `export let` binding — importers always see the current value after `setSocket(...)` replaces it.
|
|
42
|
-
|
|
43
|
-
### `setSocket(next: Socket | null): void`
|
|
44
|
-
|
|
45
|
-
Stores the project's socket.io-client instance. Called from `src/_sockets/socketInitializer.ts`.
|
|
46
|
-
|
|
47
|
-
### `incrementResponseIndex(): number`
|
|
48
|
-
|
|
49
|
-
**Returns:** The next monotonic response index (used to build per-request reply event names like `apiResponse-42`).
|
|
50
|
-
|
|
51
|
-
**Behavior:**
|
|
52
|
-
- Module-scoped counter, increments before returning so the first call returns `1`.
|
|
53
|
-
- Used by `apiRequest` / `syncRequest` clients to disambiguate concurrent requests on the same socket.
|
|
54
|
-
|
|
55
|
-
### `waitForSocket(): Promise<Socket | null>`
|
|
56
|
-
|
|
57
|
-
**Behavior:**
|
|
58
|
-
- Polls every 10 ms until `socket !== null` or 500 iterations elapse (~5 seconds).
|
|
59
|
-
- Returns the socket on success, `null` on timeout.
|
|
60
|
-
|
|
61
|
-
**Example:**
|
|
62
|
-
```typescript
|
|
63
|
-
import { waitForSocket } from '@luckystack/core/client';
|
|
64
|
-
|
|
65
|
-
const s = await waitForSocket();
|
|
66
|
-
if (!s) throw new Error('socket never connected');
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## API Reference — Custom Socket Middlewares
|
|
70
|
-
|
|
71
|
-
The framework lets consumers wedge their own Socket.io `io.use(...)` middlewares into the bootstrap without forking `loadSocket.ts`. Registered middlewares run in registration order, before any `connect` handler — same contract as a direct `io.use(...)` call.
|
|
72
|
-
|
|
73
|
-
### `registerSocketMiddleware(mw: SocketMiddleware): void`
|
|
74
|
-
|
|
75
|
-
**Signature:**
|
|
76
|
-
```typescript
|
|
77
|
-
export type SocketMiddleware = (socket: Socket, next: (err?: Error) => void) => void;
|
|
78
|
-
export const registerSocketMiddleware = (mw: SocketMiddleware): void
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Behavior:** Appends the middleware to a module-level list. `@luckystack/server`'s `loadSocket` calls `applySocketMiddlewares(io)` after constructing the `SocketIOServer` and before attaching the `connect` handler, so every registered middleware runs on the handshake of every incoming socket.
|
|
82
|
-
|
|
83
|
-
**Example — license-key gate:**
|
|
84
|
-
```typescript
|
|
85
|
-
import { registerSocketMiddleware } from '@luckystack/core';
|
|
86
|
-
|
|
87
|
-
registerSocketMiddleware((socket, next) => {
|
|
88
|
-
const licenseKey = socket.handshake.auth.licenseKey;
|
|
89
|
-
if (typeof licenseKey !== 'string' || !isValidLicense(licenseKey)) {
|
|
90
|
-
return next(new Error('invalid license'));
|
|
91
|
-
}
|
|
92
|
-
next();
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
**Example — observability tag (Datadog APM):**
|
|
97
|
-
```typescript
|
|
98
|
-
import { registerSocketMiddleware } from '@luckystack/core';
|
|
99
|
-
import tracer from 'dd-trace';
|
|
100
|
-
|
|
101
|
-
registerSocketMiddleware((socket, next) => {
|
|
102
|
-
const span = tracer.scope().active();
|
|
103
|
-
if (span) span.setTag('socket.id', socket.id);
|
|
104
|
-
next();
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### `getSocketMiddlewares(): readonly SocketMiddleware[]`
|
|
109
|
-
|
|
110
|
-
Read the registered list (frozen view). Useful for diagnostics.
|
|
111
|
-
|
|
112
|
-
### `clearSocketMiddlewares(): void`
|
|
113
|
-
|
|
114
|
-
Drops every registered middleware. Test/hot-reload helper.
|
|
115
|
-
|
|
116
|
-
### `applySocketMiddlewares(io: SocketIOServer): void`
|
|
117
|
-
|
|
118
|
-
Framework-internal — wires every registered middleware via `io.use(...)`. Called by `@luckystack/server`'s `loadSocket`; consumers should not call this directly.
|
|
119
|
-
|
|
120
|
-
## API Reference — Token Extraction
|
|
121
|
-
|
|
122
|
-
### `extractTokenFromSocket(socket: Socket): string | null`
|
|
123
|
-
|
|
124
|
-
**Signature:**
|
|
125
|
-
```typescript
|
|
126
|
-
export const extractTokenFromSocket = (socket: Socket): string | null
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**Behavior:**
|
|
130
|
-
- Reads `socket.handshake.auth.token` (when it is a string) and the configured cookie (`projectConfig.http.sessionCookieName`) from `socket.handshake.headers.cookie`.
|
|
131
|
-
- When `session.basedToken === true`: returns the auth-token first, falls back to the cookie.
|
|
132
|
-
- When `session.basedToken === false`: returns the cookie first, falls back to the auth-token.
|
|
133
|
-
|
|
134
|
-
**Returns:** The token string or `null`.
|
|
135
|
-
|
|
136
|
-
### `extractTokenFromRequest(req: IncomingMessage): string | null`
|
|
137
|
-
|
|
138
|
-
**Signature:**
|
|
139
|
-
```typescript
|
|
140
|
-
export const extractTokenFromRequest = (req: IncomingMessage): string | null
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
**Behavior:**
|
|
144
|
-
- Reads `req.headers.authorization` and extracts the bearer (`'Bearer <token>'`).
|
|
145
|
-
- Reads the configured cookie from `req.headers.cookie`.
|
|
146
|
-
- Picks cookie-first vs bearer-first based on `session.basedToken`.
|
|
147
|
-
|
|
148
|
-
## API Reference — Origin Check
|
|
149
|
-
|
|
150
|
-
### `allowedOrigin(origin: string): boolean`
|
|
151
|
-
|
|
152
|
-
**Signature:**
|
|
153
|
-
```typescript
|
|
154
|
-
const allowedOrigin = (origin: string): boolean
|
|
155
|
-
export default allowedOrigin
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
**Behavior (in execution order):**
|
|
159
|
-
1. Normalizes the origin to `scheme://host[:port]`, strips default `:80` / `:443`, lowercases.
|
|
160
|
-
2. Computes the same-origin bind location from `getBindAddress()` and the `SECURE` env var.
|
|
161
|
-
3. When `cors.allowLocalhost` and the origin matches `/^https?:\/\/localhost(:\d+)?$/i` → returns `true`.
|
|
162
|
-
4. When `cors.allowedOrigins` is a function → calls it; if it returns true (or the origin equals the bind location) → returns `true`.
|
|
163
|
-
5. When `cors.allowedOrigins` is a string array → builds a normalized allowed set (bind location + configured list); returns `true` on match.
|
|
164
|
-
6. Otherwise: dispatches the `corsRejected` hook (with origin + normalized origin + allowedOrigins + allowLocalhost) and returns `false`. Also logs via `getLogger().warn` when `logging.devLogs` is on.
|
|
165
|
-
|
|
166
|
-
**Edge cases:**
|
|
167
|
-
- An empty `origin` is treated as not-allowed.
|
|
168
|
-
- The resolver-function form is sync only — async predicates are not supported because Socket.io's CORS check is sync.
|
|
169
|
-
|
|
170
|
-
## API Reference — Offline Queue
|
|
171
|
-
|
|
172
|
-
Per-queue items:
|
|
173
|
-
```typescript
|
|
174
|
-
interface QueueItem {
|
|
175
|
-
id: string;
|
|
176
|
-
key: string;
|
|
177
|
-
run: (socketInstance: Socket) => void;
|
|
178
|
-
createdAt: number;
|
|
179
|
-
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### `isOnline(): boolean`
|
|
184
|
-
Returns `true` in non-browser environments. In browsers, reads `navigator.onLine`.
|
|
185
|
-
|
|
186
|
-
### `enqueueApiRequest(item): boolean` / `enqueueSyncRequest(item): boolean`
|
|
187
|
-
|
|
188
|
-
**Behavior:**
|
|
189
|
-
1. Evicts items older than `offlineQueue.maxAgeMs` from the relevant queue.
|
|
190
|
-
2. If `queue.length < offlineQueue.maxSize`, pushes the new item and returns `true`.
|
|
191
|
-
3. Otherwise applies the effective drop policy (per-item `dropPolicy` overrides the global default):
|
|
192
|
-
- `'drop-oldest'` — shift + push, returns `true` (logs debug).
|
|
193
|
-
- `'drop-newest'` — keep existing queue, returns `false` (logs debug).
|
|
194
|
-
- `'reject'` — returns `false` (logs warn).
|
|
195
|
-
|
|
196
|
-
### `removeApiQueueItem(id)` / `removeSyncQueueItem(id)`
|
|
197
|
-
Removes a single item by `id`. No-op if not found.
|
|
198
|
-
|
|
199
|
-
### `removeApiQueueItemsByKey(key)`
|
|
200
|
-
Removes every API queue item matching `item.key`. Used to coalesce duplicate requests.
|
|
201
|
-
|
|
202
|
-
### `flushApiQueue(canRun, socketInstance)` / `flushSyncQueue(canRun, socketInstance)`
|
|
203
|
-
|
|
204
|
-
**Behavior:**
|
|
205
|
-
- Re-entrancy guard prevents nested flushes for the same queue.
|
|
206
|
-
- Evicts expired items.
|
|
207
|
-
- Drains FIFO while `canRun()` returns `true`, calling each item's `run(socketInstance)`.
|
|
208
|
-
|
|
209
|
-
### `getApiQueueSize(): number` / `getSyncQueueSize(): number`
|
|
210
|
-
|
|
211
|
-
Read-only sizes (after eviction triggers from enqueue/flush; reads alone do not evict).
|
|
212
|
-
|
|
213
|
-
**Example:**
|
|
214
|
-
```typescript
|
|
215
|
-
import { enqueueApiRequest } from '@luckystack/core/client';
|
|
216
|
-
|
|
217
|
-
enqueueApiRequest({
|
|
218
|
-
id: requestId,
|
|
219
|
-
key: `${routeName}:${userId}`,
|
|
220
|
-
createdAt: Date.now(),
|
|
221
|
-
dropPolicy: 'drop-newest',
|
|
222
|
-
run: (s) => s.emit('apiRequest', message),
|
|
223
|
-
});
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## API Reference — CSRF + httpFetch
|
|
227
|
-
|
|
228
|
-
### `getCsrfToken(): Promise<string | null>`
|
|
229
|
-
|
|
230
|
-
**Behavior:**
|
|
231
|
-
- Returns `null` immediately in token-mode sessions (`session.basedToken === true`) — CSRF is unnecessary because cross-origin requests don't auto-attach sessionStorage.
|
|
232
|
-
- Returns the cached token if previously fetched.
|
|
233
|
-
- Otherwise fetches `/auth/csrf` with `credentials: 'include'`, caches and returns `body.csrfToken`.
|
|
234
|
-
- Inflight de-duplication: concurrent callers share the same promise.
|
|
235
|
-
|
|
236
|
-
### `clearCsrfToken(): void`
|
|
237
|
-
|
|
238
|
-
Drops the cached token + any inflight fetch. Call on logout or when a 403 `auth.csrfMismatch` is observed.
|
|
239
|
-
|
|
240
|
-
### `httpFetch(input, init?): Promise<Response>`
|
|
241
|
-
|
|
242
|
-
Drop-in `fetch` replacement that:
|
|
243
|
-
- Forces `credentials: 'include'` unless overridden.
|
|
244
|
-
- Adds `Content-Type: application/json` when a string body is present and no content-type was set.
|
|
245
|
-
- For state-changing methods (`POST`, `PUT`, `PATCH`, `DELETE`) in cookie mode: lazily fetches the CSRF token, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch` response, clears the cache and retries once with a refreshed token.
|
|
246
|
-
|
|
247
|
-
**Example:**
|
|
248
|
-
```typescript
|
|
249
|
-
import { httpFetch } from '@luckystack/core/client';
|
|
250
|
-
|
|
251
|
-
const res = await httpFetch('/api/system/myRoute/v1', {
|
|
252
|
-
method: 'POST',
|
|
253
|
-
body: JSON.stringify({ foo: 1 }),
|
|
254
|
-
});
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## API Reference — Wire Protocol Constants
|
|
258
|
-
|
|
259
|
-
### `socketEventNames`
|
|
260
|
-
|
|
261
|
-
A frozen record of event names:
|
|
262
|
-
|
|
263
|
-
| Key | Value |
|
|
264
|
-
|---|---|
|
|
265
|
-
| `apiRequest` | `'apiRequest'` |
|
|
266
|
-
| `sync` | `'sync'` |
|
|
267
|
-
| `joinRoom` / `leaveRoom` / `getJoinedRooms` | (same literal) |
|
|
268
|
-
| `updateLocation` / `updateSession` / `sessionReplaced` / `logout` | (same literal) |
|
|
269
|
-
| `intentionalDisconnect` / `intentionalReconnect` / `userAfk` / `userBack` | (same literal) |
|
|
270
|
-
| `connect` / `disconnect` / `reconnectAttempt` / `connectError` | underlying socket.io events |
|
|
271
|
-
| `apiResponsePrefix` | `'apiResponse-'` |
|
|
272
|
-
| `apiStreamPrefix` | `'apiStream-'` |
|
|
273
|
-
| `syncResponsePrefix` | `'sync-'` |
|
|
274
|
-
| `syncProgressPrefix` | `'sync-progress-'` |
|
|
275
|
-
| `joinRoomResponsePrefix` / `leaveRoomResponsePrefix` / `getJoinedRoomsResponsePrefix` | (same prefix) |
|
|
276
|
-
|
|
277
|
-
### Event name builders
|
|
278
|
-
|
|
279
|
-
| Function | Behavior |
|
|
280
|
-
|---|---|
|
|
281
|
-
| `buildApiResponseEventName(idx)` | `'apiResponse-<idx>'` |
|
|
282
|
-
| `buildApiStreamEventName(idx)` | `'apiStream-<idx>'` |
|
|
283
|
-
| `buildSyncResponseEventName(idx)` | `'sync-<idx>'` |
|
|
284
|
-
| `buildSyncProgressEventName(idx)` | `'sync-progress-<idx>'` |
|
|
285
|
-
| `buildJoinRoomResponseEventName(idx)` | `'joinRoom-<idx>'` |
|
|
286
|
-
| `buildLeaveRoomResponseEventName(idx)` | `'leaveRoom-<idx>'` |
|
|
287
|
-
| `buildGetJoinedRoomsResponseEventName(idx)` | `'getJoinedRooms-<idx>'` |
|
|
288
|
-
|
|
289
|
-
## API Reference — Service Route Parsers
|
|
290
|
-
|
|
291
|
-
### `parseServiceRouteName(value: string): ServiceRouteParseResult`
|
|
292
|
-
|
|
293
|
-
Validates `<service>/<routeName...>` strings.
|
|
294
|
-
|
|
295
|
-
**Returns:**
|
|
296
|
-
- `{ status: 'success', normalizedRouteName, service, routeName }` — when at least two non-empty segments are present.
|
|
297
|
-
- `{ status: 'error', reason }` — for empty input, empty segments, or fewer than two segments.
|
|
298
|
-
|
|
299
|
-
Normalization rules: trim, replace `\` with `/`, strip leading/trailing slashes.
|
|
300
|
-
|
|
301
|
-
### `parseTransportRouteName({ value, prefix })`
|
|
302
|
-
|
|
303
|
-
Validates a full transport route like `'api/system/foo/v1'`.
|
|
304
|
-
|
|
305
|
-
**Parameters:**
|
|
306
|
-
| Name | Type | Required | Description |
|
|
307
|
-
|---|---|---|---|
|
|
308
|
-
| `value` | `string` | yes | Raw route string. |
|
|
309
|
-
| `prefix` | `'api' \| 'sync'` | yes | Determines whether the leading `'api'`/`'sync'` segment is stripped. |
|
|
310
|
-
|
|
311
|
-
**Returns:**
|
|
312
|
-
- `{ status: 'success', normalizedFullName, version, serviceRoute }` — when the trailing segment matches `/^v\d+$/` and the leading service/route part parses cleanly.
|
|
313
|
-
- `{ status: 'error', reason }` — on length / version / segment violations.
|
|
314
|
-
|
|
315
|
-
## API Reference — Cookies
|
|
316
|
-
|
|
317
|
-
### `getCookieValue(cookieHeader: string | undefined, cookieName: string): string | null`
|
|
318
|
-
|
|
319
|
-
Regex-escapes `cookieName`, searches for `(?:^|;\s*)<name>=<value>` in the header, URL-decodes the captured value (falls back to raw on decode failure). Returns `null` when input is missing or no match.
|
|
320
|
-
|
|
321
|
-
### `hasCookie(cookieHeader, cookieName): boolean`
|
|
322
|
-
|
|
323
|
-
Convenience wrapper — true when `getCookieValue` returns a non-null value.
|
|
324
|
-
|
|
325
|
-
## API Reference — HTTP Method Inference
|
|
326
|
-
|
|
327
|
-
### `inferHttpMethod(apiName: string): HttpMethod`
|
|
328
|
-
|
|
329
|
-
Naming-convention heuristic (used as fallback when no `apiMethodMap` is registered):
|
|
330
|
-
- starts with `get` / `fetch` / `list` → `GET`
|
|
331
|
-
- starts with `delete` / `remove` → `DELETE`
|
|
332
|
-
- starts with `update` / `edit` / `patch` → `PUT`
|
|
333
|
-
- otherwise → `POST`
|
|
334
|
-
|
|
335
|
-
Strips a trailing `v\d+` segment before applying the rule, so `api/examples/getUser/v1` resolves to `'GET'`.
|
|
336
|
-
|
|
337
|
-
### `getEffectiveHttpMethod(apiConfig, apiName)`
|
|
338
|
-
|
|
339
|
-
Prefers an explicit `apiConfig.httpMethod`; otherwise calls `inferHttpMethod(apiName)`.
|
|
340
|
-
|
|
341
|
-
### `isMethodAllowed(requestMethod, allowedMethod): boolean`
|
|
342
|
-
|
|
343
|
-
Returns `true` when the request method matches the allowed method or is `'OPTIONS'` (CORS preflight passthrough).
|
|
344
|
-
|
|
345
|
-
## Hooks dispatched
|
|
346
|
-
|
|
347
|
-
| Hook name | Payload type | When |
|
|
348
|
-
|---|---|---|
|
|
349
|
-
| `corsRejected` | `CorsRejectedPayload` | `allowedOrigin` is about to return `false`. |
|
|
350
|
-
|
|
351
|
-
## Config keys consumed
|
|
352
|
-
|
|
353
|
-
| Key | Where |
|
|
354
|
-
|---|---|
|
|
355
|
-
| `session.basedToken` | token extractors, `httpFetch`, `getCsrfToken` |
|
|
356
|
-
| `http.sessionCookieName` | token extractors |
|
|
357
|
-
| `http.cors.allowedOrigins`, `http.cors.allowLocalhost` | `allowedOrigin` |
|
|
358
|
-
| `offlineQueue.maxSize`, `maxAgeMs`, `dropPolicy` | offline queue |
|
|
359
|
-
| `logging.devLogs` | `allowedOrigin` warn-logs |
|
|
360
|
-
|
|
361
|
-
## Related
|
|
362
|
-
|
|
363
|
-
- Function INDEX: `packages/core/CLAUDE.md`
|
|
364
|
-
- Architecture: `docs/ARCHITECTURE_SOCKET.md`, `docs/ARCHITECTURE_API.md`, `docs/ARCHITECTURE_SYNC.md`, `docs/ARCHITECTURE_AUTH.md`
|
|
365
|
-
- README: `packages/core/README.md`
|
|
366
|
-
- Source: `packages/core/src/socketTypes.ts`, `socketState.ts`, `socketEvents.ts`, `extractToken.ts`, `extractTokenFromRequest.ts`, `checkOrigin.ts`, `offlineQueue.ts`, `csrf.ts`, `cookies.ts`, `httpApiUtils.ts`, `serviceRoute.ts`
|
|
1
|
+
# Socket Bootstrap
|
|
2
|
+
|
|
3
|
+
> Deep specs for socket transport bootstrap, token extraction, CORS check, offline queue, CSRF, cookies, service-route helpers and event constants. Source: `packages/core/src/`. Bijgewerkt: 2026-05-20.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This topic covers the wiring around the LuckyStack socket transport:
|
|
8
|
+
|
|
9
|
+
- **Server-side socket slot** — `setIoInstance` / `getIoInstance` so framework code can broadcast.
|
|
10
|
+
- **Client-side socket singleton** — live binding `socket`, `setSocket`, `waitForSocket`, `incrementResponseIndex`.
|
|
11
|
+
- **Token extraction** — pull the session token from a Socket.io handshake (`extractTokenFromSocket`) or a Node `IncomingMessage` (`extractTokenFromRequest`).
|
|
12
|
+
- **Origin check** — `allowedOrigin` for CORS-style allow-listing, with `corsRejected` hook fan-out.
|
|
13
|
+
- **Offline queue** — `enqueueApiRequest`, `enqueueSyncRequest`, flushers, and per-item `dropPolicy` overrides.
|
|
14
|
+
- **CSRF + httpFetch** — lazy `/auth/csrf` token cache + drop-in fetch wrapper.
|
|
15
|
+
- **Wire-protocol constants** — `socketEventNames` map + per-prefix event-name builders.
|
|
16
|
+
- **Route parsers** — `parseServiceRouteName`, `parseTransportRouteName`.
|
|
17
|
+
- **Cookie helpers** — `getCookieValue`, `hasCookie`.
|
|
18
|
+
- **HTTP helpers** — `inferHttpMethod`, `getEffectiveHttpMethod`, `isMethodAllowed`.
|
|
19
|
+
|
|
20
|
+
Token extraction respects `projectConfig.session.basedToken`: when `true` (token-mode) the framework prefers `socket.handshake.auth.token` / `Authorization: Bearer ...` with the cookie as fallback. When `false` (cookie-mode) the configured session cookie is read; the bearer / handshake-auth fallback is IGNORED by default (CORE-O10 — a stolen token replayed through a header can't bypass the cookie/CSRF model) unless `projectConfig.http.acceptBearerInCookieMode` is set `true` to restore the legacy cookie-then-bearer fallback.
|
|
21
|
+
|
|
22
|
+
## API Reference — IO Instance
|
|
23
|
+
|
|
24
|
+
### `setIoInstance(io: SocketIOServer | null): void`
|
|
25
|
+
|
|
26
|
+
**Signature:**
|
|
27
|
+
```typescript
|
|
28
|
+
export const setIoInstance = (io: SocketIOServer | null): void
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Behavior:** Stores the running Socket.io server in a module-level slot. Framework code (`@luckystack/sync`, presence package, etc.) reads via `getIoInstance()` for broadcast paths.
|
|
32
|
+
|
|
33
|
+
### `getIoInstance(): SocketIOServer | null`
|
|
34
|
+
|
|
35
|
+
Returns the registered instance or `null` when no server has been set. Callers must null-check.
|
|
36
|
+
|
|
37
|
+
## API Reference — Client Socket Singleton
|
|
38
|
+
|
|
39
|
+
### `socket: Socket | null`
|
|
40
|
+
|
|
41
|
+
A live `export let` binding — importers always see the current value after `setSocket(...)` replaces it.
|
|
42
|
+
|
|
43
|
+
### `setSocket(next: Socket | null): void`
|
|
44
|
+
|
|
45
|
+
Stores the project's socket.io-client instance. Called from `src/_sockets/socketInitializer.ts`.
|
|
46
|
+
|
|
47
|
+
### `incrementResponseIndex(): number`
|
|
48
|
+
|
|
49
|
+
**Returns:** The next monotonic response index (used to build per-request reply event names like `apiResponse-42`).
|
|
50
|
+
|
|
51
|
+
**Behavior:**
|
|
52
|
+
- Module-scoped counter, increments before returning so the first call returns `1`.
|
|
53
|
+
- Used by `apiRequest` / `syncRequest` clients to disambiguate concurrent requests on the same socket.
|
|
54
|
+
|
|
55
|
+
### `waitForSocket(): Promise<Socket | null>`
|
|
56
|
+
|
|
57
|
+
**Behavior:**
|
|
58
|
+
- Polls every 10 ms until `socket !== null` or 500 iterations elapse (~5 seconds).
|
|
59
|
+
- Returns the socket on success, `null` on timeout.
|
|
60
|
+
|
|
61
|
+
**Example:**
|
|
62
|
+
```typescript
|
|
63
|
+
import { waitForSocket } from '@luckystack/core/client';
|
|
64
|
+
|
|
65
|
+
const s = await waitForSocket();
|
|
66
|
+
if (!s) throw new Error('socket never connected');
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API Reference — Custom Socket Middlewares
|
|
70
|
+
|
|
71
|
+
The framework lets consumers wedge their own Socket.io `io.use(...)` middlewares into the bootstrap without forking `loadSocket.ts`. Registered middlewares run in registration order, before any `connect` handler — same contract as a direct `io.use(...)` call.
|
|
72
|
+
|
|
73
|
+
### `registerSocketMiddleware(mw: SocketMiddleware): void`
|
|
74
|
+
|
|
75
|
+
**Signature:**
|
|
76
|
+
```typescript
|
|
77
|
+
export type SocketMiddleware = (socket: Socket, next: (err?: Error) => void) => void;
|
|
78
|
+
export const registerSocketMiddleware = (mw: SocketMiddleware): void
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Behavior:** Appends the middleware to a module-level list. `@luckystack/server`'s `loadSocket` calls `applySocketMiddlewares(io)` after constructing the `SocketIOServer` and before attaching the `connect` handler, so every registered middleware runs on the handshake of every incoming socket.
|
|
82
|
+
|
|
83
|
+
**Example — license-key gate:**
|
|
84
|
+
```typescript
|
|
85
|
+
import { registerSocketMiddleware } from '@luckystack/core';
|
|
86
|
+
|
|
87
|
+
registerSocketMiddleware((socket, next) => {
|
|
88
|
+
const licenseKey = socket.handshake.auth.licenseKey;
|
|
89
|
+
if (typeof licenseKey !== 'string' || !isValidLicense(licenseKey)) {
|
|
90
|
+
return next(new Error('invalid license'));
|
|
91
|
+
}
|
|
92
|
+
next();
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Example — observability tag (Datadog APM):**
|
|
97
|
+
```typescript
|
|
98
|
+
import { registerSocketMiddleware } from '@luckystack/core';
|
|
99
|
+
import tracer from 'dd-trace';
|
|
100
|
+
|
|
101
|
+
registerSocketMiddleware((socket, next) => {
|
|
102
|
+
const span = tracer.scope().active();
|
|
103
|
+
if (span) span.setTag('socket.id', socket.id);
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `getSocketMiddlewares(): readonly SocketMiddleware[]`
|
|
109
|
+
|
|
110
|
+
Read the registered list (frozen view). Useful for diagnostics.
|
|
111
|
+
|
|
112
|
+
### `clearSocketMiddlewares(): void`
|
|
113
|
+
|
|
114
|
+
Drops every registered middleware. Test/hot-reload helper.
|
|
115
|
+
|
|
116
|
+
### `applySocketMiddlewares(io: SocketIOServer): void`
|
|
117
|
+
|
|
118
|
+
Framework-internal — wires every registered middleware via `io.use(...)`. Called by `@luckystack/server`'s `loadSocket`; consumers should not call this directly.
|
|
119
|
+
|
|
120
|
+
## API Reference — Token Extraction
|
|
121
|
+
|
|
122
|
+
### `extractTokenFromSocket(socket: Socket): string | null`
|
|
123
|
+
|
|
124
|
+
**Signature:**
|
|
125
|
+
```typescript
|
|
126
|
+
export const extractTokenFromSocket = (socket: Socket): string | null
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Behavior:**
|
|
130
|
+
- Reads `socket.handshake.auth.token` (when it is a string) and the configured cookie (`projectConfig.http.sessionCookieName`) from `socket.handshake.headers.cookie`.
|
|
131
|
+
- When `session.basedToken === true`: returns the auth-token first, falls back to the cookie.
|
|
132
|
+
- When `session.basedToken === false`: returns the cookie first, falls back to the auth-token.
|
|
133
|
+
|
|
134
|
+
**Returns:** The token string or `null`.
|
|
135
|
+
|
|
136
|
+
### `extractTokenFromRequest(req: IncomingMessage): string | null`
|
|
137
|
+
|
|
138
|
+
**Signature:**
|
|
139
|
+
```typescript
|
|
140
|
+
export const extractTokenFromRequest = (req: IncomingMessage): string | null
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Behavior:**
|
|
144
|
+
- Reads `req.headers.authorization` and extracts the bearer (`'Bearer <token>'`).
|
|
145
|
+
- Reads the configured cookie from `req.headers.cookie`.
|
|
146
|
+
- Picks cookie-first vs bearer-first based on `session.basedToken`.
|
|
147
|
+
|
|
148
|
+
## API Reference — Origin Check
|
|
149
|
+
|
|
150
|
+
### `allowedOrigin(origin: string): boolean`
|
|
151
|
+
|
|
152
|
+
**Signature:**
|
|
153
|
+
```typescript
|
|
154
|
+
const allowedOrigin = (origin: string): boolean
|
|
155
|
+
export default allowedOrigin
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Behavior (in execution order):**
|
|
159
|
+
1. Normalizes the origin to `scheme://host[:port]`, strips default `:80` / `:443`, lowercases.
|
|
160
|
+
2. Computes the same-origin bind location from `getBindAddress()` and the `SECURE` env var.
|
|
161
|
+
3. When `cors.allowLocalhost` and the origin matches `/^https?:\/\/localhost(:\d+)?$/i` → returns `true`.
|
|
162
|
+
4. When `cors.allowedOrigins` is a function → calls it; if it returns true (or the origin equals the bind location) → returns `true`.
|
|
163
|
+
5. When `cors.allowedOrigins` is a string array → builds a normalized allowed set (bind location + configured list); returns `true` on match.
|
|
164
|
+
6. Otherwise: dispatches the `corsRejected` hook (with origin + normalized origin + allowedOrigins + allowLocalhost) and returns `false`. Also logs via `getLogger().warn` when `logging.devLogs` is on.
|
|
165
|
+
|
|
166
|
+
**Edge cases:**
|
|
167
|
+
- An empty `origin` is treated as not-allowed.
|
|
168
|
+
- The resolver-function form is sync only — async predicates are not supported because Socket.io's CORS check is sync.
|
|
169
|
+
|
|
170
|
+
## API Reference — Offline Queue
|
|
171
|
+
|
|
172
|
+
Per-queue items:
|
|
173
|
+
```typescript
|
|
174
|
+
interface QueueItem {
|
|
175
|
+
id: string;
|
|
176
|
+
key: string;
|
|
177
|
+
run: (socketInstance: Socket) => void;
|
|
178
|
+
createdAt: number;
|
|
179
|
+
dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `isOnline(): boolean`
|
|
184
|
+
Returns `true` in non-browser environments. In browsers, reads `navigator.onLine`.
|
|
185
|
+
|
|
186
|
+
### `enqueueApiRequest(item): boolean` / `enqueueSyncRequest(item): boolean`
|
|
187
|
+
|
|
188
|
+
**Behavior:**
|
|
189
|
+
1. Evicts items older than `offlineQueue.maxAgeMs` from the relevant queue.
|
|
190
|
+
2. If `queue.length < offlineQueue.maxSize`, pushes the new item and returns `true`.
|
|
191
|
+
3. Otherwise applies the effective drop policy (per-item `dropPolicy` overrides the global default):
|
|
192
|
+
- `'drop-oldest'` — shift + push, returns `true` (logs debug).
|
|
193
|
+
- `'drop-newest'` — keep existing queue, returns `false` (logs debug).
|
|
194
|
+
- `'reject'` — returns `false` (logs warn).
|
|
195
|
+
|
|
196
|
+
### `removeApiQueueItem(id)` / `removeSyncQueueItem(id)`
|
|
197
|
+
Removes a single item by `id`. No-op if not found.
|
|
198
|
+
|
|
199
|
+
### `removeApiQueueItemsByKey(key)`
|
|
200
|
+
Removes every API queue item matching `item.key`. Used to coalesce duplicate requests.
|
|
201
|
+
|
|
202
|
+
### `flushApiQueue(canRun, socketInstance)` / `flushSyncQueue(canRun, socketInstance)`
|
|
203
|
+
|
|
204
|
+
**Behavior:**
|
|
205
|
+
- Re-entrancy guard prevents nested flushes for the same queue.
|
|
206
|
+
- Evicts expired items.
|
|
207
|
+
- Drains FIFO while `canRun()` returns `true`, calling each item's `run(socketInstance)`.
|
|
208
|
+
|
|
209
|
+
### `getApiQueueSize(): number` / `getSyncQueueSize(): number`
|
|
210
|
+
|
|
211
|
+
Read-only sizes (after eviction triggers from enqueue/flush; reads alone do not evict).
|
|
212
|
+
|
|
213
|
+
**Example:**
|
|
214
|
+
```typescript
|
|
215
|
+
import { enqueueApiRequest } from '@luckystack/core/client';
|
|
216
|
+
|
|
217
|
+
enqueueApiRequest({
|
|
218
|
+
id: requestId,
|
|
219
|
+
key: `${routeName}:${userId}`,
|
|
220
|
+
createdAt: Date.now(),
|
|
221
|
+
dropPolicy: 'drop-newest',
|
|
222
|
+
run: (s) => s.emit('apiRequest', message),
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## API Reference — CSRF + httpFetch
|
|
227
|
+
|
|
228
|
+
### `getCsrfToken(): Promise<string | null>`
|
|
229
|
+
|
|
230
|
+
**Behavior:**
|
|
231
|
+
- Returns `null` immediately in token-mode sessions (`session.basedToken === true`) — CSRF is unnecessary because cross-origin requests don't auto-attach sessionStorage.
|
|
232
|
+
- Returns the cached token if previously fetched.
|
|
233
|
+
- Otherwise fetches `/auth/csrf` with `credentials: 'include'`, caches and returns `body.csrfToken`.
|
|
234
|
+
- Inflight de-duplication: concurrent callers share the same promise.
|
|
235
|
+
|
|
236
|
+
### `clearCsrfToken(): void`
|
|
237
|
+
|
|
238
|
+
Drops the cached token + any inflight fetch. Call on logout or when a 403 `auth.csrfMismatch` is observed.
|
|
239
|
+
|
|
240
|
+
### `httpFetch(input, init?): Promise<Response>`
|
|
241
|
+
|
|
242
|
+
Drop-in `fetch` replacement that:
|
|
243
|
+
- Forces `credentials: 'include'` unless overridden.
|
|
244
|
+
- Adds `Content-Type: application/json` when a string body is present and no content-type was set.
|
|
245
|
+
- For state-changing methods (`POST`, `PUT`, `PATCH`, `DELETE`) in cookie mode: lazily fetches the CSRF token, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch` response, clears the cache and retries once with a refreshed token.
|
|
246
|
+
|
|
247
|
+
**Example:**
|
|
248
|
+
```typescript
|
|
249
|
+
import { httpFetch } from '@luckystack/core/client';
|
|
250
|
+
|
|
251
|
+
const res = await httpFetch('/api/system/myRoute/v1', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
body: JSON.stringify({ foo: 1 }),
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## API Reference — Wire Protocol Constants
|
|
258
|
+
|
|
259
|
+
### `socketEventNames`
|
|
260
|
+
|
|
261
|
+
A frozen record of event names:
|
|
262
|
+
|
|
263
|
+
| Key | Value |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `apiRequest` | `'apiRequest'` |
|
|
266
|
+
| `sync` | `'sync'` |
|
|
267
|
+
| `joinRoom` / `leaveRoom` / `getJoinedRooms` | (same literal) |
|
|
268
|
+
| `updateLocation` / `updateSession` / `sessionReplaced` / `logout` | (same literal) |
|
|
269
|
+
| `intentionalDisconnect` / `intentionalReconnect` / `userAfk` / `userBack` | (same literal) |
|
|
270
|
+
| `connect` / `disconnect` / `reconnectAttempt` / `connectError` | underlying socket.io events |
|
|
271
|
+
| `apiResponsePrefix` | `'apiResponse-'` |
|
|
272
|
+
| `apiStreamPrefix` | `'apiStream-'` |
|
|
273
|
+
| `syncResponsePrefix` | `'sync-'` |
|
|
274
|
+
| `syncProgressPrefix` | `'sync-progress-'` |
|
|
275
|
+
| `joinRoomResponsePrefix` / `leaveRoomResponsePrefix` / `getJoinedRoomsResponsePrefix` | (same prefix) |
|
|
276
|
+
|
|
277
|
+
### Event name builders
|
|
278
|
+
|
|
279
|
+
| Function | Behavior |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `buildApiResponseEventName(idx)` | `'apiResponse-<idx>'` |
|
|
282
|
+
| `buildApiStreamEventName(idx)` | `'apiStream-<idx>'` |
|
|
283
|
+
| `buildSyncResponseEventName(idx)` | `'sync-<idx>'` |
|
|
284
|
+
| `buildSyncProgressEventName(idx)` | `'sync-progress-<idx>'` |
|
|
285
|
+
| `buildJoinRoomResponseEventName(idx)` | `'joinRoom-<idx>'` |
|
|
286
|
+
| `buildLeaveRoomResponseEventName(idx)` | `'leaveRoom-<idx>'` |
|
|
287
|
+
| `buildGetJoinedRoomsResponseEventName(idx)` | `'getJoinedRooms-<idx>'` |
|
|
288
|
+
|
|
289
|
+
## API Reference — Service Route Parsers
|
|
290
|
+
|
|
291
|
+
### `parseServiceRouteName(value: string): ServiceRouteParseResult`
|
|
292
|
+
|
|
293
|
+
Validates `<service>/<routeName...>` strings.
|
|
294
|
+
|
|
295
|
+
**Returns:**
|
|
296
|
+
- `{ status: 'success', normalizedRouteName, service, routeName }` — when at least two non-empty segments are present.
|
|
297
|
+
- `{ status: 'error', reason }` — for empty input, empty segments, or fewer than two segments.
|
|
298
|
+
|
|
299
|
+
Normalization rules: trim, replace `\` with `/`, strip leading/trailing slashes.
|
|
300
|
+
|
|
301
|
+
### `parseTransportRouteName({ value, prefix })`
|
|
302
|
+
|
|
303
|
+
Validates a full transport route like `'api/system/foo/v1'`.
|
|
304
|
+
|
|
305
|
+
**Parameters:**
|
|
306
|
+
| Name | Type | Required | Description |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| `value` | `string` | yes | Raw route string. |
|
|
309
|
+
| `prefix` | `'api' \| 'sync'` | yes | Determines whether the leading `'api'`/`'sync'` segment is stripped. |
|
|
310
|
+
|
|
311
|
+
**Returns:**
|
|
312
|
+
- `{ status: 'success', normalizedFullName, version, serviceRoute }` — when the trailing segment matches `/^v\d+$/` and the leading service/route part parses cleanly.
|
|
313
|
+
- `{ status: 'error', reason }` — on length / version / segment violations.
|
|
314
|
+
|
|
315
|
+
## API Reference — Cookies
|
|
316
|
+
|
|
317
|
+
### `getCookieValue(cookieHeader: string | undefined, cookieName: string): string | null`
|
|
318
|
+
|
|
319
|
+
Regex-escapes `cookieName`, searches for `(?:^|;\s*)<name>=<value>` in the header, URL-decodes the captured value (falls back to raw on decode failure). Returns `null` when input is missing or no match.
|
|
320
|
+
|
|
321
|
+
### `hasCookie(cookieHeader, cookieName): boolean`
|
|
322
|
+
|
|
323
|
+
Convenience wrapper — true when `getCookieValue` returns a non-null value.
|
|
324
|
+
|
|
325
|
+
## API Reference — HTTP Method Inference
|
|
326
|
+
|
|
327
|
+
### `inferHttpMethod(apiName: string): HttpMethod`
|
|
328
|
+
|
|
329
|
+
Naming-convention heuristic (used as fallback when no `apiMethodMap` is registered):
|
|
330
|
+
- starts with `get` / `fetch` / `list` → `GET`
|
|
331
|
+
- starts with `delete` / `remove` → `DELETE`
|
|
332
|
+
- starts with `update` / `edit` / `patch` → `PUT`
|
|
333
|
+
- otherwise → `POST`
|
|
334
|
+
|
|
335
|
+
Strips a trailing `v\d+` segment before applying the rule, so `api/examples/getUser/v1` resolves to `'GET'`.
|
|
336
|
+
|
|
337
|
+
### `getEffectiveHttpMethod(apiConfig, apiName)`
|
|
338
|
+
|
|
339
|
+
Prefers an explicit `apiConfig.httpMethod`; otherwise calls `inferHttpMethod(apiName)`.
|
|
340
|
+
|
|
341
|
+
### `isMethodAllowed(requestMethod, allowedMethod): boolean`
|
|
342
|
+
|
|
343
|
+
Returns `true` when the request method matches the allowed method or is `'OPTIONS'` (CORS preflight passthrough).
|
|
344
|
+
|
|
345
|
+
## Hooks dispatched
|
|
346
|
+
|
|
347
|
+
| Hook name | Payload type | When |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| `corsRejected` | `CorsRejectedPayload` | `allowedOrigin` is about to return `false`. |
|
|
350
|
+
|
|
351
|
+
## Config keys consumed
|
|
352
|
+
|
|
353
|
+
| Key | Where |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `session.basedToken` | token extractors, `httpFetch`, `getCsrfToken` |
|
|
356
|
+
| `http.sessionCookieName` | token extractors |
|
|
357
|
+
| `http.cors.allowedOrigins`, `http.cors.allowLocalhost` | `allowedOrigin` |
|
|
358
|
+
| `offlineQueue.maxSize`, `maxAgeMs`, `dropPolicy` | offline queue |
|
|
359
|
+
| `logging.devLogs` | `allowedOrigin` warn-logs |
|
|
360
|
+
|
|
361
|
+
## Related
|
|
362
|
+
|
|
363
|
+
- Function INDEX: `packages/core/CLAUDE.md`
|
|
364
|
+
- Architecture: `docs/ARCHITECTURE_SOCKET.md`, `docs/ARCHITECTURE_API.md`, `docs/ARCHITECTURE_SYNC.md`, `docs/ARCHITECTURE_AUTH.md`
|
|
365
|
+
- README: `packages/core/README.md`
|
|
366
|
+
- Source: `packages/core/src/socketTypes.ts`, `socketState.ts`, `socketEvents.ts`, `extractToken.ts`, `extractTokenFromRequest.ts`, `checkOrigin.ts`, `offlineQueue.ts`, `csrf.ts`, `cookies.ts`, `httpApiUtils.ts`, `serviceRoute.ts`
|