@luckystack/api 0.1.8 → 0.2.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/CLAUDE.md CHANGED
@@ -1,106 +1,113 @@
1
- # @luckystack/api
2
-
3
- > AI summary + function INDEX. For deep specs see `docs/` next to this file.
4
-
5
- ## What this package does
6
-
7
- `@luckystack/api` is the transport-agnostic API request layer for LuckyStack. It exposes two handler entry points — one for socket.io (`handleApiRequest`) and one for raw HTTP (`handleHttpApiRequest`) — that share an identical pipeline: route resolution against the generated runtime map, session lookup, auth check, rate limiting, input validation (Zod from generated types), hook dispatch, handler execution with `tryCatch`-wrapped error capture, and response normalization (localized error envelopes, default HTTP status inference, streaming via SSE on the HTTP side).
8
-
9
- Routes are discovered by `@luckystack/devkit` from `src/{page}/_api/{name}_v{N}.ts` files and surfaced at runtime via `getRuntimeApiMaps()` (from `@luckystack/core`). Consumers normally do not call these handlers directly — `@luckystack/server` wires them into the socket.io server and the HTTP request listener.
10
-
11
- ## When to USE this package
12
-
13
- - You are building a custom transport (queue worker, gRPC bridge, Lambda adapter) and need to invoke the LuckyStack API pipeline for a registered route.
14
- - You are extending the framework with a new hook in the API request lifecycle (`preApiValidate`, `preApiExecute`, `preApiRespond`, `transformApiResponse`, `postApiRespond`, etc.).
15
- - You need to understand or document the exact ordering of auth -> rate limit -> validate -> execute -> respond.
16
-
17
- ## When to NOT suggest this (yet)
18
-
19
- - Writing a new app-level API endpoint: create a file under `src/{page}/_api/{name}_v{N}.ts` instead. The handler in this package will pick it up automatically.
20
- - Client-side request invocation: use `apiRequest` from `@luckystack/core` (client subpath), not anything from this package.
21
- - Plain Express/Koa middleware: this package assumes the LuckyStack runtime maps + hook system. Do not graft it onto an unrelated server framework.
22
- - Changing the request envelope shape: that contract lives in `@luckystack/core` (response normalization, error codes, locale extraction). Edit there, not here.
23
-
24
- ## Function Index
25
-
26
- | Function / Export | One-liner | Deep doc |
27
- | --- | --- | --- |
28
- | `handleApiRequest({ msg, socket, token })` (default export) | Socket.io transport adapter. Validates `apiMessage`, resolves the route via `getRuntimeApiMaps`, runs the full pipeline, emits the response on the `buildApiResponseEventName(responseIndex)` channel. Also handles the built-in `system/logout` shortcut. | -> docs/api-request-lifecycle.md |
29
- | `handleHttpApiRequest({ name, data, token, requesterIp?, xLanguageHeader?, acceptLanguageHeader?, method?, stream? })` | HTTP transport adapter for `/api/*` POST/GET/PUT/DELETE. Same pipeline as the socket variant plus HTTP-method validation (`inferHttpMethod` or route-declared `httpMethod`). Returns `ApiNetworkResponse` and optionally pipes stream events via the provided `stream` callback (SSE on the server side). | -> docs/api-request-lifecycle.md |
30
- | Type: `ApiHttpStreamEvent` | Shape of stream payloads emitted by streaming HTTP endpoints (alias of `Record<string, unknown>` extended with route-specific keys). Consumers writing SSE bridges import this. | -> docs/api-request-lifecycle.md |
31
-
32
- ### Internal pipeline helpers (not exported, listed for AI context)
33
-
34
- These live inside `handleApiRequest.ts` / `handleHttpApiRequest.ts` and are referenced in deep docs:
35
-
36
- | Helper | Role |
37
- | --- | --- |
38
- | `validateApiMessage` | Rejects malformed socket envelopes (missing `responseIndex`, non-object data). |
39
- | `checkApiAuth` | `auth.login` gate + `validateRequest` for additional auth predicates. |
40
- | `applyApiRateLimits` | Per-route bucket (`apiEntry.rateLimit` or `defaultApiLimit`) + global per-IP bucket (`defaultIpLimit`). Fires `rateLimitExceeded` hook on reject. |
41
- | `executeApiHandler` | Wraps `apiEntry.main(...)` in `tryCatch` and a `startSpan` from `@luckystack/error-tracking`. |
42
- | `buildApiResponseEnvelope` | Normalizes success/error result into the wire envelope, inferring `httpStatus` via `defaultHttpStatusForResponse`. |
43
- | `emitApiResult` | Runs `preApiRespond` / `transformApiResponse` / `postApiRespond` hooks and emits on the socket. |
44
- | `warnIfInputTypeMissing` | Dev-only one-shot warning when a route has no generated `inputType` (Zod validation effectively disabled). |
45
-
46
- ## Pipeline order (authoritative)
47
-
48
- Both transports execute the same sequence. Deviating is a breaking change.
49
-
50
- 1. Parse message / payload shape (`validateApiMessage` / inline checks).
51
- 2. `getSession(token)` -> `setSentryUser(...)`.
52
- 3. `parseTransportRouteName` (rejects invalid `api/<page>/<name>/v<N>` shapes).
53
- 4. Built-in `system/logout` shortcut (socket transport only).
54
- 5. `getRuntimeApiMaps()` -> reject with `api.notFound` if the route is unknown.
55
- 6. `checkApiAuth` (login + `validateRequest`).
56
- 7. `applyApiRateLimits` (per-route -> global IP).
57
- 8. HTTP-only: method check (`inferHttpMethod` vs `method`).
58
- 9. `preApiValidate` hook -> `validateInputByType` (skippable on socket transport via `validation: 'relaxed'` or `{ input: 'skip' }`) -> `postApiValidate` hook.
59
- 10. `preApiExecute` hook (stop-signal aborts with localized error).
60
- 11. `executeApiHandler` (wrapped in `tryCatch` + Sentry span).
61
- 12. `postApiExecute` hook (gets `result`, `error`, `durationMs`).
62
- 13. `buildApiResponseEnvelope` -> `preApiRespond` -> `transformApiResponse` -> emit -> `postApiRespond`.
63
-
64
- ## Config keys
65
-
66
- Read at request time via `getProjectConfig()` (from `@luckystack/core`). All live under `projectConfig`:
67
-
68
- - `logging.devLogs` — gate for verbose per-request `getLogger().debug/warn/error` output.
69
- - `logging.stream` — gate for `emitStream` payload logging.
70
- - `dev.warnOnMissingInputType` — toggles the one-shot `warnIfInputTypeMissing` developer hint.
71
- - `rateLimiting.defaultApiLimit` — fallback per-route limit when an API file does not export `rateLimit`. `false` disables.
72
- - `rateLimiting.defaultIpLimit` — global per-IP bucket across all API routes. `false` disables.
73
- - `rateLimiting.windowMs` — window for both buckets.
74
-
75
- No environment variables are read directly by this package — secrets and toggles travel through `@luckystack/core`'s config layer.
76
-
77
- ## Peer dependencies
78
-
79
- - **Required (runtime deps)**: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`.
80
- - **Required (peer)**:
81
- - `@prisma/client@^6.19.0` (transitively required via `@luckystack/core` session storage).
82
- - `socket.io@^4.8.0` (only consumed for the `Socket` type — the HTTP entry point does not need a live socket.io instance at call time).
83
- - **No optional peers.** Streaming uses an injected callback, not an external SSE library.
84
-
85
- ## Hooks consumed
86
-
87
- This package only dispatches hooks (it does not register any). Consumers register handlers via `dispatchHook` / the hook registry in `@luckystack/core`. Hooks emitted, in order:
88
-
89
- - `preApiValidate` — before Zod validation runs.
90
- - `postApiValidate` — after validation, with `validation: { status, message? }`.
91
- - `preApiExecute` — may return a stop signal (`errorCode`, `httpStatus?`) to short-circuit.
92
- - `postApiExecute` — observation point with `result`, `error`, `durationMs`.
93
- - `preApiRespond` — last chance to mutate or swap `payload.response`; stop signal converts to localized error.
94
- - `transformApiResponse` — purely mutative; runs between `preApiRespond` and socket/HTTP emit.
95
- - `postApiRespond` — observation-only; response is already on the wire.
96
- - `rateLimitExceeded` — fires when a bucket rejects (scope: `route` | `user` | `ip`).
97
-
98
- Deep behavior for each hook lives in `@luckystack/core`'s hook registry doc.
99
-
100
- ## Related
101
-
102
- - Architecture deep dives: `/docs/ARCHITECTURE_API.md`, `/docs/ARCHITECTURE_ROUTING.md`.
103
- - Consumer quickstart: `./README.md`.
104
- - Generated-type contract: `/docs/ARCHITECTURE_ROUTING.md` (see "Generated maps") and root `CLAUDE.md` rule 16 (no `unknown`/`any` casts).
105
- - Session source: `@luckystack/login` (`getSession`, `logout`, `BaseSessionLayout`, `AuthProps`).
106
- - Runtime map source: `@luckystack/core` `getRuntimeApiMaps()` + `@luckystack/devkit` emitter.
1
+ # @luckystack/api
2
+
3
+ > AI summary + function INDEX. For deep specs see `docs/` next to this file.
4
+
5
+ ## What this package does
6
+
7
+ `@luckystack/api` is the transport-agnostic API request layer for LuckyStack. It exposes two handler entry points — one for socket.io (`handleApiRequest`) and one for raw HTTP (`handleHttpApiRequest`) — that share an identical pipeline: route resolution against the generated runtime map, session lookup, auth check, rate limiting, input validation (Zod from generated types), hook dispatch, handler execution with `tryCatch`-wrapped error capture, and response normalization (localized error envelopes, default HTTP status inference, streaming via SSE on the HTTP side).
8
+
9
+ > ✅ **Runtime input validation now runs in production** (api F1 / core CORE-01). `validateInputByType` (in `@luckystack/core`) used to short-circuit to `{ status: 'success' }` in production; it now runs the structural validator against the build-time-resolved generated type text. The expensive dev-only TypeScript-compiler RESOLVER is still skipped in prod (it can't run there), but the pre-resolved type is validated directly — so a route typed `data: { userId: string }` rejects a malformed prod payload exactly as in dev. Gated by `validation.runtimeMode` (default `'enforce'`); set `validation.runtimeMode: 'off'` to restore the old prod no-op. If the generated type still carries an `__RUNTIME_UNRESOLVED__::` marker (artifacts not regenerated before shipping), validation surfaces it as a LOUD error (regenerate artifacts) rather than silently passing. `validation: 'relaxed'` / `{ input: 'skip' }` still skips validation per-route (now honored on BOTH transports).
10
+
11
+ Routes are discovered by `@luckystack/devkit` from `src/{page}/_api/{name}_v{N}.ts` files and surfaced at runtime via `getRuntimeApiMaps()` (from `@luckystack/core`). Consumers normally do not call these handlers directly — `@luckystack/server` wires them into the socket.io server and the HTTP request listener.
12
+
13
+ ## When to USE this package
14
+
15
+ - You are building a custom transport (queue worker, gRPC bridge, Lambda adapter) and need to invoke the LuckyStack API pipeline for a registered route.
16
+ - You are extending the framework with a new hook in the API request lifecycle (`preApiValidate`, `preApiExecute`, `preApiRespond`, `transformApiResponse`, `postApiRespond`, etc.).
17
+ - You need to understand or document the exact ordering of auth -> rate limit -> validate -> execute -> respond.
18
+
19
+ ## When to NOT suggest this (yet)
20
+
21
+ - Writing a new app-level API endpoint: create a file under `src/{page}/_api/{name}_v{N}.ts` instead. The handler in this package will pick it up automatically.
22
+ - Client-side request invocation: use `apiRequest` from `@luckystack/core` (client subpath), not anything from this package.
23
+ - Plain Express/Koa middleware: this package assumes the LuckyStack runtime maps + hook system. Do not graft it onto an unrelated server framework.
24
+ - Changing the request envelope shape: that contract lives in `@luckystack/core` (response normalization, error codes, locale extraction). Edit there, not here.
25
+
26
+ ## Function Index
27
+
28
+ | Function / Export | One-liner | Deep doc |
29
+ | --- | --- | --- |
30
+ | `handleApiRequest({ msg, socket, token })` (default export) | Socket.io transport adapter. Validates `apiMessage`, resolves the route via `getRuntimeApiMaps`, runs the full pipeline, emits the response on the `buildApiResponseEventName(responseIndex)` channel. Also handles the built-in `system/logout` shortcut. | -> docs/api-request-lifecycle.md |
31
+ | `handleHttpApiRequest({ name, data, token, requesterIp?, xLanguageHeader?, acceptLanguageHeader?, method?, stream? })` | HTTP transport adapter for `/api/*` POST/GET/PUT/DELETE. Same pipeline as the socket variant plus HTTP-method validation (`inferHttpMethod` or route-declared `httpMethod`). Returns `ApiNetworkResponse` and optionally pipes stream events via the provided `stream` callback (SSE on the server side). | -> docs/api-request-lifecycle.md |
32
+ | Type: `ApiHttpStreamEvent` | Shape of stream payloads emitted by streaming HTTP endpoints (alias of `Record<string, unknown>` extended with route-specific keys). Consumers writing SSE bridges import this. | -> docs/api-request-lifecycle.md |
33
+
34
+ ### Internal pipeline helpers (not exported, listed for AI context)
35
+
36
+ These live inside `handleApiRequest.ts` / `handleHttpApiRequest.ts` and are referenced in deep docs:
37
+
38
+ | Helper | Role |
39
+ | --- | --- |
40
+ | `validateApiMessage` | Rejects malformed socket envelopes (missing `responseIndex`, non-object/array data). |
41
+ | `checkApiAuth` | `auth.login` gate + null-safe `validateRequest` for additional auth predicates (an anonymous caller on a public route with `additional` predicates is cleanly forbidden, not a throw — api F4). Dispatches the `apiAuthRejected` hook (`void`) on every fail path (api F9). |
42
+ | `applyApiRateLimits` | Per-route bucket (`apiEntry.rateLimit` or `defaultApiLimit`, keyed on the validated `user.id` or the resolved IP — never the raw token; basis overridable via `rateLimiting.identity`) + global per-IP bucket (`defaultIpLimit`, skippable for loopback in dev via `rateLimiting.skipLoopbackInDev`). Fires `rateLimitExceeded` hook on reject. |
43
+ | `applyGlobalIpRateLimit` | Shared global per-IP `ip:<ip>:api:all` bucket helper; also applied to the built-in `system/logout` shortcut so it can't be spammed uncapped. |
44
+ | `executeApiHandler` | Wraps `apiEntry.main(...)` in `tryCatch`. Span open/close + identity propagation moved to `preApiExecute`/`postApiExecute` hook subscribers in `@luckystack/error-tracking` (no direct `startSpan` in this handler). |
45
+ | `normalizeApiResponse` (in `_shared/responseEnvelope.ts`) | Normalizes success/error result into the wire envelope, inferring `httpStatus` via `defaultHttpStatusForResponse`. |
46
+ | `emitApiResult` | Runs `preApiRespond` / `transformApiResponse` / `postApiRespond` hooks and emits on the socket. |
47
+ | `warnIfInputTypeMissing` (in `_shared/inputTypeWarning.ts`) | Dev-only one-shot warning when a strict-mode route has no generated `inputType` (Zod validation effectively disabled). |
48
+
49
+ ## Pipeline order (authoritative)
50
+
51
+ Both transports execute the same sequence. Deviating is a breaking change.
52
+
53
+ 1. Parse message / payload shape (`validateApiMessage` / inline checks; rejects arrays).
54
+ 2. `readSession(token)` -> resolve `user` (may be null on public routes). Identity propagation to error trackers happens later via the `preApiExecute` hook subscriber, NOT a direct `setSentryUser` here.
55
+ 3. `parseTransportRouteName` (rejects invalid `api/<page>/<name>/v<N>` shapes).
56
+ 4. Built-in `system/logout` shortcut (socket transport only; now subject to the global per-IP bucket first).
57
+ 5. `getRuntimeApiMaps()` -> reject with `api.notFound` if the route is unknown.
58
+ 6. `checkApiAuth` (login + `validateRequest`).
59
+ 7. `applyApiRateLimits` (per-route -> global IP).
60
+ 8. **HTTP-only**: method check (`inferHttpMethod` vs `method`). The `httpMethod` route export and the `inferHttpMethod` heuristic are **HTTP-only**. The socket transport does NOT enforce the declared HTTP method — all routes are callable over WebSocket regardless of method declaration (socket.io has no method concept). Do not rely on `httpMethod` for authorization; use `auth` predicates or `preSocketMessage` for socket-specific gates.
61
+ 9. `preApiValidate` hook -> `validateInputByType` (skippable on BOTH transports via `validation: 'relaxed'` or `{ input: 'skip' }`) -> `postApiValidate` hook. `validateInputByType` now runs the structural validator in production too (gated by `validation.runtimeMode`, default `'enforce'`); set `validation.runtimeMode: 'off'` to restore the old prod no-op. See the caveat at the top.
62
+ 10. `preApiExecute` hook (stop-signal aborts with localized error).
63
+ 11. `executeApiHandler` (wrapped in `tryCatch`; span/identity via the `pre/postApiExecute` hook subscribers, not a direct span here).
64
+ 12. `postApiExecute` hook (gets `result`, `error`, `durationMs`).
65
+ 13. `normalizeApiResponse` -> `preApiRespond` -> `transformApiResponse` -> emit -> `postApiRespond`.
66
+
67
+ ## Config keys
68
+
69
+ Read at request time via `getProjectConfig()` (from `@luckystack/core`). All live under `projectConfig`:
70
+
71
+ - `logging.devLogs` — gate for verbose per-request `getLogger().debug/warn/error` output.
72
+ - `logging.stream` — gate for `emitStream` payload logging.
73
+ - `dev.warnOnMissingInputType` — toggles the one-shot `warnIfInputTypeMissing` developer hint.
74
+ - `rateLimiting.defaultApiLimit` — fallback per-route limit when an API file does not export `rateLimit`. `false` disables.
75
+ - `rateLimiting.defaultIpLimit` — global per-IP bucket across all API routes. `false` disables.
76
+ - `rateLimiting.windowMs` — window for both buckets.
77
+ - `rateLimiting.skipLoopbackInDev` — when `true` (default `false`), skips ONLY the global per-IP ABUSE bucket for loopback IPs in non-production (per-route bucket still applies). Lets a dev / the test runner hammer localhost without tripping the cross-route cap. Gated on this explicit flag (not inferred from `NODE_ENV` + a spoofable address) and on `isLoopbackIp(resolvedIp)`.
78
+ - `rateLimiting.identity` — optional `(params) => { scope: 'user'|'ip'|'custom'; id } | null` callback that overrides the per-route bucket BASIS (api-key / tenant). Returning `null` falls back to the default (validated `user.id`, else resolved IP — NEVER the raw token).
79
+
80
+ No environment variables are read directly by this package — secrets and toggles travel through `@luckystack/core`'s config layer.
81
+
82
+ ## Peer dependencies
83
+
84
+ - **Required (runtime deps)**: `@luckystack/core`, `@luckystack/error-tracking`. **`@luckystack/login` is NOT a dependency** (0.2.0 decoupling) — sessions resolve through core's `readSession` / session-provider registry, with login as the default *provider* (optional package), not a hard runtime dep.
85
+ - **Required (peer)**:
86
+ - `@prisma/client@^6.19.0` (transitively required via `@luckystack/core` session storage).
87
+ - `socket.io@^4.8.0` (only consumed for the `Socket` type — the HTTP entry point does not need a live socket.io instance at call time).
88
+ - **No optional peers.** Streaming uses an injected callback, not an external SSE library.
89
+
90
+ ## Hooks consumed
91
+
92
+ This package only dispatches hooks (it does not register any). Consumers register handlers via `dispatchHook` / the hook registry in `@luckystack/core`. Hooks emitted, in order:
93
+
94
+ - `preSocketMessage` — (socket transport only) at the very top, before session lookup / route resolution / auth. Stop signal rejects the message. Transport-level seam mirroring `preHttpRequest`; `channel: 'api'`.
95
+ - `preApiValidate` — before Zod validation runs.
96
+ - `postApiValidate` — after validation, with `validation: { status, message? }`.
97
+ - `preApiExecute` — may return a stop signal (`errorCode`, `httpStatus?`) to short-circuit.
98
+ - `postApiExecute` — observation point with `result`, `error`, `durationMs`.
99
+ - `preApiRespond` — last chance to mutate or swap `payload.response`; stop signal converts to localized error.
100
+ - `transformApiResponse` — purely mutative; runs between `preApiRespond` and socket/HTTP emit.
101
+ - `postApiRespond` — observation-only; response is already on the wire.
102
+ - `rateLimitExceeded` — fires when a bucket rejects (scope: `route` | `user` | `ip`).
103
+ - `apiAuthRejected` — fires on every auth-fail path (login required, `additional[]` predicate failed, or a misconfigured predicate). Observational (`void`-dispatched, stop signal ignored); payload `{ routeName, reason, userId, ip?, transport?, failedKey? }`. Lets abuse-detection / audit subscribers see failed authorizations (e.g. credential-stuffing) without forking the handler.
104
+
105
+ Deep behavior for each hook lives in `@luckystack/core`'s hook registry doc.
106
+
107
+ ## Related
108
+
109
+ - Architecture deep dives: `/docs/ARCHITECTURE_API.md`, `/docs/ARCHITECTURE_ROUTING.md`.
110
+ - Consumer quickstart: `./README.md`.
111
+ - Generated-type contract: `/docs/ARCHITECTURE_ROUTING.md` (see "Generated maps") and root `CLAUDE.md` rule 16 (no `unknown`/`any` casts).
112
+ - Session source: `@luckystack/core`'s session-provider registry — `readSession(token)` / `performLogout(...)` (see `docs/ARCHITECTURE_SESSION.md`). `@luckystack/login` is the DEFAULT provider when installed, not a hard dep. Shared types `BaseSessionLayout` / `AuthProps` come from `@luckystack/core`.
113
+ - Runtime map source: `@luckystack/core` `getRuntimeApiMaps()` + `@luckystack/devkit` emitter.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Mathijs van Melick
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mathijs van Melick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,106 +1,106 @@
1
- # @luckystack/api
2
-
3
- > Type-safe API request handlers for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). WebSocket-first via socket.io with HTTP fallback. File-based routing, generated route map, integrated rate limiting, validation, hooks, and error-tracking tracing.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @luckystack/api @luckystack/core @luckystack/login @luckystack/error-tracking socket.io
9
- ```
10
-
11
- ## Quickstart
12
-
13
- API endpoints live in `src/{page}/_api/{name}_v{N}.ts` and are picked up by the file-based router. Each file exports `main`, plus optional metadata (`auth`, `method`, `rateLimit`).
14
-
15
- ```ts
16
- // src/settings/_api/updateUser_v1.ts
17
- import type { AuthProps } from '@luckystack/core';
18
- import type { ApiResponse } from '@luckystack/core';
19
-
20
- export const rateLimit: number | false = 20;
21
- export const method = 'POST' as const;
22
- export const auth: AuthProps = { login: true };
23
-
24
- export interface ApiParams {
25
- data: { name: string };
26
- user: SessionLayout;
27
- functions: Functions;
28
- }
29
-
30
- export const main = async ({ data, user }: ApiParams): Promise<ApiResponse> => {
31
- await prisma.user.update({ where: { id: user.id }, data: { name: data.name } });
32
- return { status: 'success', result: { ok: true } };
33
- };
34
- ```
35
-
36
- The package exposes the two transport adapters that `@luckystack/server` wires into Socket.io and HTTP:
37
-
38
- ```ts
39
- import handleApiRequest from '@luckystack/api';
40
- import { handleHttpApiRequest } from '@luckystack/api';
41
-
42
- io.on('connection', socket => {
43
- socket.on('apiRequest', msg => handleApiRequest({ msg, socket, token }));
44
- });
45
-
46
- httpServer.on('request', async (req, res) => {
47
- if (req.url?.startsWith('/api/')) {
48
- const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
49
- res.end(JSON.stringify(result));
50
- }
51
- });
52
- ```
53
-
54
- You typically don't call these yourself — `createLuckyStackServer` does. Use this package directly only when building a custom transport.
55
-
56
- ## How it integrates
57
-
58
- 1. **Validates** the inbound payload against the Zod schema generated from your `ApiParams['data']` interface (via `@luckystack/devkit`).
59
- 2. **Rate-limits** by IP + token using `checkRateLimit` from `@luckystack/core`.
60
- 3. **Authenticates** via `getSession` from `@luckystack/login` when `auth.login === true`.
61
- 4. **Dispatches** the `preApiExecute` hook (may abort with a stop signal).
62
- 5. **Calls** your `main(...)` and captures errors via `tryCatch` (auto-forwarded to Sentry).
63
- 6. **Dispatches** the `postApiExecute` hook with the result + duration.
64
- 7. **Returns** the response via socket `ack` or HTTP body / SSE stream.
65
-
66
- ## Generated types
67
-
68
- `apiRequest` (in `@luckystack/core/client`) is fully typed against the route map emitted by `@luckystack/devkit` from your `_api/*` files (default location: `src/_sockets/apiTypes.generated.ts`). Use route-name + version literals so inference works:
69
-
70
- ```ts
71
- const result = await apiRequest({
72
- name: 'settings/updateUser',
73
- version: 'v1',
74
- data: { name: 'Alice' },
75
- });
76
-
77
- if (result.status === 'success') {
78
- // result is typed from the matching `main` return type.
79
- }
80
- ```
81
-
82
- Do not wrap `apiRequest` in `unknown` / `any` shims (see `.claude/CLAUDE.md` rule 16). If inference fails, fix the typing source or regenerate maps instead.
83
-
84
- ## Public API
85
-
86
- | Export | Purpose |
87
- | --- | --- |
88
- | `handleApiRequest({ msg, socket, token })` | Socket.io request handler (default export). |
89
- | `handleHttpApiRequest({ name, data, token, ... })` | HTTP fallback for `/api/*` routes; returns `Promise<ApiNetworkResponse>` and supports SSE streaming via a `stream` callback. |
90
- | Type: `ApiHttpStreamEvent` | SSE event shape emitted by streaming endpoints. |
91
-
92
- ## Related architecture docs
93
-
94
- - [`docs/ARCHITECTURE_API.md`](../../docs/ARCHITECTURE_API.md) — full request lifecycle, streaming, error contract.
95
- - [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — `_api/` file conventions and method inference.
96
-
97
- ## Dependencies
98
-
99
- - Runtime: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`
100
- - Peer (canonical ranges, standardized 2026-05-07):
101
- - `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
102
- - `socket.io@^4.8.0`
103
-
104
- ## License
105
-
106
- MIT — see [LICENSE](../../LICENSE).
1
+ # @luckystack/api
2
+
3
+ > Type-safe API request handlers for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). WebSocket-first via socket.io with HTTP fallback. File-based routing, generated route map, integrated rate limiting, validation, hooks, and error-tracking tracing.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @luckystack/api @luckystack/core @luckystack/login @luckystack/error-tracking socket.io
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ API endpoints live in `src/{page}/_api/{name}_v{N}.ts` and are picked up by the file-based router. Each file exports `main`, plus optional metadata (`auth`, `method`, `rateLimit`).
14
+
15
+ ```ts
16
+ // src/settings/_api/updateUser_v1.ts
17
+ import type { AuthProps } from '@luckystack/core';
18
+ import type { ApiResponse } from '@luckystack/core';
19
+
20
+ export const rateLimit: number | false = 20;
21
+ export const method = 'POST' as const;
22
+ export const auth: AuthProps = { login: true };
23
+
24
+ export interface ApiParams {
25
+ data: { name: string };
26
+ user: SessionLayout;
27
+ functions: Functions;
28
+ }
29
+
30
+ export const main = async ({ data, user }: ApiParams): Promise<ApiResponse> => {
31
+ await prisma.user.update({ where: { id: user.id }, data: { name: data.name } });
32
+ return { status: 'success', result: { ok: true } };
33
+ };
34
+ ```
35
+
36
+ The package exposes the two transport adapters that `@luckystack/server` wires into Socket.io and HTTP:
37
+
38
+ ```ts
39
+ import handleApiRequest from '@luckystack/api';
40
+ import { handleHttpApiRequest } from '@luckystack/api';
41
+
42
+ io.on('connection', socket => {
43
+ socket.on('apiRequest', msg => handleApiRequest({ msg, socket, token }));
44
+ });
45
+
46
+ httpServer.on('request', async (req, res) => {
47
+ if (req.url?.startsWith('/api/')) {
48
+ const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
49
+ res.end(JSON.stringify(result));
50
+ }
51
+ });
52
+ ```
53
+
54
+ You typically don't call these yourself — `createLuckyStackServer` does. Use this package directly only when building a custom transport.
55
+
56
+ ## How it integrates
57
+
58
+ 1. **Validates** the inbound payload against the Zod schema generated from your `ApiParams['data']` interface (via `@luckystack/devkit`).
59
+ 2. **Rate-limits** by IP + token using `checkRateLimit` from `@luckystack/core`.
60
+ 3. **Authenticates** via `getSession` from `@luckystack/login` when `auth.login === true`.
61
+ 4. **Dispatches** the `preApiExecute` hook (may abort with a stop signal).
62
+ 5. **Calls** your `main(...)` and captures errors via `tryCatch` (auto-forwarded to Sentry).
63
+ 6. **Dispatches** the `postApiExecute` hook with the result + duration.
64
+ 7. **Returns** the response via socket `ack` or HTTP body / SSE stream.
65
+
66
+ ## Generated types
67
+
68
+ `apiRequest` (in `@luckystack/core/client`) is fully typed against the route map emitted by `@luckystack/devkit` from your `_api/*` files (default location: `src/_sockets/apiTypes.generated.ts`). Use route-name + version literals so inference works:
69
+
70
+ ```ts
71
+ const result = await apiRequest({
72
+ name: 'settings/updateUser',
73
+ version: 'v1',
74
+ data: { name: 'Alice' },
75
+ });
76
+
77
+ if (result.status === 'success') {
78
+ // result is typed from the matching `main` return type.
79
+ }
80
+ ```
81
+
82
+ Do not wrap `apiRequest` in `unknown` / `any` shims (see `.claude/CLAUDE.md` rule 16). If inference fails, fix the typing source or regenerate maps instead.
83
+
84
+ ## Public API
85
+
86
+ | Export | Purpose |
87
+ | --- | --- |
88
+ | `handleApiRequest({ msg, socket, token })` | Socket.io request handler (default export). |
89
+ | `handleHttpApiRequest({ name, data, token, ... })` | HTTP fallback for `/api/*` routes; returns `Promise<ApiNetworkResponse>` and supports SSE streaming via a `stream` callback. |
90
+ | Type: `ApiHttpStreamEvent` | SSE event shape emitted by streaming endpoints. |
91
+
92
+ ## Related architecture docs
93
+
94
+ - [`docs/ARCHITECTURE_API.md`](../../docs/ARCHITECTURE_API.md) — full request lifecycle, streaming, error contract.
95
+ - [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — `_api/` file conventions and method inference.
96
+
97
+ ## Dependencies
98
+
99
+ - Runtime: `@luckystack/core`, `@luckystack/login`, `@luckystack/error-tracking`
100
+ - Peer (canonical ranges, standardized 2026-05-07):
101
+ - `@prisma/client@^6.19.0` (transitively required via `@luckystack/core`)
102
+ - `socket.io@^4.8.0`
103
+
104
+ ## License
105
+
106
+ MIT — see [LICENSE](../../LICENSE).
package/dist/index.d.ts CHANGED
@@ -6,7 +6,10 @@ interface handleApiRequestType {
6
6
  socket: Socket;
7
7
  token: string | null;
8
8
  }
9
- declare function handleApiRequest({ msg, socket, token }: handleApiRequestType): Promise<boolean | void>;
9
+ declare function handleApiRequest(args: handleApiRequestType): Promise<void>;
10
+
11
+ /** Loosely-typed stream payload emitted by streaming endpoints. */
12
+ type ApiStreamPayload = Record<string, unknown>;
10
13
 
11
14
  /**
12
15
  * HTTP API Request Handler
@@ -63,7 +66,6 @@ type ApiNetworkResponse<T = Record<string, unknown>> = ({
63
66
  value: string | number | boolean;
64
67
  }[];
65
68
  };
66
- type ApiStreamPayload = Record<string, unknown>;
67
69
  type ApiHttpStreamEvent = ApiStreamPayload;
68
70
  declare function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse>;
69
71