stitchkit 0.0.1 → 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/LICENSE +21 -0
- package/README.md +376 -4
- package/dist/browser/client.d.ts +30 -0
- package/dist/browser/client.d.ts.map +1 -0
- package/dist/browser/http.d.ts +77 -0
- package/dist/browser/http.d.ts.map +1 -0
- package/dist/browser/socket-io.d.ts +69 -0
- package/dist/browser/socket-io.d.ts.map +1 -0
- package/dist/browser/stream.d.ts +2 -0
- package/dist/browser/stream.d.ts.map +1 -0
- package/dist/contract/define.d.ts +94 -0
- package/dist/contract/define.d.ts.map +1 -0
- package/dist/contract/errors.d.ts +47 -0
- package/dist/contract/errors.d.ts.map +1 -0
- package/dist/contract/index.d.ts +4 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +26 -0
- package/dist/contract/pagination.d.ts +19 -0
- package/dist/contract/pagination.d.ts.map +1 -0
- package/dist/index-5sxnvwb1.js +368 -0
- package/dist/index-809wc1tt.js +18 -0
- package/dist/index-a35v22fh.js +29 -0
- package/dist/index-kckky6zw.js +96 -0
- package/dist/index-ke4mx4ea.js +38 -0
- package/dist/index-n7bmdwmz.js +68 -0
- package/dist/index-v2z2v3mq.js +587 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/internal/errors.d.ts +5 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/typed.d.ts +9 -0
- package/dist/internal/typed.d.ts.map +1 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +26 -0
- package/dist/observability/audit.d.ts +32 -0
- package/dist/observability/audit.d.ts.map +1 -0
- package/dist/observability/context.d.ts +60 -0
- package/dist/observability/context.d.ts.map +1 -0
- package/dist/observability/event.d.ts +49 -0
- package/dist/observability/event.d.ts.map +1 -0
- package/dist/observability/index.d.ts +14 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +266 -0
- package/dist/observability/sanitize.d.ts +42 -0
- package/dist/observability/sanitize.d.ts.map +1 -0
- package/dist/observability/trace.d.ts +42 -0
- package/dist/observability/trace.d.ts.map +1 -0
- package/dist/react/cache-bridge.d.ts +53 -0
- package/dist/react/cache-bridge.d.ts.map +1 -0
- package/dist/react/cursor-query.d.ts +37 -0
- package/dist/react/cursor-query.d.ts.map +1 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +52 -0
- package/dist/server/cache.d.ts +15 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/context.d.ts +9 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/create.d.ts +4 -0
- package/dist/server/create.d.ts.map +1 -0
- package/dist/server/event-bus.d.ts +24 -0
- package/dist/server/event-bus.d.ts.map +1 -0
- package/dist/server/implement.d.ts +16 -0
- package/dist/server/implement.d.ts.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +399 -0
- package/dist/server/logger.d.ts +21 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/middleware/auth.d.ts +50 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/cookies.d.ts +34 -0
- package/dist/server/middleware/cookies.d.ts.map +1 -0
- package/dist/server/middleware/cors.d.ts +9 -0
- package/dist/server/middleware/cors.d.ts.map +1 -0
- package/dist/server/multipart.d.ts +14 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/node.d.ts +12 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/rate-limit.d.ts +17 -0
- package/dist/server/rate-limit.d.ts.map +1 -0
- package/dist/server/request.d.ts +22 -0
- package/dist/server/request.d.ts.map +1 -0
- package/dist/server/router.d.ts +42 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/socket-io.d.ts +42 -0
- package/dist/server/socket-io.d.ts.map +1 -0
- package/dist/server/stream.d.ts +18 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/swept-map.d.ts +22 -0
- package/dist/server/swept-map.d.ts.map +1 -0
- package/dist/server/types.d.ts +127 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/tools/agent.d.ts +27 -0
- package/dist/tools/agent.d.ts.map +1 -0
- package/dist/tools/coerce.d.ts +8 -0
- package/dist/tools/coerce.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +45 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/flatten.d.ts +15 -0
- package/dist/tools/flatten.d.ts.map +1 -0
- package/dist/tools/json-schema.d.ts +18 -0
- package/dist/tools/json-schema.d.ts.map +1 -0
- package/dist/tools/manifest.d.ts +13 -0
- package/dist/tools/manifest.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts +21 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.d.ts +25 -0
- package/dist/tools/mcp-stdio.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts +92 -0
- package/dist/tools/mcp.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +73 -0
- package/dist/tools/mount.d.ts.map +1 -0
- package/dist/tools/names.d.ts +2 -0
- package/dist/tools/names.d.ts.map +1 -0
- package/dist/tools/remote.d.ts +22 -0
- package/dist/tools/remote.d.ts.map +1 -0
- package/dist/tools/schema.d.ts +21 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +57 -0
- package/dist/tools/view-file.d.ts.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +837 -0
- package/package.json +147 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ZodType, z } from 'zod';
|
|
2
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
3
|
+
export declare const ALL_TRANSPORTS: readonly ["HTTP", "MCP", "AGENT"];
|
|
4
|
+
export type Transport = (typeof ALL_TRANSPORTS)[number];
|
|
5
|
+
export type TransportSource = 'http' | 'mcp' | 'agent';
|
|
6
|
+
interface EndpointDefBase {
|
|
7
|
+
method: HttpMethod;
|
|
8
|
+
path: string;
|
|
9
|
+
desc: string;
|
|
10
|
+
scope?: string;
|
|
11
|
+
params?: ZodType<unknown>;
|
|
12
|
+
input?: ZodType<unknown>;
|
|
13
|
+
output?: ZodType<unknown>;
|
|
14
|
+
multipart?: string;
|
|
15
|
+
/**
|
|
16
|
+
* HTTP client timeout in ms for this endpoint. Use it for slow synchronous
|
|
17
|
+
* endpoints (AI generation) that need more than the client default. A
|
|
18
|
+
* property of the endpoint — declared once, the typed client applies it.
|
|
19
|
+
*/
|
|
20
|
+
timeout?: number;
|
|
21
|
+
}
|
|
22
|
+
interface HttpOnlyEndpointDef extends EndpointDefBase {
|
|
23
|
+
expose: readonly ['HTTP'];
|
|
24
|
+
toolName?: never;
|
|
25
|
+
}
|
|
26
|
+
interface ToolEndpointDef extends EndpointDefBase {
|
|
27
|
+
toolName?: string;
|
|
28
|
+
expose?: readonly Transport[];
|
|
29
|
+
}
|
|
30
|
+
export type EndpointDef = HttpOnlyEndpointDef | ToolEndpointDef;
|
|
31
|
+
export interface ContractMeta<TScope extends string = string> {
|
|
32
|
+
prefix: string;
|
|
33
|
+
scope?: TScope;
|
|
34
|
+
}
|
|
35
|
+
export interface ContractDef<T extends Record<string, EndpointDef> = Record<string, EndpointDef>, TScope extends string = string> {
|
|
36
|
+
meta: ContractMeta<TScope>;
|
|
37
|
+
endpoints: T;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Declare an API contract — a `prefix` plus a map of endpoints (method, path,
|
|
41
|
+
* Zod `params` / `input` / `output`, `scope`, `expose`). One contract drives
|
|
42
|
+
* the HTTP routes, the MCP and agent tools, and the typed client. Throws at
|
|
43
|
+
* definition time on a duplicate `toolName`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function defineContract<const T extends Record<string, EndpointDef>>(meta: {
|
|
46
|
+
prefix: string;
|
|
47
|
+
}, endpoints: T): ContractDef<T, 'public'>;
|
|
48
|
+
export declare function defineContract<TScope extends string, const T extends Record<string, EndpointDef>>(meta: {
|
|
49
|
+
prefix: string;
|
|
50
|
+
scope: TScope;
|
|
51
|
+
}, endpoints: T): ContractDef<T, TScope>;
|
|
52
|
+
export interface RuntimeContext {
|
|
53
|
+
params: unknown;
|
|
54
|
+
input: unknown;
|
|
55
|
+
file?: File;
|
|
56
|
+
source: TransportSource;
|
|
57
|
+
traceId?: string;
|
|
58
|
+
spanId?: string;
|
|
59
|
+
ipAddress?: string;
|
|
60
|
+
userAgent?: string;
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
export interface HandlerContext<TParams = undefined, TInput = undefined> {
|
|
64
|
+
params: TParams;
|
|
65
|
+
input: TInput;
|
|
66
|
+
file?: File;
|
|
67
|
+
source: TransportSource;
|
|
68
|
+
traceId?: string;
|
|
69
|
+
spanId?: string;
|
|
70
|
+
ipAddress?: string;
|
|
71
|
+
userAgent?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
type Prop<T, K extends string> = K extends keyof T ? T[K] : undefined;
|
|
75
|
+
type InferInput<S> = S extends ZodType ? z.input<S> : unknown;
|
|
76
|
+
type MultipartArgs<E> = E extends {
|
|
77
|
+
multipart: infer K extends string;
|
|
78
|
+
} ? {
|
|
79
|
+
[P in K]: Blob;
|
|
80
|
+
} : unknown;
|
|
81
|
+
type EndpointArgs<E> = InferInput<Prop<E, 'params'>> & InferInput<Prop<E, 'input'>> & MultipartArgs<E>;
|
|
82
|
+
type EndpointOutput<E> = Prop<E, 'output'> extends ZodType<infer O> ? O : undefined;
|
|
83
|
+
export type EndpointFn<E> = [keyof EndpointArgs<E>] extends [never] ? () => Promise<EndpointOutput<E>> : (args: EndpointArgs<E>) => Promise<EndpointOutput<E>>;
|
|
84
|
+
export type TypedClient<C extends Record<string, EndpointDef>> = {
|
|
85
|
+
[K in keyof C]: EndpointFn<C[K]>;
|
|
86
|
+
};
|
|
87
|
+
type ExposesHttp<E> = E extends {
|
|
88
|
+
expose: readonly Transport[];
|
|
89
|
+
} ? 'HTTP' extends E['expose'][number] ? true : false : true;
|
|
90
|
+
export type TypedHttpClient<C extends Record<string, EndpointDef>> = {
|
|
91
|
+
[K in keyof C as ExposesHttp<C[K]> extends true ? K : never]: EndpointFn<C[K]>;
|
|
92
|
+
};
|
|
93
|
+
export {};
|
|
94
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/contract/define.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public error envelope — the JSON shape of every HTTP error response, and
|
|
3
|
+
* the one type the server (`AppError.toJSON`) and the typed HTTP client are
|
|
4
|
+
* declared against. Tool (MCP / agent) errors use a flatter, model-facing
|
|
5
|
+
* shape instead — see `formatToolError` in `tools/mount.ts`.
|
|
6
|
+
*/
|
|
7
|
+
export interface ErrorEnvelope {
|
|
8
|
+
error: {
|
|
9
|
+
/** Stable, machine-readable error code. */
|
|
10
|
+
code: string;
|
|
11
|
+
/** Human-readable message. */
|
|
12
|
+
message?: string;
|
|
13
|
+
/** Structured error context. */
|
|
14
|
+
details?: unknown;
|
|
15
|
+
/** A remediation hint. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The framework's error model — a stable `code`, an HTTP `status`, optional
|
|
21
|
+
* structured `details` and a `hint`. `toJSON()` renders the public error
|
|
22
|
+
* envelope. Throw it directly, or via the typed helpers below.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AppError extends Error {
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly status: number;
|
|
27
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
28
|
+
readonly hint?: string | undefined;
|
|
29
|
+
constructor(code: string, message?: string, status?: number, details?: Record<string, unknown> | undefined, hint?: string | undefined);
|
|
30
|
+
static is(err: unknown): err is AppError;
|
|
31
|
+
toJSON(): ErrorEnvelope;
|
|
32
|
+
}
|
|
33
|
+
/** Throw a `404 NOT_FOUND` error. */
|
|
34
|
+
export declare function notFound(message?: string): never;
|
|
35
|
+
/** Throw a `400 BAD_REQUEST` error, optionally with structured `details`. */
|
|
36
|
+
export declare function badRequest(message: string, details?: Record<string, unknown>): never;
|
|
37
|
+
/** Throw a `401 UNAUTHORIZED` error. */
|
|
38
|
+
export declare function unauthorized(message?: string): never;
|
|
39
|
+
/** Throw a `403 FORBIDDEN` error. */
|
|
40
|
+
export declare function forbidden(message?: string): never;
|
|
41
|
+
/** Throw a `409 CONFLICT` error, optionally with structured `details`. */
|
|
42
|
+
export declare function conflict(message?: string, details?: Record<string, unknown>): never;
|
|
43
|
+
/** Throw a `429 RATE_LIMITED` error. */
|
|
44
|
+
export declare function rateLimited(message?: string): never;
|
|
45
|
+
/** Throw an `AppError` for any `code`, mapping known codes to their HTTP status (else 500). */
|
|
46
|
+
export declare function appError(code: string, message?: string, details?: Record<string, unknown>): never;
|
|
47
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/contract/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE;QACL,2CAA2C;QAC3C,IAAI,EAAE,MAAM,CAAC;QACb,8BAA8B;QAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,gCAAgC;QAChC,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,0BAA0B;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED;;;;GAIG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAEf,IAAI,EAAE,MAAM;aAEZ,MAAM,EAAE,MAAM;aACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;aACjC,IAAI,CAAC,EAAE,MAAM;gBAJb,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,MAAM,EACA,MAAM,GAAE,MAAY,EACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA,EACjC,IAAI,CAAC,EAAE,MAAM,YAAA;IAM/B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,QAAQ;IAIxC,MAAM,IAAI,aAAa;CAUxB;AAED,qCAAqC;AACrC,wBAAgB,QAAQ,CAAC,OAAO,SAAc,GAAG,KAAK,CAErD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEpF;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,OAAO,SAAiB,GAAG,KAAK,CAE5D;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,OAAO,SAAc,GAAG,KAAK,CAEtD;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,OAAO,SAAa,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CAEvF;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,OAAO,SAAsB,GAAG,KAAK,CAEhE;AAaD,+FAA+F;AAC/F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,KAAK,CAEP"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ALL_TRANSPORTS, type ContractDef, type ContractMeta, defineContract, type EndpointDef, type EndpointFn, type HandlerContext, type HttpMethod, type RuntimeContext, type Transport, type TransportSource, type TypedClient, type TypedHttpClient, } from './define';
|
|
2
|
+
export { AppError, appError, badRequest, conflict, type ErrorEnvelope, forbidden, notFound, rateLimited, unauthorized, } from './errors';
|
|
3
|
+
export { type Paginated, paginatedSchema } from './pagination';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,aAAa,EAClB,SAAS,EACT,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,KAAK,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_TRANSPORTS,
|
|
3
|
+
AppError,
|
|
4
|
+
appError,
|
|
5
|
+
badRequest,
|
|
6
|
+
conflict,
|
|
7
|
+
defineContract,
|
|
8
|
+
forbidden,
|
|
9
|
+
notFound,
|
|
10
|
+
paginatedSchema,
|
|
11
|
+
rateLimited,
|
|
12
|
+
unauthorized
|
|
13
|
+
} from "../index-kckky6zw.js";
|
|
14
|
+
export {
|
|
15
|
+
unauthorized,
|
|
16
|
+
rateLimited,
|
|
17
|
+
paginatedSchema,
|
|
18
|
+
notFound,
|
|
19
|
+
forbidden,
|
|
20
|
+
defineContract,
|
|
21
|
+
conflict,
|
|
22
|
+
badRequest,
|
|
23
|
+
appError,
|
|
24
|
+
AppError,
|
|
25
|
+
ALL_TRANSPORTS
|
|
26
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ZodType, z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Cursor-paginated response envelope — the family standard.
|
|
4
|
+
*
|
|
5
|
+
* Every list endpoint returns this shape: an `items` array plus an opaque
|
|
6
|
+
* `nextCursor` (`null` at the end). One envelope → one infinite-query helper
|
|
7
|
+
* (`createCursorQuery`) with zero per-endpoint accessors. Domain extras (a
|
|
8
|
+
* `total` counter, `stats`) are added alongside via `.extend()`.
|
|
9
|
+
*/
|
|
10
|
+
export interface Paginated<T> {
|
|
11
|
+
items: T[];
|
|
12
|
+
nextCursor: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** Zod schema for a `Paginated<T>` response of `itemSchema`. */
|
|
15
|
+
export declare function paginatedSchema<T extends ZodType>(itemSchema: T): z.ZodObject<{
|
|
16
|
+
items: z.ZodArray<T>;
|
|
17
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/contract/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,CAAC,SAAS,OAAO,EAAE,UAAU,EAAE,CAAC;;;kBAK/D"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
mapObject,
|
|
4
|
+
typedEntries
|
|
5
|
+
} from "./index-809wc1tt.js";
|
|
6
|
+
|
|
7
|
+
// src/browser/http.ts
|
|
8
|
+
import ky, { isHTTPError } from "ky";
|
|
9
|
+
class ApiError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
details;
|
|
13
|
+
hint;
|
|
14
|
+
constructor(code, status = 0, details, message, hint) {
|
|
15
|
+
super(message ?? `API Error: ${code}`);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.details = details;
|
|
19
|
+
this.hint = hint;
|
|
20
|
+
this.name = "ApiError";
|
|
21
|
+
}
|
|
22
|
+
static is(error) {
|
|
23
|
+
return error instanceof ApiError;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function parseApiErrorBody(body) {
|
|
27
|
+
if (!isRecord(body) || !isRecord(body.error))
|
|
28
|
+
return null;
|
|
29
|
+
const error = body.error;
|
|
30
|
+
if (typeof error.code !== "string")
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
code: error.code,
|
|
34
|
+
message: typeof error.message === "string" ? error.message : undefined,
|
|
35
|
+
details: error.details,
|
|
36
|
+
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function createHttpClient(config) {
|
|
40
|
+
let ssrCookies = null;
|
|
41
|
+
let isLoggedOut = false;
|
|
42
|
+
const listeners = new Set;
|
|
43
|
+
const authEndpoints = config.authEndpoints ?? ["auth/"];
|
|
44
|
+
const parseError = config.parseError ?? parseApiErrorBody;
|
|
45
|
+
function emit(event) {
|
|
46
|
+
for (const fn of listeners) {
|
|
47
|
+
try {
|
|
48
|
+
fn(event);
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const client = ky.create({
|
|
53
|
+
prefix: config.baseUrl,
|
|
54
|
+
credentials: config.credentials ?? "include",
|
|
55
|
+
timeout: config.timeout ?? 30000,
|
|
56
|
+
retry: {
|
|
57
|
+
limit: config.retry?.limit ?? 2,
|
|
58
|
+
methods: config.retry?.methods ?? ["get"],
|
|
59
|
+
statusCodes: config.retry?.statusCodes ?? []
|
|
60
|
+
},
|
|
61
|
+
hooks: {
|
|
62
|
+
beforeRequest: [
|
|
63
|
+
({ request: request2 }) => {
|
|
64
|
+
if (ssrCookies) {
|
|
65
|
+
request2.headers.set("Cookie", ssrCookies);
|
|
66
|
+
}
|
|
67
|
+
const extra = typeof config.headers === "function" ? config.headers() : config.headers;
|
|
68
|
+
if (extra) {
|
|
69
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
70
|
+
request2.headers.set(key, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
afterResponse: [
|
|
76
|
+
async ({ request: request2, response }) => {
|
|
77
|
+
if (response.status === 401) {
|
|
78
|
+
const url = new URL(request2.url).pathname;
|
|
79
|
+
if (!isLoggedOut && !authEndpoints.some((path) => url.includes(path))) {
|
|
80
|
+
isLoggedOut = true;
|
|
81
|
+
emit({ type: "unauthorized" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const body = await response.clone().json().catch(() => null);
|
|
86
|
+
if (body) {
|
|
87
|
+
const parsed = parseError(body);
|
|
88
|
+
if (parsed) {
|
|
89
|
+
throw new ApiError(parsed.code, response.status, parsed.details, parsed.message, parsed.hint);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
async function request(method, url, data, options = {}) {
|
|
98
|
+
const kyOptions = {
|
|
99
|
+
timeout: options.timeout
|
|
100
|
+
};
|
|
101
|
+
if (options.params) {
|
|
102
|
+
const searchParams = new URLSearchParams;
|
|
103
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
104
|
+
if (value === undefined)
|
|
105
|
+
continue;
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
for (const item of value)
|
|
108
|
+
searchParams.append(key, String(item));
|
|
109
|
+
} else {
|
|
110
|
+
searchParams.set(key, String(value));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (searchParams.size > 0) {
|
|
114
|
+
kyOptions.searchParams = searchParams;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (data instanceof FormData) {
|
|
118
|
+
kyOptions.body = data;
|
|
119
|
+
} else if (data !== undefined) {
|
|
120
|
+
kyOptions.json = data;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
if (options.responseType === "blob") {
|
|
124
|
+
return client[method](url, kyOptions).blob();
|
|
125
|
+
}
|
|
126
|
+
const response = await client[method](url, kyOptions);
|
|
127
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
return response.json();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (ApiError.is(error))
|
|
133
|
+
throw error;
|
|
134
|
+
const isAbort = error instanceof Error && error.name === "AbortError";
|
|
135
|
+
if (!isAbort) {
|
|
136
|
+
emit({ type: "network_error" });
|
|
137
|
+
}
|
|
138
|
+
const status = isAbort ? 0 : isHTTPError(error) ? error.response.status : 0;
|
|
139
|
+
const msg = error instanceof Error ? error.message : undefined;
|
|
140
|
+
throw new ApiError("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
get: (url, options) => request("get", url, undefined, options),
|
|
145
|
+
post: (url, data, options) => request("post", url, data, options),
|
|
146
|
+
put: (url, data, options) => request("put", url, data, options),
|
|
147
|
+
patch: (url, data, options) => request("patch", url, data, options),
|
|
148
|
+
delete: (url, options) => request("delete", url, undefined, options),
|
|
149
|
+
setServerContext(cookies) {
|
|
150
|
+
ssrCookies = cookies;
|
|
151
|
+
},
|
|
152
|
+
subscribe(listener) {
|
|
153
|
+
listeners.add(listener);
|
|
154
|
+
return () => listeners.delete(listener);
|
|
155
|
+
},
|
|
156
|
+
logout() {
|
|
157
|
+
isLoggedOut = true;
|
|
158
|
+
emit({ type: "logout" });
|
|
159
|
+
},
|
|
160
|
+
resetLogoutState() {
|
|
161
|
+
isLoggedOut = false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/browser/client.ts
|
|
167
|
+
function withTimeout(options, timeout) {
|
|
168
|
+
if (timeout === undefined)
|
|
169
|
+
return options;
|
|
170
|
+
return { ...options, timeout };
|
|
171
|
+
}
|
|
172
|
+
function isParamArray(value) {
|
|
173
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
|
|
174
|
+
}
|
|
175
|
+
function collectQueryParams(args, skipKeys) {
|
|
176
|
+
const params = {};
|
|
177
|
+
let hasParams = false;
|
|
178
|
+
for (const [key, value] of Object.entries(args)) {
|
|
179
|
+
if (skipKeys.has(key) || value === undefined || value === null)
|
|
180
|
+
continue;
|
|
181
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
182
|
+
params[key] = value;
|
|
183
|
+
hasParams = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return hasParams ? params : undefined;
|
|
187
|
+
}
|
|
188
|
+
function createClient(contract, configOrClient, contractConfig) {
|
|
189
|
+
const client = {};
|
|
190
|
+
const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient);
|
|
191
|
+
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
192
|
+
if (endpoint.expose && !endpoint.expose.includes("HTTP"))
|
|
193
|
+
continue;
|
|
194
|
+
setClientMethod(client, key, makeMethod(endpoint));
|
|
195
|
+
}
|
|
196
|
+
return client;
|
|
197
|
+
}
|
|
198
|
+
function createClients(contracts, http) {
|
|
199
|
+
return mapObject(contracts, (_key, contract) => createClient(contract, http));
|
|
200
|
+
}
|
|
201
|
+
function isHttpAdapter(value) {
|
|
202
|
+
return typeof value === "object" && "get" in value && typeof value.get === "function";
|
|
203
|
+
}
|
|
204
|
+
function setClientMethod(target, key, method) {
|
|
205
|
+
target[key] = method;
|
|
206
|
+
}
|
|
207
|
+
function createHttpMethod(endpoint, prefix, client, config) {
|
|
208
|
+
const httpMethod = endpoint.method.toLowerCase();
|
|
209
|
+
const isGet = httpMethod === "get";
|
|
210
|
+
const paramNames = extractParamNames(endpoint.path);
|
|
211
|
+
const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
|
|
212
|
+
return (...args) => {
|
|
213
|
+
const firstArg = args[0] ?? {};
|
|
214
|
+
let pathPrefixStr = "";
|
|
215
|
+
if (config?.pathPrefix) {
|
|
216
|
+
pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
|
|
217
|
+
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
218
|
+
pathPrefixStr += "/";
|
|
219
|
+
}
|
|
220
|
+
let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
|
|
221
|
+
for (const name of paramNames) {
|
|
222
|
+
const value = firstArg[name];
|
|
223
|
+
if (value === undefined || value === null) {
|
|
224
|
+
throw new Error(`Missing path param: ${name}`);
|
|
225
|
+
}
|
|
226
|
+
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
227
|
+
}
|
|
228
|
+
if (url.endsWith("/"))
|
|
229
|
+
url = url.slice(0, -1);
|
|
230
|
+
if (endpoint.multipart) {
|
|
231
|
+
const file = firstArg[endpoint.multipart];
|
|
232
|
+
if (!(file instanceof Blob)) {
|
|
233
|
+
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
234
|
+
}
|
|
235
|
+
const formData = new FormData;
|
|
236
|
+
formData.append(endpoint.multipart, file);
|
|
237
|
+
appendFormFields(formData, firstArg, new Set([...prefixKeys, endpoint.multipart]));
|
|
238
|
+
return client.post(url, formData, withTimeout(undefined, endpoint.timeout));
|
|
239
|
+
}
|
|
240
|
+
if (isGet) {
|
|
241
|
+
const params = collectQueryParams(firstArg, prefixKeys);
|
|
242
|
+
return client.get(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
|
|
243
|
+
}
|
|
244
|
+
if (httpMethod === "delete") {
|
|
245
|
+
const params = collectQueryParams(firstArg, prefixKeys);
|
|
246
|
+
return client.delete(url, withTimeout(params ? { params } : undefined, endpoint.timeout));
|
|
247
|
+
}
|
|
248
|
+
const payload = {};
|
|
249
|
+
for (const [key, value] of Object.entries(firstArg)) {
|
|
250
|
+
if (!prefixKeys.has(key) && value !== undefined) {
|
|
251
|
+
payload[key] = value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint.timeout));
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function createFetchMethod(endpoint, prefix, config) {
|
|
258
|
+
return async (args) => {
|
|
259
|
+
let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args);
|
|
260
|
+
const headers = {
|
|
261
|
+
Accept: "application/json",
|
|
262
|
+
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
263
|
+
};
|
|
264
|
+
const isQuery = endpoint.method === "GET" || endpoint.method === "DELETE";
|
|
265
|
+
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
266
|
+
if (isQuery && args) {
|
|
267
|
+
const remaining = stripParams(args, endpoint.path);
|
|
268
|
+
const searchParams = new URLSearchParams;
|
|
269
|
+
for (const [k, v] of Object.entries(remaining)) {
|
|
270
|
+
if (v === undefined || v === null)
|
|
271
|
+
continue;
|
|
272
|
+
if (isParamArray(v)) {
|
|
273
|
+
for (const item of v)
|
|
274
|
+
searchParams.append(k, String(item));
|
|
275
|
+
} else if (typeof v !== "object") {
|
|
276
|
+
searchParams.set(k, String(v));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (searchParams.size > 0)
|
|
280
|
+
url += `?${searchParams}`;
|
|
281
|
+
}
|
|
282
|
+
if (hasBody)
|
|
283
|
+
headers["Content-Type"] = "application/json";
|
|
284
|
+
if (endpoint.multipart && args) {
|
|
285
|
+
const file = args[endpoint.multipart];
|
|
286
|
+
if (!(file instanceof Blob)) {
|
|
287
|
+
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
288
|
+
}
|
|
289
|
+
const formData = new FormData;
|
|
290
|
+
formData.append(endpoint.multipart, file);
|
|
291
|
+
appendFormFields(formData, stripParams(args, endpoint.path), new Set([endpoint.multipart]));
|
|
292
|
+
const res2 = await fetch(url, {
|
|
293
|
+
method: endpoint.method,
|
|
294
|
+
headers,
|
|
295
|
+
credentials: config.credentials,
|
|
296
|
+
body: formData
|
|
297
|
+
});
|
|
298
|
+
if (!res2.ok) {
|
|
299
|
+
await throwForErrorResponse(res2, config, null);
|
|
300
|
+
}
|
|
301
|
+
if (res2.status === 204)
|
|
302
|
+
return;
|
|
303
|
+
const json2 = await res2.json();
|
|
304
|
+
return endpoint.output ? endpoint.output.parse(json2) : json2;
|
|
305
|
+
}
|
|
306
|
+
const res = await fetch(url, {
|
|
307
|
+
method: endpoint.method,
|
|
308
|
+
headers,
|
|
309
|
+
credentials: config.credentials,
|
|
310
|
+
...hasBody && { body: JSON.stringify(stripParams(hasBody, endpoint.path)) }
|
|
311
|
+
});
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
await throwForErrorResponse(res, config, { error: res.statusText });
|
|
314
|
+
}
|
|
315
|
+
if (res.status === 204)
|
|
316
|
+
return;
|
|
317
|
+
const json = await res.json();
|
|
318
|
+
return endpoint.output ? endpoint.output.parse(json) : json;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function appendFormFields(formData, values, skipKeys) {
|
|
322
|
+
for (const [key, value] of Object.entries(values)) {
|
|
323
|
+
if (skipKeys.has(key) || value === undefined || value === null)
|
|
324
|
+
continue;
|
|
325
|
+
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function throwForErrorResponse(res, config, fallbackBody) {
|
|
329
|
+
const body = await res.json().catch(() => fallbackBody);
|
|
330
|
+
config.onError?.(res.status, body);
|
|
331
|
+
const parsed = parseApiErrorBody(body);
|
|
332
|
+
if (parsed) {
|
|
333
|
+
throw new ApiError(parsed.code, res.status, parsed.details, parsed.message, parsed.hint);
|
|
334
|
+
}
|
|
335
|
+
throw new ApiError("HTTP_ERROR", res.status, { body });
|
|
336
|
+
}
|
|
337
|
+
function extractParamNames(path) {
|
|
338
|
+
const matches = path.match(/:(\w+)/g);
|
|
339
|
+
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
340
|
+
}
|
|
341
|
+
function buildFetchUrl(baseUrl, prefix, path, args) {
|
|
342
|
+
let fullPath = `/${prefix}${path === "/" ? "" : path}`;
|
|
343
|
+
if (args) {
|
|
344
|
+
fullPath = fullPath.replace(/:(\w+)/g, (_, key) => {
|
|
345
|
+
const val = args[key];
|
|
346
|
+
if (val === undefined || val === null) {
|
|
347
|
+
throw new Error(`Missing path param: ${key}`);
|
|
348
|
+
}
|
|
349
|
+
return encodeURIComponent(String(val));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return `${baseUrl}${fullPath}`;
|
|
353
|
+
}
|
|
354
|
+
function stripParams(args, path) {
|
|
355
|
+
const paramNames = new Set;
|
|
356
|
+
for (const match of path.matchAll(/:(\w+)/g)) {
|
|
357
|
+
if (match[1])
|
|
358
|
+
paramNames.add(match[1]);
|
|
359
|
+
}
|
|
360
|
+
const result = {};
|
|
361
|
+
for (const [k, v] of Object.entries(args)) {
|
|
362
|
+
if (!paramNames.has(k))
|
|
363
|
+
result[k] = v;
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { ApiError, createHttpClient, createClient, createClients };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/internal/typed.ts
|
|
2
|
+
function typedEntries(value) {
|
|
3
|
+
return Object.entries(value);
|
|
4
|
+
}
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function mapObject(source, mapper) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const [key, value] of typedEntries(source)) {
|
|
11
|
+
const mapped = mapper(key, value);
|
|
12
|
+
if (mapped !== undefined)
|
|
13
|
+
result[key] = mapped;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { typedEntries, isRecord, mapObject };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppError
|
|
3
|
+
} from "./index-kckky6zw.js";
|
|
4
|
+
|
|
5
|
+
// src/internal/errors.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
function formatZodError(error) {
|
|
8
|
+
const issues = error.issues.slice(0, 5);
|
|
9
|
+
const lines = issues.map((issue) => {
|
|
10
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
11
|
+
return `${path}: ${issue.message}`;
|
|
12
|
+
});
|
|
13
|
+
const suffix = error.issues.length > 5 ? `
|
|
14
|
+
...and ${error.issues.length - 5} more issues` : "";
|
|
15
|
+
return lines.join(`
|
|
16
|
+
`) + suffix;
|
|
17
|
+
}
|
|
18
|
+
function normalizeError(err) {
|
|
19
|
+
if (AppError.is(err))
|
|
20
|
+
return err;
|
|
21
|
+
if (err instanceof z.ZodError) {
|
|
22
|
+
return new AppError("VALIDATION_ERROR", formatZodError(err), 400);
|
|
23
|
+
}
|
|
24
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25
|
+
const safeMessage = msg.length > 200 ? `${msg.slice(0, 200)}...` : msg;
|
|
26
|
+
return new AppError("INTERNAL_SERVER_ERROR", safeMessage, 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { formatZodError, normalizeError };
|