@luckystack/core 0.1.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.
@@ -0,0 +1,180 @@
1
+ # Session Types
2
+
3
+ > Deep specs for the foundational session/auth types plus `validateRequest`, `isFalsy`, and `validateInputByType`. Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`. Bijgewerkt: 2026-05-20.
4
+
5
+ ## Overview
6
+
7
+ `BaseSessionLayout` is the structural contract every project session type extends. It lives in `@luckystack/core` (not `@luckystack/login`) so `validateRequest` can consume it without creating a circular dep: login depends on core for prisma/redis, so the session shape has to live in the lower layer. `@luckystack/login` re-exports the type for ergonomics — existing `import type { BaseSessionLayout } from '@luckystack/login'` paths keep working.
8
+
9
+ `AuthProps` defines the auth gate that every API/sync handler exports (`login: true/false` + an optional `additional[]` predicate list). `validateRequest({ auth, user })` is the runtime check that turns those declarations into a `ValidationResult`.
10
+
11
+ `validateInputByType` is the runtime input validator used by the api/sync handlers in dev. It lazy-loads `@luckystack/devkit` (the heavy TS compiler resolver) only when running in non-production. In production it short-circuits to success because the prod esbuild bundle marks devkit as external and the validator should not need TS to validate request bodies at request time.
12
+
13
+ ## Types
14
+
15
+ ```typescript
16
+ export interface SessionLocation {
17
+ pathName: string;
18
+ searchParams?: Record<string, string>;
19
+ }
20
+
21
+ export interface BaseSessionLayout {
22
+ id: string;
23
+ token: string;
24
+ email?: string | null;
25
+ name?: string | null;
26
+ avatar?: string | null;
27
+ avatarFallback?: string | null;
28
+ admin?: boolean | null;
29
+ language?: string | null;
30
+ location?: SessionLocation;
31
+ roomCodes?: string[];
32
+ csrfToken?: string;
33
+ lastLogin?: Date | string | null;
34
+ previousLogin?: Date | string | null;
35
+ }
36
+
37
+ export interface AuthProps {
38
+ login: boolean;
39
+ additional?: {
40
+ key: keyof BaseSessionLayout;
41
+ value?: unknown;
42
+ type?: 'string' | 'number' | 'boolean';
43
+ nullish?: boolean;
44
+ mustBeFalsy?: boolean;
45
+ }[];
46
+ }
47
+
48
+ export interface ValidationResult {
49
+ status: 'success' | 'error';
50
+ errorCode?: string;
51
+ errorParams?: { key: string; value: string | number | boolean }[];
52
+ httpStatus?: number;
53
+ }
54
+ ```
55
+
56
+ ### Extending `BaseSessionLayout`
57
+
58
+ Projects extend the type in their own `config.ts`:
59
+
60
+ ```typescript
61
+ import type { BaseSessionLayout } from '@luckystack/core';
62
+
63
+ export interface SessionLayout extends BaseSessionLayout {
64
+ organizationId?: string;
65
+ role?: 'member' | 'admin' | 'owner';
66
+ }
67
+ ```
68
+
69
+ The framework's `validateRequest` only inspects keys it knows from `BaseSessionLayout`; project-specific keys are checked via `additional[]`.
70
+
71
+ ## API Reference
72
+
73
+ ### `isFalsy(value: unknown): boolean`
74
+
75
+ **Signature:**
76
+ ```typescript
77
+ export const isFalsy = (value: unknown): boolean
78
+ ```
79
+
80
+ **Behavior:** Returns `true` for `false`, `0`, `0n`, `''`, `null`, `undefined`, or `NaN`. Everything else is truthy.
81
+
82
+ **Note:** This is a stricter "falsy" than JavaScript's coercion-based check because it ALSO treats `NaN` as falsy. Used inside `validateRequest` for the `mustBeFalsy` predicate.
83
+
84
+ ### `validateRequest({ auth, user }): ValidationResult`
85
+
86
+ **Signature:**
87
+ ```typescript
88
+ export const validateRequest = ({
89
+ auth,
90
+ user,
91
+ }: {
92
+ auth: AuthProps;
93
+ user: BaseSessionLayout;
94
+ }): ValidationResult
95
+ ```
96
+
97
+ **Behavior (in order):**
98
+ 1. If `auth.additional` is undefined → returns `{ status: 'success' }` immediately. (The `login` flag is enforced by the surrounding handler, not by this function.)
99
+ 2. For each `condition` in `auth.additional`:
100
+ - If `condition.key` is not in `user` → returns `{ status: 'error', errorCode: 'auth.invalidCondition', errorParams: [{ key: 'key', value: condition.key }], httpStatus: 500 }`. This is a developer setup error, not a runtime auth failure.
101
+ - Reads `val = user[condition.key]`. Computes `isNullish = val === null || val === undefined`.
102
+ - `nullish: true` requires `isNullish`; `nullish: false` requires NOT `isNullish`. Mismatch → forbid.
103
+ - `type: 'string' | 'number' | 'boolean'` requires `typeof val === <type>` (skipped when `val` is nullish). Mismatch → forbid.
104
+ - `'value' in condition` requires strict equality `val === condition.value`. The membership check distinguishes "no exact-value constraint" from `value: undefined` (the latter explicitly demands `val === undefined`).
105
+ - `mustBeFalsy: true` requires `isFalsy(val)`; `mustBeFalsy: false` requires `!isFalsy(val)`. Mismatch → forbid.
106
+ 3. After all conditions pass → returns `{ status: 'success' }`.
107
+
108
+ **Forbid shape:**
109
+ ```typescript
110
+ { status: 'error', errorCode: 'auth.forbidden', errorParams: [{ key: 'key', value: <key> }], httpStatus: 403 }
111
+ ```
112
+
113
+ **Constraint AND-semantics:** Multiple fields on a single `additional` entry are AND'd (every specified field must pass). Entries across the array are also AND'd.
114
+
115
+ **Example — admin-only endpoint:**
116
+ ```typescript
117
+ export const auth: AuthProps = {
118
+ login: true,
119
+ additional: [{ key: 'admin', value: true, type: 'boolean' }],
120
+ };
121
+ ```
122
+
123
+ **Example — verified email required:**
124
+ ```typescript
125
+ export const auth: AuthProps = {
126
+ login: true,
127
+ additional: [{ key: 'email', nullish: false, type: 'string' }],
128
+ };
129
+ ```
130
+
131
+ ### `validateInputByType({ typeText, value, rootKey, filePath? }): Promise<ValidationResult>`
132
+
133
+ **Signature:**
134
+ ```typescript
135
+ type ValidationResult =
136
+ | { status: 'success' }
137
+ | { status: 'error'; message: string };
138
+
139
+ export const validateInputByType = async ({
140
+ typeText,
141
+ value,
142
+ rootKey,
143
+ filePath,
144
+ }: {
145
+ typeText?: string;
146
+ value: unknown;
147
+ rootKey: string;
148
+ filePath?: string;
149
+ }): Promise<ValidationResult>
150
+ ```
151
+
152
+ **Behavior:**
153
+ 1. When `typeText` is empty, whitespace, or literal `'any'` → returns `{ status: 'success' }`.
154
+ 2. When `process.env.NODE_ENV === 'production'` → returns `{ status: 'success' }` (avoids loading the TS compiler in prod).
155
+ 3. Otherwise lazy-imports `@luckystack/devkit` via an indirect module ID (`const devkitModuleId: string = '@luckystack/devkit'; await import(devkitModuleId)`) so `tsc` doesn't try to resolve devkit at build time (devkit depends on core — a literal import would be a build-time circular dep).
156
+ 4. Calls `devkit.resolveRuntimeTypeText({ typeText, filePath })`. On `status: 'error'` returns `{ status: 'error', message: '<rootKey>: <message>' }`.
157
+ 5. Otherwise calls the internal `validateType(resolvedType.typeText, value, rootKey)` recursive validator.
158
+
159
+ **Supported type forms (validateType):**
160
+ - Primitives: `string`, `number`, `boolean`, `true`, `false`, `null`, `undefined`, `Date` (accepts ISO string or `Date` instance).
161
+ - String/number literal types (single-quoted or double-quoted).
162
+ - Unions (`A | B`) — succeeds on first matching branch.
163
+ - Intersections (`A & B`) — requires all branches to succeed.
164
+ - Arrays (`T[]`) — checks every element.
165
+ - `Record<K, V>` — accepts any non-array object.
166
+ - Object literals `{ key?: T; key2: T2; [index: keyof IndexKey]: T }` — checks each named field, applies index signatures for unknown keys.
167
+ - `any` / `unknown` — always succeeds.
168
+ - Unresolved type aliases or generics return `{ status: 'error', message: '<path>: unresolved type <name>' }`.
169
+
170
+ **Edge cases:**
171
+ - The `__RUNTIME_UNRESOLVED__::<message>` sentinel from devkit surfaces as `{ status: 'error', message: '<path>: <message>' }` so a missing project type doesn't crash the request.
172
+ - Object validation rejects unknown keys when there is no matching index signature.
173
+ - Index-signature keys can be `string`, `number`, string-literal unions, or literal types; `number` keys require the key to match `/^-?\d+(\.\d+)?$/`.
174
+
175
+ ## Related
176
+
177
+ - Function INDEX: `packages/core/CLAUDE.md`
178
+ - Architecture: `docs/ARCHITECTURE_SESSION.md`, `docs/ARCHITECTURE_AUTH.md`, `docs/ARCHITECTURE_API.md`
179
+ - README: `packages/core/README.md`
180
+ - Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`
@@ -0,0 +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` the framework prefers `socket.handshake.auth.token` / `Authorization: Bearer ...`, otherwise the configured session cookie wins (with the other transport as 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`
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@luckystack/core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "description": "LuckyStack core: socket-first transport contracts, Redis adapter, project-config registry, hooks registry, response normalization, rate limiting, and shared session types. Foundation for every other @luckystack/* package.",
10
+ "license": "MIT",
11
+ "author": "Mathijs van Melick <mathijsvanmelick3@gmail.com>",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ItsLucky23/LuckyStack-v2.git",
15
+ "directory": "packages/core"
16
+ },
17
+ "homepage": "https://github.com/ItsLucky23/LuckyStack-v2/tree/master/packages/core#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/ItsLucky23/LuckyStack-v2/issues"
20
+ },
21
+ "keywords": [
22
+ "luckystack",
23
+ "socket.io",
24
+ "realtime",
25
+ "framework",
26
+ "fullstack",
27
+ "prisma",
28
+ "typescript",
29
+ "redis",
30
+ "react",
31
+ "nodejs"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20.0.0"
35
+ },
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ },
43
+ "./client": {
44
+ "types": "./dist/client.d.ts",
45
+ "import": "./dist/client.js"
46
+ },
47
+ "./eslint": {
48
+ "types": "./dist/eslint/index.d.ts",
49
+ "import": "./dist/eslint/index.js"
50
+ },
51
+ "./typemap": {
52
+ "types": "./dist/apiTypeStubs.d.ts"
53
+ }
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "README.md",
58
+ "CLAUDE.md",
59
+ "docs",
60
+ "LICENSE",
61
+ "CHANGELOG.md"
62
+ ],
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "build:watch": "tsup --watch",
66
+ "test": "vitest run"
67
+ },
68
+ "dependencies": {
69
+ "@socket.io/redis-adapter": "^8.3.0",
70
+ "dotenv": "^16.6.1"
71
+ },
72
+ "peerDependencies": {
73
+ "@prisma/client": "^6.19.0",
74
+ "eslint": "^9.0.0",
75
+ "ioredis": "^5.10.0",
76
+ "react": "^19.2.0",
77
+ "react-dom": "^19.2.0",
78
+ "react-router-dom": "^7.0.0",
79
+ "socket.io": "^4.8.0",
80
+ "socket.io-client": "^4.8.0",
81
+ "sonner": "^2.0.0",
82
+ "zod": "^4.0.0"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "eslint": { "optional": true },
86
+ "react": { "optional": true },
87
+ "react-dom": { "optional": true },
88
+ "react-router-dom": { "optional": true },
89
+ "sonner": { "optional": true }
90
+ }
91
+ }