@luckystack/api 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.
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +106 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +816 -0
- package/dist/index.js.map +1 -0
- package/docs/api-request-lifecycle.md +280 -0
- package/docs/auth-flow.md +232 -0
- package/docs/error-handling.md +247 -0
- package/docs/generated-types.md +264 -0
- package/docs/rate-limiting.md +188 -0
- package/docs/routing-conventions.md +164 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@luckystack/api` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial public release as part of the LuckyStack package split.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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.
|
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
ADDED
|
@@ -0,0 +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).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { apiMessage, HttpMethod } from '@luckystack/core';
|
|
2
|
+
import { Socket } from 'socket.io';
|
|
3
|
+
|
|
4
|
+
interface handleApiRequestType {
|
|
5
|
+
msg: apiMessage;
|
|
6
|
+
socket: Socket;
|
|
7
|
+
token: string | null;
|
|
8
|
+
}
|
|
9
|
+
declare function handleApiRequest({ msg, socket, token }: handleApiRequestType): Promise<boolean | void>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HTTP API Request Handler
|
|
13
|
+
*
|
|
14
|
+
* Handles API requests coming via HTTP (instead of WebSocket).
|
|
15
|
+
* Reuses existing API handlers but returns results as HTTP response.
|
|
16
|
+
*
|
|
17
|
+
* Payload format:
|
|
18
|
+
* ```json
|
|
19
|
+
* {
|
|
20
|
+
* "name": "api/examples/publicApi",
|
|
21
|
+
* "data": { "message": "hello" }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // In server.ts
|
|
28
|
+
* if (pathname.startsWith('/api/')) {
|
|
29
|
+
* const token = extractTokenFromRequest(req);
|
|
30
|
+
* const body = await parseJsonBody(req);
|
|
31
|
+
* const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
|
|
32
|
+
* res.end(JSON.stringify(result));
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
interface HttpApiRequestParams {
|
|
37
|
+
name: string;
|
|
38
|
+
data: Record<string, unknown>;
|
|
39
|
+
token: string | null;
|
|
40
|
+
requesterIp?: string;
|
|
41
|
+
xLanguageHeader?: string | string[];
|
|
42
|
+
acceptLanguageHeader?: string | string[];
|
|
43
|
+
/** HTTP method from the request */
|
|
44
|
+
method?: HttpMethod;
|
|
45
|
+
stream?: (payload: ApiHttpStreamEvent) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this
|
|
48
|
+
* to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight
|
|
49
|
+
* stream emits. Handlers receive it as `abortSignal` in params.
|
|
50
|
+
*/
|
|
51
|
+
abortSignal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
type ApiNetworkResponse<T = Record<string, unknown>> = ({
|
|
54
|
+
status: 'success';
|
|
55
|
+
httpStatus: number;
|
|
56
|
+
} & T) | {
|
|
57
|
+
status: 'error';
|
|
58
|
+
httpStatus: number;
|
|
59
|
+
message: string;
|
|
60
|
+
errorCode: string;
|
|
61
|
+
errorParams?: {
|
|
62
|
+
key: string;
|
|
63
|
+
value: string | number | boolean;
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
type ApiStreamPayload = Record<string, unknown>;
|
|
67
|
+
type ApiHttpStreamEvent = ApiStreamPayload;
|
|
68
|
+
declare function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse>;
|
|
69
|
+
|
|
70
|
+
export { type ApiHttpStreamEvent, handleApiRequest, handleHttpApiRequest };
|