silgi 0.0.13 → 0.1.0-beta.1
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/README.md +102 -1
- package/dist/_virtual/_rolldown/runtime.mjs +5 -0
- package/dist/adapters/astro.d.mts +17 -0
- package/dist/adapters/astro.mjs +24 -0
- package/dist/adapters/aws-lambda.d.mts +31 -0
- package/dist/adapters/aws-lambda.mjs +85 -0
- package/dist/adapters/elysia.d.mts +17 -0
- package/dist/adapters/elysia.mjs +76 -0
- package/dist/adapters/express.d.mts +16 -0
- package/dist/adapters/express.mjs +78 -0
- package/dist/adapters/fastify.d.mts +15 -0
- package/dist/adapters/fastify.mjs +78 -0
- package/dist/adapters/message-port.d.mts +37 -0
- package/dist/adapters/message-port.mjs +129 -0
- package/dist/adapters/nestjs.d.mts +25 -0
- package/dist/adapters/nestjs.mjs +91 -0
- package/dist/adapters/nextjs.d.mts +21 -0
- package/dist/adapters/nextjs.mjs +30 -0
- package/dist/adapters/peer.d.mts +27 -0
- package/dist/adapters/peer.mjs +36 -0
- package/dist/adapters/remix.d.mts +17 -0
- package/dist/adapters/remix.mjs +24 -0
- package/dist/adapters/solidstart.d.mts +14 -0
- package/dist/adapters/solidstart.mjs +30 -0
- package/dist/adapters/sveltekit.d.mts +18 -0
- package/dist/adapters/sveltekit.mjs +33 -0
- package/dist/analyze.mjs +26 -0
- package/dist/broker/index.d.mts +62 -0
- package/dist/broker/index.mjs +153 -0
- package/dist/broker/nats.d.mts +33 -0
- package/dist/broker/nats.mjs +31 -0
- package/dist/broker/redis.d.mts +51 -0
- package/dist/broker/redis.mjs +92 -0
- package/dist/builder.d.mts +36 -0
- package/dist/builder.mjs +51 -0
- package/dist/callable.d.mts +17 -0
- package/dist/callable.mjs +42 -0
- package/dist/client/adapters/fetch/index.d.mts +17 -0
- package/dist/client/adapters/fetch/index.mjs +61 -0
- package/dist/client/adapters/ofetch/index.d.mts +41 -0
- package/dist/client/adapters/ofetch/index.mjs +92 -0
- package/dist/client/client.d.mts +29 -0
- package/dist/client/client.mjs +54 -0
- package/dist/client/dynamic-link.d.mts +15 -0
- package/dist/client/dynamic-link.mjs +16 -0
- package/dist/client/index.d.mts +7 -0
- package/dist/client/index.mjs +6 -0
- package/dist/client/interceptor.d.mts +31 -0
- package/dist/client/interceptor.mjs +34 -0
- package/dist/client/merge.d.mts +28 -0
- package/dist/client/merge.mjs +30 -0
- package/dist/client/openapi.d.mts +29 -0
- package/dist/client/openapi.mjs +89 -0
- package/dist/client/plugins/batch.d.mts +20 -0
- package/dist/client/plugins/batch.mjs +64 -0
- package/dist/client/plugins/csrf.d.mts +13 -0
- package/dist/client/plugins/csrf.mjs +20 -0
- package/dist/client/plugins/dedupe.d.mts +10 -0
- package/dist/client/plugins/dedupe.mjs +28 -0
- package/dist/client/plugins/index.d.mts +5 -0
- package/dist/client/plugins/index.mjs +5 -0
- package/dist/client/plugins/retry.d.mts +11 -0
- package/dist/client/plugins/retry.mjs +21 -0
- package/dist/client/server.d.mts +16 -0
- package/dist/client/server.mjs +60 -0
- package/dist/client/types.d.mts +29 -0
- package/dist/codec/devalue.d.mts +21 -0
- package/dist/codec/devalue.mjs +32 -0
- package/dist/codec/msgpack.d.mts +21 -0
- package/dist/codec/msgpack.mjs +59 -0
- package/dist/compile.d.mts +52 -0
- package/dist/compile.mjs +304 -0
- package/dist/contract.d.mts +36 -0
- package/dist/contract.mjs +40 -0
- package/dist/core/error.d.mts +104 -0
- package/dist/core/error.mjs +139 -0
- package/dist/core/handler.mjs +546 -0
- package/dist/core/iterator.d.mts +17 -0
- package/dist/core/iterator.mjs +79 -0
- package/dist/core/router-utils.mjs +16 -0
- package/dist/core/schema.d.mts +19 -0
- package/dist/core/schema.mjs +26 -0
- package/dist/core/serve.mjs +38 -0
- package/dist/core/sse.d.mts +16 -0
- package/dist/core/sse.mjs +95 -0
- package/dist/core/storage.d.mts +21 -0
- package/dist/core/storage.mjs +63 -0
- package/dist/core/utils.mjs +21 -0
- package/dist/fast-stringify.mjs +125 -0
- package/dist/index.d.mts +15 -37
- package/dist/index.mjs +13 -7
- package/dist/integrations/ai/index.d.mts +25 -0
- package/dist/integrations/ai/index.mjs +116 -0
- package/dist/integrations/react/index.d.mts +83 -0
- package/dist/integrations/react/index.mjs +197 -0
- package/dist/integrations/tanstack-query/index.d.mts +120 -0
- package/dist/integrations/tanstack-query/index.mjs +100 -0
- package/dist/integrations/tanstack-query/ssr.d.mts +51 -0
- package/dist/integrations/tanstack-query/ssr.mjs +89 -0
- package/dist/integrations/zod/converter.d.mts +75 -0
- package/dist/integrations/zod/converter.mjs +345 -0
- package/dist/integrations/zod/index.d.mts +2 -0
- package/dist/integrations/zod/index.mjs +2 -0
- package/dist/lazy.d.mts +24 -0
- package/dist/lazy.mjs +27 -0
- package/dist/lifecycle.d.mts +36 -0
- package/dist/lifecycle.mjs +46 -0
- package/dist/map-input.d.mts +17 -0
- package/dist/map-input.mjs +24 -0
- package/dist/plugins/analytics.d.mts +168 -0
- package/dist/plugins/analytics.mjs +459 -0
- package/dist/plugins/batch-server.d.mts +20 -0
- package/dist/plugins/batch-server.mjs +86 -0
- package/dist/plugins/body-limit.d.mts +16 -0
- package/dist/plugins/body-limit.mjs +44 -0
- package/dist/plugins/cache.d.mts +170 -0
- package/dist/plugins/cache.mjs +200 -0
- package/dist/plugins/coerce.d.mts +21 -0
- package/dist/plugins/coerce.mjs +46 -0
- package/dist/plugins/compression.d.mts +19 -0
- package/dist/plugins/compression.mjs +23 -0
- package/dist/plugins/cookies.d.mts +44 -0
- package/dist/plugins/cookies.mjs +67 -0
- package/dist/plugins/cors.d.mts +39 -0
- package/dist/plugins/cors.mjs +56 -0
- package/dist/plugins/custom-serializer.d.mts +57 -0
- package/dist/plugins/custom-serializer.mjs +40 -0
- package/dist/plugins/file-upload.d.mts +38 -0
- package/dist/plugins/file-upload.mjs +100 -0
- package/dist/plugins/index.d.mts +16 -0
- package/dist/plugins/index.mjs +16 -0
- package/dist/plugins/otel.d.mts +35 -0
- package/dist/plugins/otel.mjs +40 -0
- package/dist/plugins/pino.d.mts +60 -0
- package/dist/plugins/pino.mjs +42 -0
- package/dist/plugins/pubsub.d.mts +50 -0
- package/dist/plugins/pubsub.mjs +53 -0
- package/dist/plugins/ratelimit.d.mts +51 -0
- package/dist/plugins/ratelimit.mjs +81 -0
- package/dist/plugins/signing.d.mts +41 -0
- package/dist/plugins/signing.mjs +115 -0
- package/dist/plugins/strict-get.d.mts +10 -0
- package/dist/plugins/strict-get.mjs +33 -0
- package/dist/route/add.mjs +240 -0
- package/dist/route/compiler.mjs +373 -0
- package/dist/route/context.mjs +12 -0
- package/dist/route/types.d.mts +11 -0
- package/dist/route/utils.mjs +17 -0
- package/dist/scalar.d.mts +53 -0
- package/dist/scalar.mjs +315 -0
- package/dist/silgi.d.mts +139 -0
- package/dist/silgi.mjs +113 -0
- package/dist/trpc-interop.d.mts +22 -0
- package/dist/trpc-interop.mjs +68 -0
- package/dist/types.d.mts +82 -0
- package/dist/ws.d.mts +42 -0
- package/dist/ws.mjs +137 -0
- package/lib/dashboard/index.html +123 -0
- package/lib/ocache.d.mts +1 -0
- package/lib/ocache.mjs +1 -0
- package/lib/ofetch.d.mts +1 -0
- package/lib/ofetch.mjs +1 -0
- package/lib/srvx.d.mts +1 -0
- package/lib/srvx.mjs +1 -0
- package/lib/unstorage.d.mts +1 -0
- package/lib/unstorage.mjs +1 -0
- package/package.json +291 -65
- package/bin/silgi.mjs +0 -3
- package/dist/chunks/generate.mjs +0 -933
- package/dist/chunks/init.mjs +0 -21
- package/dist/cli/config.d.mts +0 -19
- package/dist/cli/config.d.ts +0 -19
- package/dist/cli/config.mjs +0 -5
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.mjs +0 -119
- package/dist/index.d.ts +0 -37
- package/dist/plugins/openapi.d.mts +0 -138
- package/dist/plugins/openapi.d.ts +0 -138
- package/dist/plugins/openapi.mjs +0 -204
- package/dist/plugins/scalar.d.mts +0 -14
- package/dist/plugins/scalar.d.ts +0 -14
- package/dist/plugins/scalar.mjs +0 -66
- package/dist/shared/silgi.BMCYk2cR.mjs +0 -841
- package/dist/shared/silgi.D5qK9QOm.d.mts +0 -301
- package/dist/shared/silgi.D5qK9QOm.d.ts +0 -301
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/plugins/cookies.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cookie helpers — parse, set, and delete cookies.
|
|
4
|
+
*
|
|
5
|
+
* Lightweight utilities for working with cookies in Silgi handlers.
|
|
6
|
+
* No dependencies. Works with both serve() and handler().
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { getCookie, setCookie, deleteCookie } from "silgi/cookies"
|
|
11
|
+
*
|
|
12
|
+
* const auth = k.guard((ctx) => {
|
|
13
|
+
* const token = getCookie(ctx.headers, "session")
|
|
14
|
+
* if (!token) throw new SilgiError("UNAUTHORIZED")
|
|
15
|
+
* return { sessionToken: token }
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
/** Parse a specific cookie from a headers object or cookie string. */
|
|
20
|
+
function getCookie(headers, name) {
|
|
21
|
+
const cookieStr = typeof headers === "string" ? headers : headers.cookie ?? headers.Cookie ?? "";
|
|
22
|
+
if (!cookieStr) return void 0;
|
|
23
|
+
const prefix = `${name}=`;
|
|
24
|
+
const cookies = cookieStr.split(";");
|
|
25
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
26
|
+
const c = cookies[i].trim();
|
|
27
|
+
if (c.startsWith(prefix)) return decodeURIComponent(c.slice(prefix.length));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Parse all cookies from a headers object or cookie string. */
|
|
31
|
+
function parseCookies(headers) {
|
|
32
|
+
const cookieStr = typeof headers === "string" ? headers : headers.cookie ?? headers.Cookie ?? "";
|
|
33
|
+
if (!cookieStr) return {};
|
|
34
|
+
const result = {};
|
|
35
|
+
const cookies = cookieStr.split(";");
|
|
36
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
37
|
+
const c = cookies[i].trim();
|
|
38
|
+
const eq = c.indexOf("=");
|
|
39
|
+
if (eq === -1) continue;
|
|
40
|
+
const key = c.slice(0, eq);
|
|
41
|
+
const value = c.slice(eq + 1);
|
|
42
|
+
result[key] = decodeURIComponent(value);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/** Serialize a Set-Cookie header value. */
|
|
47
|
+
function setCookie(name, value, options = {}) {
|
|
48
|
+
const { path = "/", httpOnly = true, secure, sameSite = "lax", maxAge, expires, domain } = options;
|
|
49
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
50
|
+
if (path) cookie += `; Path=${path}`;
|
|
51
|
+
if (domain) cookie += `; Domain=${domain}`;
|
|
52
|
+
if (maxAge !== void 0) cookie += `; Max-Age=${maxAge}`;
|
|
53
|
+
if (expires) cookie += `; Expires=${expires.toUTCString()}`;
|
|
54
|
+
if (httpOnly) cookie += "; HttpOnly";
|
|
55
|
+
if (secure ?? (sameSite === "none" || typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV === "production")) cookie += "; Secure";
|
|
56
|
+
cookie += `; SameSite=${sameSite.charAt(0).toUpperCase() + sameSite.slice(1)}`;
|
|
57
|
+
return cookie;
|
|
58
|
+
}
|
|
59
|
+
/** Create a Set-Cookie header that deletes a cookie. */
|
|
60
|
+
function deleteCookie(name, options = {}) {
|
|
61
|
+
return setCookie(name, "", {
|
|
62
|
+
...options,
|
|
63
|
+
maxAge: 0
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { deleteCookie, getCookie, parseCookies, setCookie };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/plugins/cors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* CORS plugin — v2 hook-based.
|
|
4
|
+
*
|
|
5
|
+
* Adds CORS headers to every response via lifecycle hooks.
|
|
6
|
+
* Handles preflight OPTIONS requests automatically.
|
|
7
|
+
*/
|
|
8
|
+
interface CORSOptions {
|
|
9
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
10
|
+
methods?: string[];
|
|
11
|
+
allowedHeaders?: string[];
|
|
12
|
+
exposedHeaders?: string[];
|
|
13
|
+
credentials?: boolean;
|
|
14
|
+
maxAge?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create CORS hooks for silgi().
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { cors } from "silgi/cors"
|
|
22
|
+
*
|
|
23
|
+
* const k = silgi({
|
|
24
|
+
* context: (req) => ({}),
|
|
25
|
+
* hooks: cors({ origin: "https://app.example.com", credentials: true }),
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function cors(options?: CORSOptions): {
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
options: CORSOptions;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* CORS header map — use in custom serve() or middleware.
|
|
35
|
+
* Returns headers object to merge into responses.
|
|
36
|
+
*/
|
|
37
|
+
declare function corsHeaders(options?: CORSOptions, requestOrigin?: string): Record<string, string>;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { CORSOptions, cors, corsHeaders };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/plugins/cors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create CORS hooks for silgi().
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { cors } from "silgi/cors"
|
|
8
|
+
*
|
|
9
|
+
* const k = silgi({
|
|
10
|
+
* context: (req) => ({}),
|
|
11
|
+
* hooks: cors({ origin: "https://app.example.com", credentials: true }),
|
|
12
|
+
* })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
function cors(options = {}) {
|
|
16
|
+
const origin = options.origin ?? "*";
|
|
17
|
+
if (options.credentials && typeof origin === "string" && origin === "*") throw new Error("[silgi/cors] Cannot use credentials: true with origin: \"*\". Set an explicit origin.");
|
|
18
|
+
return {
|
|
19
|
+
headers: corsHeaders(options),
|
|
20
|
+
options
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* CORS header map — use in custom serve() or middleware.
|
|
25
|
+
* Returns headers object to merge into responses.
|
|
26
|
+
*/
|
|
27
|
+
function corsHeaders(options = {}, requestOrigin) {
|
|
28
|
+
const origin = options.origin ?? "*";
|
|
29
|
+
const methods = options.methods ?? [
|
|
30
|
+
"GET",
|
|
31
|
+
"POST",
|
|
32
|
+
"PUT",
|
|
33
|
+
"PATCH",
|
|
34
|
+
"DELETE",
|
|
35
|
+
"OPTIONS"
|
|
36
|
+
];
|
|
37
|
+
const allowedHeaders = options.allowedHeaders ?? ["Content-Type", "Authorization"];
|
|
38
|
+
const headers = {
|
|
39
|
+
"access-control-allow-methods": methods.join(", "),
|
|
40
|
+
"access-control-allow-headers": allowedHeaders.join(", ")
|
|
41
|
+
};
|
|
42
|
+
if (typeof origin === "string") headers["access-control-allow-origin"] = origin;
|
|
43
|
+
else if (Array.isArray(origin)) {
|
|
44
|
+
headers["access-control-allow-origin"] = requestOrigin && origin.includes(requestOrigin) ? requestOrigin : "";
|
|
45
|
+
headers["vary"] = "Origin";
|
|
46
|
+
} else if (typeof origin === "function" && requestOrigin) {
|
|
47
|
+
headers["access-control-allow-origin"] = origin(requestOrigin) ? requestOrigin : "";
|
|
48
|
+
headers["vary"] = "Origin";
|
|
49
|
+
}
|
|
50
|
+
if (options.credentials) headers["access-control-allow-credentials"] = "true";
|
|
51
|
+
if (options.maxAge !== void 0) headers["access-control-max-age"] = String(options.maxAge);
|
|
52
|
+
if (options.exposedHeaders) headers["access-control-expose-headers"] = options.exposedHeaders.join(", ");
|
|
53
|
+
return headers;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { cors, corsHeaders };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/plugins/custom-serializer.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Custom JSON serializers — extend serialization for custom types.
|
|
4
|
+
*
|
|
5
|
+
* Register custom type handlers that run during JSON stringify/parse.
|
|
6
|
+
* Works with both serve() responses and client deserialization.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createSerializer } from "silgi/plugins"
|
|
11
|
+
*
|
|
12
|
+
* const serializer = createSerializer()
|
|
13
|
+
* .register("Date", {
|
|
14
|
+
* test: (v) => v instanceof Date,
|
|
15
|
+
* serialize: (v) => v.toISOString(),
|
|
16
|
+
* deserialize: (v) => new Date(v),
|
|
17
|
+
* })
|
|
18
|
+
* .register("BigInt", {
|
|
19
|
+
* test: (v) => typeof v === "bigint",
|
|
20
|
+
* serialize: (v) => v.toString(),
|
|
21
|
+
* deserialize: (v) => BigInt(v),
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Use as JSON replacer/reviver
|
|
25
|
+
* JSON.stringify(data, serializer.replacer)
|
|
26
|
+
* JSON.parse(text, serializer.reviver)
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
interface TypeHandler<T = unknown> {
|
|
30
|
+
/** Test if a value is this type */
|
|
31
|
+
test: (value: unknown) => boolean;
|
|
32
|
+
/** Convert to JSON-safe value */
|
|
33
|
+
serialize: (value: T) => unknown;
|
|
34
|
+
/** Convert back from JSON-safe value */
|
|
35
|
+
deserialize: (value: unknown) => T;
|
|
36
|
+
}
|
|
37
|
+
interface Serializer {
|
|
38
|
+
/** Register a custom type handler. Returns self for chaining. */
|
|
39
|
+
register<T>(tag: string, handler: TypeHandler<T>): Serializer;
|
|
40
|
+
/** JSON.stringify replacer function */
|
|
41
|
+
replacer: (key: string, value: unknown) => unknown;
|
|
42
|
+
/** JSON.parse reviver function */
|
|
43
|
+
reviver: (key: string, value: unknown) => unknown;
|
|
44
|
+
/** Stringify with custom types */
|
|
45
|
+
stringify: (value: unknown) => string;
|
|
46
|
+
/** Parse with custom types */
|
|
47
|
+
parse: (text: string) => unknown;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a custom serializer with support for non-JSON types.
|
|
51
|
+
*
|
|
52
|
+
* Values are wrapped as `{ __$type: "Tag", __$value: serialized }`
|
|
53
|
+
* during stringify and unwrapped during parse.
|
|
54
|
+
*/
|
|
55
|
+
declare function createSerializer(): Serializer;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { Serializer, TypeHandler, createSerializer };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/plugins/custom-serializer.ts
|
|
2
|
+
const TYPE_TAG = "__$type";
|
|
3
|
+
const VALUE_TAG = "__$value";
|
|
4
|
+
/**
|
|
5
|
+
* Create a custom serializer with support for non-JSON types.
|
|
6
|
+
*
|
|
7
|
+
* Values are wrapped as `{ __$type: "Tag", __$value: serialized }`
|
|
8
|
+
* during stringify and unwrapped during parse.
|
|
9
|
+
*/
|
|
10
|
+
function createSerializer() {
|
|
11
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
12
|
+
const replacer = (_key, value) => {
|
|
13
|
+
for (const [tag, handler] of handlers) if (handler.test(value)) return {
|
|
14
|
+
[TYPE_TAG]: tag,
|
|
15
|
+
[VALUE_TAG]: handler.serialize(value)
|
|
16
|
+
};
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
const reviver = (_key, value) => {
|
|
20
|
+
if (typeof value === "object" && value !== null && TYPE_TAG in value && VALUE_TAG in value) {
|
|
21
|
+
const tag = value[TYPE_TAG];
|
|
22
|
+
const handler = handlers.get(tag);
|
|
23
|
+
if (handler) return handler.deserialize(value[VALUE_TAG]);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
const serializer = {
|
|
28
|
+
register(tag, handler) {
|
|
29
|
+
handlers.set(tag, handler);
|
|
30
|
+
return serializer;
|
|
31
|
+
},
|
|
32
|
+
replacer,
|
|
33
|
+
reviver,
|
|
34
|
+
stringify: (value) => JSON.stringify(value, replacer),
|
|
35
|
+
parse: (text) => JSON.parse(text, reviver)
|
|
36
|
+
};
|
|
37
|
+
return serializer;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { createSerializer };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { GuardDef } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/file-upload.d.ts
|
|
4
|
+
interface FileGuardOptions {
|
|
5
|
+
/** Maximum file size in bytes. Default: 10MB */
|
|
6
|
+
maxFileSize?: number;
|
|
7
|
+
/** Allowed MIME types (supports wildcards like "image/*"). Default: all */
|
|
8
|
+
allowedTypes?: string[];
|
|
9
|
+
/** Maximum number of files. Default: 1 */
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
/** Form field name for the file. Default: "file" */
|
|
12
|
+
fieldName?: string;
|
|
13
|
+
}
|
|
14
|
+
interface UploadedFile {
|
|
15
|
+
name: string;
|
|
16
|
+
size: number;
|
|
17
|
+
type: string;
|
|
18
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
19
|
+
text(): Promise<string>;
|
|
20
|
+
stream(): ReadableStream<Uint8Array>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Guard that parses multipart file uploads from the request.
|
|
24
|
+
*
|
|
25
|
+
* Adds `ctx.file` (single) or `ctx.files` (multiple) to the context.
|
|
26
|
+
* Validates file size and MIME type before the procedure runs.
|
|
27
|
+
*/
|
|
28
|
+
declare function fileGuard(options?: FileGuardOptions): GuardDef<Record<string, unknown>>;
|
|
29
|
+
/**
|
|
30
|
+
* Parse multipart form data from a Request.
|
|
31
|
+
* Returns files and fields separately.
|
|
32
|
+
*/
|
|
33
|
+
declare function parseMultipart(request: Request): Promise<{
|
|
34
|
+
files: UploadedFile[];
|
|
35
|
+
fields: Record<string, string>;
|
|
36
|
+
}>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { FileGuardOptions, UploadedFile, fileGuard, parseMultipart };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SilgiError } from "../core/error.mjs";
|
|
2
|
+
//#region src/plugins/file-upload.ts
|
|
3
|
+
/**
|
|
4
|
+
* File upload/download — type-safe File/Blob handling.
|
|
5
|
+
*
|
|
6
|
+
* Parse multipart/form-data requests and pass File objects
|
|
7
|
+
* to procedure handlers. Works with serve() and handler().
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { fileInput, fileGuard } from "silgi/plugins"
|
|
12
|
+
*
|
|
13
|
+
* const uploadAvatar = k
|
|
14
|
+
* .$use(fileGuard({ maxFileSize: 5 * 1024 * 1024, allowedTypes: ["image/*"] }))
|
|
15
|
+
* .$resolve(async ({ ctx }) => {
|
|
16
|
+
* const file = ctx.file
|
|
17
|
+
* const buffer = await file.arrayBuffer()
|
|
18
|
+
* return { name: file.name, size: file.size, type: file.type }
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Guard that parses multipart file uploads from the request.
|
|
24
|
+
*
|
|
25
|
+
* Adds `ctx.file` (single) or `ctx.files` (multiple) to the context.
|
|
26
|
+
* Validates file size and MIME type before the procedure runs.
|
|
27
|
+
*/
|
|
28
|
+
function fileGuard(options = {}) {
|
|
29
|
+
const { maxFileSize = 10 * 1024 * 1024, allowedTypes, maxFiles = 1, fieldName: _fieldName = "file" } = options;
|
|
30
|
+
return {
|
|
31
|
+
kind: "guard",
|
|
32
|
+
fn: (ctx) => {
|
|
33
|
+
const files = ctx.__files;
|
|
34
|
+
if (!files || files.length === 0) throw new SilgiError("BAD_REQUEST", {
|
|
35
|
+
status: 400,
|
|
36
|
+
message: "No file uploaded"
|
|
37
|
+
});
|
|
38
|
+
if (files.length > maxFiles) throw new SilgiError("BAD_REQUEST", {
|
|
39
|
+
status: 400,
|
|
40
|
+
message: `Too many files: ${files.length} (max ${maxFiles})`
|
|
41
|
+
});
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
if (file.size > maxFileSize) throw new SilgiError("PAYLOAD_TOO_LARGE", {
|
|
44
|
+
status: 413,
|
|
45
|
+
message: `File too large: ${file.size} bytes (max ${maxFileSize})`,
|
|
46
|
+
data: {
|
|
47
|
+
maxFileSize,
|
|
48
|
+
actualSize: file.size,
|
|
49
|
+
fileName: file.name
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (allowedTypes && !matchesMimeType(file.type, allowedTypes)) throw new SilgiError("BAD_REQUEST", {
|
|
53
|
+
status: 400,
|
|
54
|
+
message: `File type not allowed: ${file.type}`,
|
|
55
|
+
data: {
|
|
56
|
+
allowedTypes,
|
|
57
|
+
actualType: file.type,
|
|
58
|
+
fileName: file.name
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return maxFiles === 1 ? { file: files[0] } : { files };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse multipart form data from a Request.
|
|
68
|
+
* Returns files and fields separately.
|
|
69
|
+
*/
|
|
70
|
+
async function parseMultipart(request) {
|
|
71
|
+
const formData = await request.formData();
|
|
72
|
+
const files = [];
|
|
73
|
+
const fields = {};
|
|
74
|
+
for (const [key, value] of formData.entries()) if (value instanceof File) files.push({
|
|
75
|
+
name: value.name,
|
|
76
|
+
size: value.size,
|
|
77
|
+
type: value.type,
|
|
78
|
+
arrayBuffer: () => value.arrayBuffer(),
|
|
79
|
+
text: () => value.text(),
|
|
80
|
+
stream: () => value.stream()
|
|
81
|
+
});
|
|
82
|
+
else fields[key] = value;
|
|
83
|
+
return {
|
|
84
|
+
files,
|
|
85
|
+
fields
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function matchesMimeType(actual, patterns) {
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
if (pattern === "*" || pattern === "*/*") return true;
|
|
91
|
+
if (pattern === actual) return true;
|
|
92
|
+
if (pattern.endsWith("/*")) {
|
|
93
|
+
const prefix = pattern.slice(0, -2);
|
|
94
|
+
if (actual.startsWith(prefix + "/")) return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { fileGuard, parseMultipart };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AnalyticsCollector, AnalyticsOptions, AnalyticsSnapshot, ErrorEntry, ProcedureSnapshot, RequestTrace, TraceSpan, analyticsHTML, errorToMarkdown, trace } from "./analytics.mjs";
|
|
2
|
+
import { CookieOptions, deleteCookie, getCookie, parseCookies, setCookie } from "./cookies.mjs";
|
|
3
|
+
import { CORSOptions, cors, corsHeaders } from "./cors.mjs";
|
|
4
|
+
import { Span, Tracer, otelWrap } from "./otel.mjs";
|
|
5
|
+
import { Logger, LoggingOptions, loggingHooks } from "./pino.mjs";
|
|
6
|
+
import { MemoryRateLimiter, MemoryRateLimiterOptions, RateLimitGuardOptions, RateLimitResult, RateLimiter, rateLimitGuard } from "./ratelimit.mjs";
|
|
7
|
+
import { CompressionOptions, compressionWrap } from "./compression.mjs";
|
|
8
|
+
import { BodyLimitOptions, bodyLimitGuard } from "./body-limit.mjs";
|
|
9
|
+
import { strictGetGuard } from "./strict-get.mjs";
|
|
10
|
+
import { decrypt, encrypt, sign, unsign } from "./signing.mjs";
|
|
11
|
+
import { coerceGuard, coerceObject, coerceValue } from "./coerce.mjs";
|
|
12
|
+
import { BatchHandlerOptions, createBatchHandler } from "./batch-server.mjs";
|
|
13
|
+
import { MemoryPubSub, PubSubBackend, Publisher, createPublisher } from "./pubsub.mjs";
|
|
14
|
+
import { Serializer, TypeHandler, createSerializer } from "./custom-serializer.mjs";
|
|
15
|
+
import { FileGuardOptions, UploadedFile, fileGuard, parseMultipart } from "./file-upload.mjs";
|
|
16
|
+
export { AnalyticsCollector, type AnalyticsOptions, type AnalyticsSnapshot, type BatchHandlerOptions, type BodyLimitOptions, type CORSOptions, type CompressionOptions, type CookieOptions, type ErrorEntry, type FileGuardOptions, type Logger, type LoggingOptions, MemoryPubSub, MemoryRateLimiter, type MemoryRateLimiterOptions, type ProcedureSnapshot, type PubSubBackend, type Publisher, type RateLimitGuardOptions, type RateLimitResult, type RateLimiter, RequestTrace, type Serializer, type Span, type TraceSpan, type Tracer, type TypeHandler, type UploadedFile, analyticsHTML, bodyLimitGuard, coerceGuard, coerceObject, coerceValue, compressionWrap, cors, corsHeaders, createBatchHandler, createPublisher, createSerializer, decrypt, deleteCookie, encrypt, errorToMarkdown, fileGuard, getCookie, loggingHooks, otelWrap, parseCookies, parseMultipart, rateLimitGuard, setCookie, sign, strictGetGuard, trace, unsign };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AnalyticsCollector, RequestTrace, analyticsHTML, errorToMarkdown, trace } from "./analytics.mjs";
|
|
2
|
+
import { cors, corsHeaders } from "./cors.mjs";
|
|
3
|
+
import { otelWrap } from "./otel.mjs";
|
|
4
|
+
import { loggingHooks } from "./pino.mjs";
|
|
5
|
+
import { MemoryRateLimiter, rateLimitGuard } from "./ratelimit.mjs";
|
|
6
|
+
import { compressionWrap } from "./compression.mjs";
|
|
7
|
+
import { bodyLimitGuard } from "./body-limit.mjs";
|
|
8
|
+
import { strictGetGuard } from "./strict-get.mjs";
|
|
9
|
+
import { deleteCookie, getCookie, parseCookies, setCookie } from "./cookies.mjs";
|
|
10
|
+
import { decrypt, encrypt, sign, unsign } from "./signing.mjs";
|
|
11
|
+
import { coerceGuard, coerceObject, coerceValue } from "./coerce.mjs";
|
|
12
|
+
import { createBatchHandler } from "./batch-server.mjs";
|
|
13
|
+
import { MemoryPubSub, createPublisher } from "./pubsub.mjs";
|
|
14
|
+
import { createSerializer } from "./custom-serializer.mjs";
|
|
15
|
+
import { fileGuard, parseMultipart } from "./file-upload.mjs";
|
|
16
|
+
export { AnalyticsCollector, MemoryPubSub, MemoryRateLimiter, RequestTrace, analyticsHTML, bodyLimitGuard, coerceGuard, coerceObject, coerceValue, compressionWrap, cors, corsHeaders, createBatchHandler, createPublisher, createSerializer, decrypt, deleteCookie, encrypt, errorToMarkdown, fileGuard, getCookie, loggingHooks, otelWrap, parseCookies, parseMultipart, rateLimitGuard, setCookie, sign, strictGetGuard, trace, unsign };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WrapDef } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/otel.d.ts
|
|
4
|
+
interface Span {
|
|
5
|
+
setAttribute(key: string, value: string | number | boolean): void;
|
|
6
|
+
setStatus(status: {
|
|
7
|
+
code: number;
|
|
8
|
+
message?: string;
|
|
9
|
+
}): void;
|
|
10
|
+
addEvent(name: string, attributes?: Record<string, unknown>): void;
|
|
11
|
+
end(): void;
|
|
12
|
+
}
|
|
13
|
+
interface Tracer {
|
|
14
|
+
startSpan(name: string, options?: {
|
|
15
|
+
attributes?: Record<string, string | number | boolean>;
|
|
16
|
+
}): Span;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an OTel tracing wrap middleware.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { otelWrap } from "silgi/otel"
|
|
24
|
+
* import { trace } from "@opentelemetry/api"
|
|
25
|
+
*
|
|
26
|
+
* const tracing = otelWrap(trace.getTracer("my-service"))
|
|
27
|
+
*
|
|
28
|
+
* const proc = k
|
|
29
|
+
* .$use(tracing)
|
|
30
|
+
* .$resolve(({ ctx }) => ctx.db.find())
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function otelWrap(tracer: Tracer, spanName?: string): WrapDef;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { Span, Tracer, otelWrap };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/plugins/otel.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create an OTel tracing wrap middleware.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { otelWrap } from "silgi/otel"
|
|
8
|
+
* import { trace } from "@opentelemetry/api"
|
|
9
|
+
*
|
|
10
|
+
* const tracing = otelWrap(trace.getTracer("my-service"))
|
|
11
|
+
*
|
|
12
|
+
* const proc = k
|
|
13
|
+
* .$use(tracing)
|
|
14
|
+
* .$resolve(({ ctx }) => ctx.db.find())
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function otelWrap(tracer, spanName = "rpc.call") {
|
|
18
|
+
return {
|
|
19
|
+
kind: "wrap",
|
|
20
|
+
fn: async (_ctx, next) => {
|
|
21
|
+
const span = tracer.startSpan(spanName, { attributes: { "rpc.system": "silgi" } });
|
|
22
|
+
try {
|
|
23
|
+
const result = await next();
|
|
24
|
+
span.setStatus({ code: 0 });
|
|
25
|
+
return result;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
span.setStatus({
|
|
28
|
+
code: 2,
|
|
29
|
+
message: String(error)
|
|
30
|
+
});
|
|
31
|
+
span.addEvent("exception", { "exception.message": error instanceof Error ? error.message : String(error) });
|
|
32
|
+
throw error;
|
|
33
|
+
} finally {
|
|
34
|
+
span.end();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { otelWrap };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/plugins/pino.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pino logging plugin — v2 hook-based.
|
|
4
|
+
*
|
|
5
|
+
* Logs request lifecycle events via silgi hooks.
|
|
6
|
+
*/
|
|
7
|
+
interface Logger {
|
|
8
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
9
|
+
info(obj: Record<string, unknown>, msg?: string): void;
|
|
10
|
+
error(obj: Record<string, unknown>, msg?: string): void;
|
|
11
|
+
warn(obj: Record<string, unknown>, msg?: string): void;
|
|
12
|
+
debug(obj: Record<string, unknown>, msg?: string): void;
|
|
13
|
+
}
|
|
14
|
+
interface LoggingOptions {
|
|
15
|
+
/** The root logger instance */
|
|
16
|
+
logger: Logger;
|
|
17
|
+
/** Log request received events (default: true) */
|
|
18
|
+
logRequests?: boolean;
|
|
19
|
+
/** Log response events (default: true) */
|
|
20
|
+
logResponses?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create logging hooks for silgi().
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import pino from "pino"
|
|
28
|
+
* import { loggingHooks } from "silgi/pino"
|
|
29
|
+
*
|
|
30
|
+
* const k = silgi({
|
|
31
|
+
* context: (req) => ({}),
|
|
32
|
+
* hooks: loggingHooks({ logger: pino() }),
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function loggingHooks(options: LoggingOptions): {
|
|
37
|
+
error: ({
|
|
38
|
+
path,
|
|
39
|
+
error
|
|
40
|
+
}: {
|
|
41
|
+
path: string;
|
|
42
|
+
error: unknown;
|
|
43
|
+
}) => void;
|
|
44
|
+
response?: (({
|
|
45
|
+
path,
|
|
46
|
+
durationMs
|
|
47
|
+
}: {
|
|
48
|
+
path: string;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
}) => void) | undefined;
|
|
51
|
+
request?: (({
|
|
52
|
+
path,
|
|
53
|
+
input
|
|
54
|
+
}: {
|
|
55
|
+
path: string;
|
|
56
|
+
input: unknown;
|
|
57
|
+
}) => void) | undefined;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { Logger, LoggingOptions, loggingHooks };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/plugins/pino.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create logging hooks for silgi().
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import pino from "pino"
|
|
8
|
+
* import { loggingHooks } from "silgi/pino"
|
|
9
|
+
*
|
|
10
|
+
* const k = silgi({
|
|
11
|
+
* context: (req) => ({}),
|
|
12
|
+
* hooks: loggingHooks({ logger: pino() }),
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function loggingHooks(options) {
|
|
17
|
+
const { logger } = options;
|
|
18
|
+
const logRequests = options.logRequests ?? true;
|
|
19
|
+
const logResponses = options.logResponses ?? true;
|
|
20
|
+
return {
|
|
21
|
+
...logRequests && { request: ({ path, input }) => {
|
|
22
|
+
logger.info({
|
|
23
|
+
path,
|
|
24
|
+
hasInput: input !== void 0
|
|
25
|
+
}, "request received");
|
|
26
|
+
} },
|
|
27
|
+
...logResponses && { response: ({ path, durationMs }) => {
|
|
28
|
+
logger.info({
|
|
29
|
+
path,
|
|
30
|
+
durationMs: Math.round(durationMs * 100) / 100
|
|
31
|
+
}, "response sent");
|
|
32
|
+
} },
|
|
33
|
+
error: ({ path, error }) => {
|
|
34
|
+
logger.error({
|
|
35
|
+
path,
|
|
36
|
+
error: error instanceof Error ? error.message : String(error)
|
|
37
|
+
}, "request error");
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { loggingHooks };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/plugins/pubsub.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Publisher/PubSub — event pub/sub with pluggable backends.
|
|
4
|
+
*
|
|
5
|
+
* Publish events from any procedure, subscribe from SSE/WebSocket.
|
|
6
|
+
* Built-in memory adapter. Redis/Upstash adapters plug in via interface.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createPublisher, MemoryPubSub } from "silgi/plugins"
|
|
11
|
+
*
|
|
12
|
+
* const pubsub = createPublisher(new MemoryPubSub())
|
|
13
|
+
*
|
|
14
|
+
* // Publish from a mutation
|
|
15
|
+
* const createUser = k.$resolve(async ({ input, ctx }) => {
|
|
16
|
+
* const user = await ctx.db.users.create(input)
|
|
17
|
+
* await pubsub.publish("user:created", user)
|
|
18
|
+
* return user
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* // Subscribe via SSE
|
|
22
|
+
* const onUserCreated = k.subscription(async function* () {
|
|
23
|
+
* yield* pubsub.subscribe("user:created")
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
interface PubSubBackend {
|
|
28
|
+
publish(channel: string, data: unknown): Promise<void>;
|
|
29
|
+
subscribe(channel: string, callback: (data: unknown) => void): () => void;
|
|
30
|
+
}
|
|
31
|
+
declare class MemoryPubSub implements PubSubBackend {
|
|
32
|
+
#private;
|
|
33
|
+
publish(channel: string, data: unknown): Promise<void>;
|
|
34
|
+
subscribe(channel: string, callback: (data: unknown) => void): () => void;
|
|
35
|
+
}
|
|
36
|
+
interface Publisher {
|
|
37
|
+
/** Publish an event to a channel */
|
|
38
|
+
publish(channel: string, data: unknown): Promise<void>;
|
|
39
|
+
/** Subscribe to a channel — returns an async iterable for use in subscriptions */
|
|
40
|
+
subscribe<T = unknown>(channel: string): AsyncGenerator<T, void, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a publisher from a PubSub backend.
|
|
44
|
+
*
|
|
45
|
+
* The publisher exposes `publish()` for mutations and `subscribe()`
|
|
46
|
+
* as an async generator for SSE/WebSocket subscriptions.
|
|
47
|
+
*/
|
|
48
|
+
declare function createPublisher(backend: PubSubBackend): Publisher;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { MemoryPubSub, PubSubBackend, Publisher, createPublisher };
|