@luckystack/api 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -0
- package/CHANGELOG.md +6 -1
- package/README.md +2 -103
- package/dist/_shared/apiTypes.d.ts +68 -0
- package/dist/_shared/applyApiRateLimits.d.ts +30 -0
- package/dist/_shared/backpressure.d.ts +4 -0
- package/dist/_shared/httpValidationStage.d.ts +14 -0
- package/dist/_shared/inputTypeWarning.d.ts +1 -0
- package/dist/_shared/logFlags.d.ts +4 -0
- package/dist/_shared/rateLimitIdentity.d.ts +1 -0
- package/dist/_shared/requestLifecycle.d.ts +14 -0
- package/dist/_shared/responseEnvelope.d.ts +9 -0
- package/dist/_shared/socketValidationStage.d.ts +13 -0
- package/dist/handleApiRequest.d.ts +9 -0
- package/dist/handleHttpApiRequest.d.ts +60 -0
- package/dist/index.d.ts +3 -72
- package/dist/index.js.map +1 -1
- package/docs/api-request-lifecycle.md +3 -276
- package/docs/auth-flow.md +3 -228
- package/docs/error-handling.md +3 -242
- package/docs/generated-types.md +3 -260
- package/docs/rate-limiting.md +3 -184
- package/docs/routing-conventions.md +3 -160
- package/package.json +4 -4
- package/CLAUDE.md +0 -111
package/AGENTS.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @luckystack/api — design context
|
|
2
|
+
|
|
3
|
+
API is the route-adapter layer between generated runtime maps and socket/HTTP transports; server bootstraps it. Keep auth, validation, rate limiting, hooks and response normalization consistent across transports. Their implementations, not a hand-maintained step list, are authoritative: `src/handleApiRequest.ts`, `src/handleHttpApiRequest.ts`, `src/_shared/`, `src/transportParity.test.ts`.
|
|
4
|
+
|
|
5
|
+
Do not use an HTTP method declaration as an authorization gate: sockets have no HTTP method. Authenticate and authorize with route auth policy on both transports. Validate input before application execution; do not expose schema-error details from a protected route to unauthorized callers. Derive per-user limiter identity from validated session identity, not an attacker-supplied token; treat forwarded IP headers as trusted only behind the configured proxy (`../core/src/resolveClientIp.ts`). Keep observational hooks observational and do not bypass error normalization.
|
|
6
|
+
|
|
7
|
+
Type maps are generated contracts, not a reason for unsafe casts: regenerate artifacts and correct the generator/source when typing fails. See `docs/generated-types.md` and repository `AGENTS.md`.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,12 @@ All notable changes to `@luckystack/api` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.11.0] - 2026-10-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Type declarations are emitted per source file by TypeScript 7 (`tsc -b`) instead of one bundled `.d.ts` per entry, so the framework build no longer runs package by package. The exported types are unchanged; relative imports inside the declarations carry `.js`, so `moduleResolution: "node16"`/`"nodenext"` projects resolve them as before.
|
|
13
|
+
- Package design guidance now ships as `AGENTS.md`, not `CLAUDE.md`. Handwritten docs focus on rationale and safety; exact API contracts remain in installed declarations/source.
|
|
9
14
|
|
|
10
15
|
## [0.8.4] - 2026-08-17
|
|
11
16
|
|
package/README.md
CHANGED
|
@@ -1,106 +1,5 @@
|
|
|
1
1
|
# @luckystack/api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe API handlers for LuckyStack's socket-first transport with HTTP fallback. Install with `npm install @luckystack/api socket.io`; add optional `@prisma/client` for Prisma-backed routes (see `package.json`). The server package normally wires the handlers; route discovery and typed maps are generated by devkit.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @luckystack/api socket.io
|
|
9
|
-
# Optional Prisma-backed route code: npm install @prisma/client
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Quickstart
|
|
13
|
-
|
|
14
|
-
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`).
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
// src/settings/_api/updateUser_v1.ts
|
|
18
|
-
import type { AuthProps } from '@luckystack/core';
|
|
19
|
-
import type { ApiResponse } from '@luckystack/core';
|
|
20
|
-
|
|
21
|
-
export const rateLimit: number | false = 20;
|
|
22
|
-
export const method = 'POST' as const;
|
|
23
|
-
export const auth: AuthProps = { login: true };
|
|
24
|
-
|
|
25
|
-
export interface ApiParams {
|
|
26
|
-
data: { name: string };
|
|
27
|
-
user: SessionLayout;
|
|
28
|
-
functions: Functions;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const main = async ({ data, user }: ApiParams): Promise<ApiResponse> => {
|
|
32
|
-
await prisma.user.update({ where: { id: user.id }, data: { name: data.name } });
|
|
33
|
-
return { status: 'success', result: { ok: true } };
|
|
34
|
-
};
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The package exposes the two transport adapters that `@luckystack/server` wires into Socket.io and HTTP:
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import handleApiRequest from '@luckystack/api';
|
|
41
|
-
import { handleHttpApiRequest } from '@luckystack/api';
|
|
42
|
-
|
|
43
|
-
io.on('connection', socket => {
|
|
44
|
-
socket.on('apiRequest', msg => handleApiRequest({ msg, socket, token }));
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
httpServer.on('request', async (req, res) => {
|
|
48
|
-
if (req.url?.startsWith('/api/')) {
|
|
49
|
-
const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
|
|
50
|
-
res.end(JSON.stringify(result));
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
You typically don't call these yourself — `createLuckyStackServer` does. Use this package directly only when building a custom transport.
|
|
56
|
-
|
|
57
|
-
## How it integrates
|
|
58
|
-
|
|
59
|
-
1. **Authenticates** via `readSession` from `@luckystack/core` (the session-provider registry; `@luckystack/login` is the optional default provider, not a hard dependency) when `auth.login === true`. Auth runs first so unauthenticated probes can't enumerate routes or learn input shape from the validation message.
|
|
60
|
-
2. **Rate-limits** with two buckets via `checkRateLimit` from `@luckystack/core`: a per-route bucket keyed on the validated `user.id` (anonymous callers fall back to the resolved IP — never the raw session token, so re-login can't reset the limit; the basis is overridable via `rateLimiting.identity`) plus a global per-IP abuse bucket.
|
|
61
|
-
3. **Validates** the inbound payload against the Zod schema generated from your `ApiParams['data']` interface (via `@luckystack/devkit`).
|
|
62
|
-
4. **Dispatches** the `preApiExecute` hook (may abort with a stop signal).
|
|
63
|
-
5. **Calls** your `main(...)` and captures thrown errors via `tryCatch` (fanned out to every registered error tracker).
|
|
64
|
-
6. **Dispatches** the `postApiExecute` hook with the result + duration.
|
|
65
|
-
7. **Returns** the response via socket `ack` or HTTP body / SSE stream.
|
|
66
|
-
|
|
67
|
-
## Generated types
|
|
68
|
-
|
|
69
|
-
`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:
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
const result = await apiRequest({
|
|
73
|
-
name: 'settings/updateUser',
|
|
74
|
-
version: 'v1',
|
|
75
|
-
data: { name: 'Alice' },
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (result.status === 'success') {
|
|
79
|
-
// result is typed from the matching `main` return type.
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Do not wrap `apiRequest` in `unknown` / `any` shims (see repository-root [`CLAUDE.md`](../../CLAUDE.md) rule 21). If inference fails, fix the typing source or regenerate maps instead.
|
|
84
|
-
|
|
85
|
-
## Public API
|
|
86
|
-
|
|
87
|
-
| Export | Purpose |
|
|
88
|
-
| --- | --- |
|
|
89
|
-
| `handleApiRequest({ msg, socket, token })` | Socket.io request handler (default export). |
|
|
90
|
-
| `handleHttpApiRequest({ name, data, token, ... })` | HTTP fallback for `/api/*` routes; returns `Promise<ApiNetworkResponse>` and supports SSE streaming via a `stream` callback. |
|
|
91
|
-
| Type: `ApiHttpStreamEvent` | SSE event shape emitted by streaming endpoints. |
|
|
92
|
-
|
|
93
|
-
## Related architecture docs
|
|
94
|
-
|
|
95
|
-
- [`docs/ARCHITECTURE_API.md`](../../docs/ARCHITECTURE_API.md) — full request lifecycle, streaming, error contract.
|
|
96
|
-
- [`docs/ARCHITECTURE_ROUTING.md`](../../docs/ARCHITECTURE_ROUTING.md) — `_api/` file conventions and method inference.
|
|
97
|
-
|
|
98
|
-
## Dependencies
|
|
99
|
-
|
|
100
|
-
- Runtime: `@luckystack/core`, `@luckystack/error-tracking` (`@luckystack/login` is NOT a runtime dependency — sessions resolve through core's session-provider registry; login is the optional default provider)
|
|
101
|
-
- Required peer: `socket.io@^4.8.0`
|
|
102
|
-
- Optional peer: `@prisma/client@^6.19.0` — only needed when the active session/user provider uses the default Prisma client.
|
|
103
|
-
|
|
104
|
-
## License
|
|
105
|
-
|
|
106
|
-
MIT — see [LICENSE](../../LICENSE).
|
|
5
|
+
Keep the same authorization and validation boundary on both transports. A declared HTTP method cannot protect a WebSocket route. When route inference fails, regenerate artifacts and fix the source type instead of casting. See [design context](./AGENTS.md), [auth rationale](./docs/auth-flow.md) and [generated types](./docs/generated-types.md). For signatures and route declarations consult `src/` and the generated map.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AuthProps, BaseSessionLayout as SessionLayout, ErrorFormatter, HttpMethod } from '@luckystack/core';
|
|
2
|
+
/** Loosely-typed stream payload emitted by streaming endpoints. */
|
|
3
|
+
export type ApiStreamPayload = Record<string, unknown>;
|
|
4
|
+
export interface RuntimeSuccessResponse {
|
|
5
|
+
status: 'success';
|
|
6
|
+
message?: string;
|
|
7
|
+
httpStatus?: number;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface RuntimeErrorResponse {
|
|
11
|
+
status: 'error';
|
|
12
|
+
errorCode?: string;
|
|
13
|
+
errorParams?: {
|
|
14
|
+
key: string;
|
|
15
|
+
value: string | number | boolean;
|
|
16
|
+
}[];
|
|
17
|
+
httpStatus?: number;
|
|
18
|
+
message?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** Discriminated union returned by a registered API handler's `main`. */
|
|
22
|
+
export type RuntimeApiResponse = RuntimeSuccessResponse | RuntimeErrorResponse;
|
|
23
|
+
/** Narrowing guard for an object that is already a `RuntimeApiResponse`. */
|
|
24
|
+
export declare const isRuntimeApiResponse: (value: unknown) => value is RuntimeApiResponse;
|
|
25
|
+
export interface ApiFlushPressureOptions {
|
|
26
|
+
thresholdBytes?: number;
|
|
27
|
+
}
|
|
28
|
+
export type ApiFlushPressure = (options?: ApiFlushPressureOptions) => Promise<void>;
|
|
29
|
+
/** Shape every registered API route exposes on the runtime map. */
|
|
30
|
+
export interface RuntimeApiEntry {
|
|
31
|
+
auth: AuthProps;
|
|
32
|
+
main: (params: {
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
user: SessionLayout | null;
|
|
35
|
+
functions: Record<string, unknown>;
|
|
36
|
+
stream: (payload?: ApiStreamPayload) => void;
|
|
37
|
+
abortSignal: AbortSignal;
|
|
38
|
+
flushPressure: ApiFlushPressure;
|
|
39
|
+
}) => Promise<RuntimeApiResponse> | RuntimeApiResponse;
|
|
40
|
+
inputType?: string;
|
|
41
|
+
inputTypeFilePath?: string;
|
|
42
|
+
rateLimit?: number | false;
|
|
43
|
+
httpMethod?: HttpMethod;
|
|
44
|
+
/**
|
|
45
|
+
* Per-route validation strictness.
|
|
46
|
+
* `'strict'` (default): runtime Zod validation runs, mismatched payloads
|
|
47
|
+
* are rejected with `api.invalidInputType`.
|
|
48
|
+
* `'relaxed'` / `{ input: 'skip' }`: skip the validate step. Use for public
|
|
49
|
+
* webhooks that receive third-party-shaped payloads you can't model in TS,
|
|
50
|
+
* or for migration windows when input shapes are in flux.
|
|
51
|
+
*/
|
|
52
|
+
validation?: 'strict' | 'relaxed' | {
|
|
53
|
+
input: 'skip' | 'strict';
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Per-route error response formatter. Receives the normalized error
|
|
57
|
+
* envelope + context and returns the shape to emit. Falls back to the
|
|
58
|
+
* global formatter from `registerErrorFormatter(...)`, then to the
|
|
59
|
+
* framework default `normalizeErrorResponse`.
|
|
60
|
+
*/
|
|
61
|
+
errorFormatter?: ErrorFormatter;
|
|
62
|
+
}
|
|
63
|
+
/** Mutable envelope shape passed between respond-phase hooks + the formatter. */
|
|
64
|
+
export interface ApiResponseEnvelope {
|
|
65
|
+
status: 'success' | 'error';
|
|
66
|
+
httpStatus?: number;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';
|
|
2
|
+
export interface ApplyApiRateLimitsArgs {
|
|
3
|
+
/** Effective client IP (already resolved by the caller's transport adapter). */
|
|
4
|
+
resolvedIp: string;
|
|
5
|
+
token: string | null;
|
|
6
|
+
user: SessionLayout | null;
|
|
7
|
+
resolvedName: string;
|
|
8
|
+
rateLimit: number | false | undefined;
|
|
9
|
+
transport: 'socket' | 'http';
|
|
10
|
+
/**
|
|
11
|
+
* When true, skip the global-IP (`ip:…:api:all`) bucket check. Used by the
|
|
12
|
+
* HTTP transport to honour `rateLimiting.skipLoopbackInDev` for loopback
|
|
13
|
+
* callers without duplicating bucket logic here. The socket transport never
|
|
14
|
+
* sets this (loopback skip is HTTP-only).
|
|
15
|
+
*/
|
|
16
|
+
skipGlobalIpBucket?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ApplyApiRateLimitsResult {
|
|
19
|
+
/** True when all buckets allow the request; false when any bucket exceeded. */
|
|
20
|
+
allowed: boolean;
|
|
21
|
+
/** The error payload to emit when `allowed` is false. */
|
|
22
|
+
errorCode?: string;
|
|
23
|
+
resetIn?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Checks per-route + global-IP rate-limit buckets for an API request.
|
|
27
|
+
* Returns `{ allowed: true }` when all buckets allow through, or
|
|
28
|
+
* `{ allowed: false, errorCode, resetIn }` when any bucket is full.
|
|
29
|
+
*/
|
|
30
|
+
export declare const applyApiRateLimits: ({ resolvedIp, user, resolvedName, rateLimit, transport, skipGlobalIpBucket, }: ApplyApiRateLimitsArgs) => Promise<ApplyApiRateLimitsResult>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Socket } from 'socket.io';
|
|
2
|
+
import type { ApiFlushPressure } from './apiTypes.js';
|
|
3
|
+
export declare const createApiFlushPressure: (socket: Socket, abortSignal: AbortSignal) => ApiFlushPressure;
|
|
4
|
+
export declare const httpApiFlushPressureNoop: ApiFlushPressure;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';
|
|
2
|
+
import type { RuntimeApiEntry } from './apiTypes.js';
|
|
3
|
+
export declare const runHttpApiValidation: ({ resolvedName, inputType, inputTypeFilePath, requestData, user, validation, }: {
|
|
4
|
+
resolvedName: string;
|
|
5
|
+
inputType?: string;
|
|
6
|
+
inputTypeFilePath?: string;
|
|
7
|
+
requestData: Record<string, unknown>;
|
|
8
|
+
user: SessionLayout | null;
|
|
9
|
+
validation: RuntimeApiEntry['validation'];
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const warnIfInputTypeMissing: (resolvedName: string, inputType: string | undefined) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deriveTokenBucketId } from '@luckystack/core';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Socket } from 'socket.io';
|
|
2
|
+
import type { ApiStreamPayload, ApiFlushPressure } from './apiTypes.js';
|
|
3
|
+
export interface ApiRequestLifecycle {
|
|
4
|
+
abortController: AbortController;
|
|
5
|
+
abortSignal: AbortSignal;
|
|
6
|
+
emitStream: (payload?: ApiStreamPayload) => void;
|
|
7
|
+
flushPressure: ApiFlushPressure;
|
|
8
|
+
cleanupRequest: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const createApiRequestLifecycle: ({ socket, responseIndex, resolvedName }: {
|
|
11
|
+
socket: Socket;
|
|
12
|
+
responseIndex: number;
|
|
13
|
+
resolvedName: string;
|
|
14
|
+
}) => ApiRequestLifecycle;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';
|
|
2
|
+
import type { ApiResponseEnvelope, RuntimeApiResponse } from './apiTypes.js';
|
|
3
|
+
export declare const normalizeApiResponse: ({ resolvedName, error, result, preferredLocale, user, }: {
|
|
4
|
+
resolvedName: string;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
result: RuntimeApiResponse | undefined | null;
|
|
7
|
+
preferredLocale: string | null | undefined;
|
|
8
|
+
user: SessionLayout | null;
|
|
9
|
+
}) => ApiResponseEnvelope;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BaseSessionLayout as SessionLayout } from '@luckystack/core';
|
|
2
|
+
import type { RuntimeApiEntry } from './apiTypes.js';
|
|
3
|
+
type EmitInvalidInputType = () => void | Promise<void>;
|
|
4
|
+
export declare const resolveValidationMode: (validation: RuntimeApiEntry['validation']) => 'strict' | 'relaxed';
|
|
5
|
+
export declare const runSocketApiValidation: ({ apiEntry, resolvedName, normalizedData, user, cleanupRequest, emitInvalidInputType, }: {
|
|
6
|
+
apiEntry: RuntimeApiEntry;
|
|
7
|
+
resolvedName: string;
|
|
8
|
+
normalizedData: Record<string, unknown>;
|
|
9
|
+
user: SessionLayout | null;
|
|
10
|
+
cleanupRequest: () => void;
|
|
11
|
+
emitInvalidInputType: EmitInvalidInputType;
|
|
12
|
+
}) => Promise<boolean>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { apiMessage } from '@luckystack/core';
|
|
2
|
+
import { Socket } from 'socket.io';
|
|
3
|
+
interface handleApiRequestType {
|
|
4
|
+
msg: apiMessage;
|
|
5
|
+
socket: Socket;
|
|
6
|
+
token: string | null;
|
|
7
|
+
}
|
|
8
|
+
export default function handleApiRequest(args: handleApiRequestType): Promise<void>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { HttpMethod } from '@luckystack/core';
|
|
2
|
+
import type { ApiStreamPayload } from './_shared/apiTypes.js';
|
|
3
|
+
/**
|
|
4
|
+
* HTTP API Request Handler
|
|
5
|
+
*
|
|
6
|
+
* Handles API requests coming via HTTP (instead of WebSocket).
|
|
7
|
+
* Reuses existing API handlers but returns results as HTTP response.
|
|
8
|
+
*
|
|
9
|
+
* Payload format:
|
|
10
|
+
* ```json
|
|
11
|
+
* {
|
|
12
|
+
* "name": "api/examples/publicApi",
|
|
13
|
+
* "data": { "message": "hello" }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // In server.ts
|
|
20
|
+
* if (pathname.startsWith('/api/')) {
|
|
21
|
+
* const token = extractTokenFromRequest(req);
|
|
22
|
+
* const body = await parseJsonBody(req);
|
|
23
|
+
* const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
|
|
24
|
+
* res.end(JSON.stringify(result));
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
interface HttpApiRequestParams {
|
|
29
|
+
name: string;
|
|
30
|
+
data: Record<string, unknown>;
|
|
31
|
+
token: string | null;
|
|
32
|
+
requesterIp?: string;
|
|
33
|
+
xLanguageHeader?: string | string[];
|
|
34
|
+
acceptLanguageHeader?: string | string[];
|
|
35
|
+
/** HTTP method from the request */
|
|
36
|
+
method?: HttpMethod;
|
|
37
|
+
stream?: (payload: ApiHttpStreamEvent) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this
|
|
40
|
+
* to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight
|
|
41
|
+
* stream emits. Handlers receive it as `abortSignal` in params.
|
|
42
|
+
*/
|
|
43
|
+
abortSignal?: AbortSignal;
|
|
44
|
+
}
|
|
45
|
+
type ApiNetworkResponse<T = Record<string, unknown>> = ({
|
|
46
|
+
status: 'success';
|
|
47
|
+
httpStatus: number;
|
|
48
|
+
} & T) | {
|
|
49
|
+
status: 'error';
|
|
50
|
+
httpStatus: number;
|
|
51
|
+
message: string;
|
|
52
|
+
errorCode: string;
|
|
53
|
+
errorParams?: {
|
|
54
|
+
key: string;
|
|
55
|
+
value: string | number | boolean;
|
|
56
|
+
}[];
|
|
57
|
+
};
|
|
58
|
+
export type ApiHttpStreamEvent = ApiStreamPayload;
|
|
59
|
+
export declare function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse>;
|
|
60
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,72 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
interface handleApiRequestType {
|
|
5
|
-
msg: apiMessage;
|
|
6
|
-
socket: Socket;
|
|
7
|
-
token: string | null;
|
|
8
|
-
}
|
|
9
|
-
declare function handleApiRequest(args: handleApiRequestType): Promise<void>;
|
|
10
|
-
|
|
11
|
-
/** Loosely-typed stream payload emitted by streaming endpoints. */
|
|
12
|
-
type ApiStreamPayload = Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* HTTP API Request Handler
|
|
16
|
-
*
|
|
17
|
-
* Handles API requests coming via HTTP (instead of WebSocket).
|
|
18
|
-
* Reuses existing API handlers but returns results as HTTP response.
|
|
19
|
-
*
|
|
20
|
-
* Payload format:
|
|
21
|
-
* ```json
|
|
22
|
-
* {
|
|
23
|
-
* "name": "api/examples/publicApi",
|
|
24
|
-
* "data": { "message": "hello" }
|
|
25
|
-
* }
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* // In server.ts
|
|
31
|
-
* if (pathname.startsWith('/api/')) {
|
|
32
|
-
* const token = extractTokenFromRequest(req);
|
|
33
|
-
* const body = await parseJsonBody(req);
|
|
34
|
-
* const result = await handleHttpApiRequest({ name: body.name, data: body.data, token });
|
|
35
|
-
* res.end(JSON.stringify(result));
|
|
36
|
-
* }
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
interface HttpApiRequestParams {
|
|
40
|
-
name: string;
|
|
41
|
-
data: Record<string, unknown>;
|
|
42
|
-
token: string | null;
|
|
43
|
-
requesterIp?: string;
|
|
44
|
-
xLanguageHeader?: string | string[];
|
|
45
|
-
acceptLanguageHeader?: string | string[];
|
|
46
|
-
/** HTTP method from the request */
|
|
47
|
-
method?: HttpMethod;
|
|
48
|
-
stream?: (payload: ApiHttpStreamEvent) => void;
|
|
49
|
-
/**
|
|
50
|
-
* Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this
|
|
51
|
-
* to `req.on('close', ...)` so a closed SSE/HTTP connection aborts in-flight
|
|
52
|
-
* stream emits. Handlers receive it as `abortSignal` in params.
|
|
53
|
-
*/
|
|
54
|
-
abortSignal?: AbortSignal;
|
|
55
|
-
}
|
|
56
|
-
type ApiNetworkResponse<T = Record<string, unknown>> = ({
|
|
57
|
-
status: 'success';
|
|
58
|
-
httpStatus: number;
|
|
59
|
-
} & T) | {
|
|
60
|
-
status: 'error';
|
|
61
|
-
httpStatus: number;
|
|
62
|
-
message: string;
|
|
63
|
-
errorCode: string;
|
|
64
|
-
errorParams?: {
|
|
65
|
-
key: string;
|
|
66
|
-
value: string | number | boolean;
|
|
67
|
-
}[];
|
|
68
|
-
};
|
|
69
|
-
type ApiHttpStreamEvent = ApiStreamPayload;
|
|
70
|
-
declare function handleHttpApiRequest(params: HttpApiRequestParams): Promise<ApiNetworkResponse>;
|
|
71
|
-
|
|
72
|
-
export { type ApiHttpStreamEvent, handleApiRequest, handleHttpApiRequest };
|
|
1
|
+
export { default as handleApiRequest } from './handleApiRequest.js';
|
|
2
|
+
export { handleHttpApiRequest } from './handleHttpApiRequest.js';
|
|
3
|
+
export type { ApiHttpStreamEvent } from './handleHttpApiRequest.js';
|